NOSSの雑記

主に競プロでやったことを書きます。

ABC120 参加記

atcoder.jp

コンテストお疲れ様でした。

A - Favorite Sound (100)

A - Favorite Sound

A/BとCの小さいほうが答えになります

#include <bits/stdc++.h>
using namespace std;

int main() {
    int a, b, c;
    cin >> a >> b >> c;
    cout << min(c,b/a) << endl;
    return 0;
}

B - K-th Common Divisor (200)

B - K-th Common Divisor

AとBの公約数のうちK番目に大きい数を求めよ。

  • 1 \le A,B \le 100

A,Bが十分小さいので難しいことは考えなくても全探索できます。

#include <bits/stdc++.h>
using namespace std;

int main() {
    int a, b, k;
    cin >> a >> b >> k;
    for(int i=min(a,b); i>0; i--){
        if(a%i==0 && b%i==0)k--;
        if(k==0){
            cout << i << endl;
            break;
        }
    }
    return 0;
}

C - Unification (300)

C - Unification

0,1のみからなる長さNの文字列Sが与えられる。となり合う0と1を取り除くという操作を何度でも行えるとき最大で何文字取り除けるか求めよ。

  • 1 \le N \le 10^{5}

最終的に文字列がどのような状態になっているか考えてみましょう。もしまだ0,1の両方が残っているならばどこかで0と1が必ず隣接しているはずなので取り除く操作を行えます(両方残っているのに取り除けないという状態はありえない)。

なので操作が行えなくなったとき残っているのは0,1のいずれかのみです。1回の操作で0,1は1つずつ減るので操作を行える回数は min(0の個数,1の個数) になります。

#include <bits/stdc++.h>
using namespace std;

int main() {
    string s;
    cin >> s;
    int cnt[2]{};
    for(auto c : s){
        cnt[c-'0']++;
    }
    cout << min(cnt[0],cnt[1])*2 << endl;
    return 0;
}

D - Decayed Bridges (400)

D - Decayed Bridges

N頂点M辺の連結な無向グラフがあります。ここからM本の辺を1番目から順に削除します。i番目の辺を削除したとき連結でない頂点の組(a,b)の個数をもとめよ。(ただしa < b)

  • 2 \le N \le 10^{5}
  • 1 \le M \le 10^{5}
  • 多重辺や自己ループは存在しない

頂点が連結であるかどうかを高速に判定するためにはUnionFindTreeが使えます。しかし、通常のUF木では辺を削除する(集合を分割する)操作は行えません。

ここで時系列を逆から見てみます。すると 1, 2, 3, ..., M-1, M の順に辺を削除する操作は M, M-1, ..., 3, 2, 1 の順に辺をつないでいく操作とみなすことができます! これはUF木で実現可能です。

すべての辺が削除されているときは任意のペアが連結でないので個数は N(N-1)/2 です。辺で頂点a,bをつなぐときa,bがまだ連結でないならばペアの個数が減ります。このとき減るペアはaが属する連結成分にある頂点とbが属する連結成分にある頂点をペアにしたものなのでその個数は(aを含む連結成分の頂点数)*(bを含む連結成分の頂点数)になります。これを新たに辺をつなぐ度に引いていけばよいです。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> P;

struct UnionFindTree {
    vector<int> par;
    vector<int> rank;
    vector<int> siz;

    void init(int n) {
        par.resize(n);
        rank.resize(n);
        siz.resize(n);
        for (int i = 0; i < n; i++) {
            par[i] = i;
            rank[i] = 0;
            siz[i] = 1;
        }
    }
    int find(int x) {
        if (par[x] == x) {
            return x;
        } else {
            return par[x] = find(par[x]);
        }
    }
    void unite(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) return;
        if (rank[x] < rank[y]) swap(x, y);
        if (rank[x] == rank[y]) rank[x]++;
        par[y] = x;
        siz[x] += siz[y];
    }
    bool is_same(int x, int y) {
        return find(x) == find(y);
    }
    int size(int x) {
        x = find(x);
        return siz[x];
    }
};

int main() {
    ll n, m;
    cin >> n >> m;
    vector<P> es;
    for(int i=0; i<m; i++){
        int a, b;
        cin >> a >> b;
        a--; b--;
        es.push_back({a,b});
    }
    UnionFindTree g;
    g.init(n);
    vector<ll> ans(m);
    ll cnt = n*(n-1)/2;
    for(int i=m-1; i>=0; i--){
        ans[i] = cnt;
        int a = es[i].first, b = es[i].second;
        if(g.is_same(a,b)) continue;
        cnt -= ll(g.size(a)) * ll(g.size(b));
        g.unite(a,b);
    }
    for(int i=0; i<m; i++){
        cout << ans[i] << endl;
    }
    return 0;
}

結果

結果はABCDの4完15:17(+1WA)で61位でした。Dでlonglongにし忘れていなければ30位には入っていたようですが速度的には今のベストが出せたと思います。

Dの逆からUnionFindはよい知見ですね。