Disjoint Set(Union Find木)を一から書けるようにする
将来のコーディング試験に備えて。そのうちfenwick treeとかほかのも書くかもしれない。(typedef long long ll;とかrepマクロの定義は省略する)
あくまで自分のための備忘録なので、初めて組む人はこっち見た方がいいと思う Union-Find Tree を理解する!素集合系を扱うデータ構造 | アルゴリズムロジック
コーディングの順番としては、 ノードの定義→findRoot→isSame→unite(→Setsize) とするのが自然な気がする。
まずは普通にノードを定義する。集合の大きさがよく問われる気がするので、sizeによる併合を考えておく。
private: vector<ll> sizes, parents; public: disjoint_set(ll N) { sizes.resize(N); parents.resize(N); rep(i, N) { sizes[i] = 1; parents[i] = i; } }
sizesとか直で見れないようにしておく。後でわかるが、sizes[i]はノードiを含む集合の大きさになるとは限らないからだ(sizes[iの親ノード]が集合の大きさになる)。
次に集合の代表ノード(根)を見つける。
ll findRoot(ll x) { if(parents[x] != x) parents[x] = findRoot(parents[x]); // 経路圧縮 return parents[x]; }
経路圧縮してO(log(N))にする。 そして、uniteとisSameを実装する。isSameからuniteが自然かな。uniteは「長いものに巻かれろ」という覚え方で、小さい方から大きい方に接続する。
bool isSame(ll x, ll y) { return findRoot(x) == findRoot(y); } bool unite(ll x, ll y) { if (isSame(x, y)) return false; x = findRoot(x); y = findRoot(y); if (sizes[x] > sizes[y]) { // サイズが小さい方を大きい方につける parents[y] = x; sizes[x] += sizes[y]; } else { parents[x] = y; sizes[y] += sizes[x]; } return true; }
これだとunite時に無駄に二回findRootしていることになるが、まあ定数倍だし、こっちの方がわかりやすいのでこのままにしておこうと思う。小さい木を大きい方につけることで、経路圧縮と合わせて計算量はほとんど定数倍(O(α))に落ちる。 最後にSetsizeを実装して終わり。
ll Setsize(ll x) {
return sizes[findRoot(x)];
}
全体としてはこんな感じ:
struct disjoint_set{ private: vector<ll> sizes, parents; public: disjoint_set(ll N) { sizes.resize(N); parents.resize(N); rep(i, N) { sizes[i] = 1; parents[i] = i; } } ll findRoot(ll x) { if(parents[x] != x) parents[x] = findRoot(parents[x]); // 経路圧縮 return parents[x]; } bool isSame(ll x, ll y) { return findRoot(x) == findRoot(y); } bool unite(ll x, ll y) { if (isSame(x, y)) return false; x = findRoot(x); y = findRoot(y); if (sizes[x] > sizes[y]) { // サイズが小さい方を大きい方につける parents[y] = x; sizes[x] += sizes[y]; } else { parents[x] = y; sizes[y] += sizes[x]; } return true; } ll Setsize(ll x) { return sizes[findRoot(x)]; } };
verifiedする:
よさそう。