自己流union-findの使い方
この記事では自分の愛用しているデータ構造であるunion-find木の自分のつかいかたについて書きます。
あくまで「自分の」使い方なのでもっといい使い方があるかもしれません。
この記事では、union-find木がどういうデータ構造かは知っているものとして話を進めます。
1.最小全域木
とりあえず一般的に一番union-find木が使われるシチュエーションのはず。
みんな大好きクラスカル法です。多分これについてはいうことはないので割愛します。プリムとかいうアルゴリズムでもできるらしいし。(使ったことない)
2.本当にunion-findが想定解の場合
50問に1問くらいの割合で(これは体感なのでどれくらいかはわからない)、本当にunion-find木が想定解っぽくて、かつ最小全域木問題ではないような問題があります。大体の人がUF木つかうのはここまでだと思っている。
http://www.codeforces.com/problemset/problem/217/A
これとか。簡単。
http://poj.org/problem?id=1291
ちょっと変わった問題だけどこれは面白い。良問。
http://poj.org/problem?id=2985
他のデータ構造と組み合わせて使うこともあります。
これらの問題に共通して言えることは、「集合を適当な順序でくっつけていって、連結成分について何か聞かれるので答えてください」ということです。このような問題の場合、union-findは「適当な順序で」集合を併合していけるという点で強いです(集合に要素を1個ずつ追加していくんだったら別にvectorかなにかをもっておけば事足りる)。
実際このような問題はそんなに多くなくて、setとかsegtreeを使う問題のほうが多い印象があります。ここまでは普通の人の使い方です。
3.連結成分の検出
実はunion-findはグラフに対して、「連結成分はいくつありますか」とか「各連結成分ごとに何かをする」という作業に強いです。連結成分の検出は幅優先探索をすればできますが、バグりやすい上にデバッグしにくく、また問題ごとに形が変わるのでライブラリがあればunion-find木を使うほうが楽です。オーダーもα(n)倍になるだけだし実際定数くらいです。
以下にグリッドグラフ上で連結成分を全部検出するコードを示します。ここでは周囲8方向を連結だとみなします。
#include<stdio.h> #include<vector> #include<algorithm> using namespace std; #define LIMIT 252 typedef pair<int,int>pii; pii par[LIMIT][LIMIT]; int ran[LIMIT][LIMIT]; int ren[LIMIT][LIMIT]; void init() { for(int i=0;i<LIMIT;i++) { for(int j=0;j<LIMIT;j++) { par[i][j]=make_pair(i,j); ran[i][j]=0; ren[i][j]=1; } } } pii find(pii a) { if(par[a.first][a.second]==a) { return a; } else { return par[a.first][a.second]=find(par[a.first][a.second]); } } void unite(pii a,pii b) { a=find(a); b=find(b); if(a==b) { return; } if(ran[a.first][a.second]>ran[b.first][b.second]) { par[b.first][b.second]=a; ren[a.first][a.second]+=ren[b.first][b.second]; } else { par[a.first][a.second]=b; ren[b.first][b.second]+=ren[a.first][a.second]; } if(ran[a.first][a.second]==ran[b.first][b.second]) { ran[b.first][b.second]++; } } int map[252][252]; int main() { init(); int mx,my; scanf("%d%d",&mx,&my); for(int i=0;i<252;i++) { for(int j=0;j<252;j++) { map[i][j]=0; } } for(int i=0;i<mx;i++) { for(int j=0;j<my;j++) { char zan; scanf(" %c",&zan); if(zan=='1') { if(map[i][j]==1) { unite(make_pair(i,j),make_pair(i+1,j+1)); } if(map[i][j+1]==1) { unite(make_pair(i,j+1),make_pair(i+1,j+1)); } if(map[i][j+2]==1) { unite(make_pair(i,j+2),make_pair(i+1,j+1)); } if(map[i+1][j]==1) { unite(make_pair(i+1,j),make_pair(i+1,j+1)); } map[i+1][j+1]=1; } } } }
蟻本と違うのは、ren配列に連結成分の大きさが入っていることです。
連結成分の大きさを見たい時には、そこでfindして出てきた結果をren配列に食わせると見えます。
あと、このようにuniteを4方向ぶん実装すればいいようになっています。これは周囲4方向をつながっているとみなすなら2方向でよくなって楽です。
また、入力と一緒に処理できるのもうれしいです。
とりあえずunion-find木では、このように、幅優先探索をさぼることができます。また、「各連結成分で一番上にあるもの」とか「各連結成分内で一番書いてある数字が大きいもの」とかも配列を追加してunite関数をいじれば簡単に取り出せます。
以下問題例
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0569
イルミネーション。連結成分だけが欲しいのでunion-findでいいです。
http://arc006.contest.atcoder.jp/tasks/arc006_4
この方法でさぼれます。
http://codeforces.com/contest/11/problem/C
上のと似てる。
注意すべきなのは、同じ幅優先探索でも、「最短経路を求めるタイプのものはできない」ということです。
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1166
例えばこれ。入力の形式的にunion-findしたくなるけどできない。
あと、切れません。切れません。大切なことなので二回言いました。
4.link-cut tree
切れるらしい。こわい。知らない。