【Java】素集合データ構造[互いに素な集合] (Union Find Tree, Disjoint Set Forest) 
2015/05/22 Fri [edit]
また例によって色々な本を見ているのだが、今回はプログラミングコンテスト攻略のためのアルゴリズムとデータ構造という本の P.322~323 に載っている(C++ 版)「互いに素な集合」(Union Find, Disjoint Set)で少しだけ疑問に思った所があったので分析してみたら、微妙なバグ(?)を見つけた。
といっても答えは間違ってないので、動作的には問題ない。正誤表にも載ってないね。検索してもそもそもあまり日本語サイトすら出てこなかったので、いつものように Wikipedia を見ながら簡単なものを作ってみた。といってもクラス(本では DisjointSet)部分をそのまま作りなおしたという感じだけどね。
●静的配列版 UnionFindTree
/**<h1>素集合データ構造 (Disjoint Set Forest)</h1>
* <p>互いに素な集合。
* <br>内部的には配列で定義されている。</p>
*/
public class UnionFindTree {
private int[] parent; //親ノード
private int[] rank; //高さ or 次数
/**<h1>コンストラクタ</h1>
* <p></p>
* @param size : ノード数
*/
public UnionFindTree(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++) {
makeSet(i);
}
}
/**<h1>ノードを作成する</h1>
* <p>作成されたノードはルートとなる。
* <br>元のサイズより大きいノードを指定した場合エラー。</p>
* @param x : ノード
*/
public void makeSet(int x) {
parent[x] = x;
rank[x] = 0;
}
/**<h1>x, y の属している木を結合する</h1>
* <p>同じ木の場合は何もしない。</p>
* @param x : ノード1
* @param y : ノード2
*/
public void union(int x, int y) {
final int xRoot = find(x);
final int yRoot = find(y);
if (rank[xRoot] > rank[yRoot]) {
parent[yRoot] = xRoot; //x の木に結合
}
else if(rank[xRoot] < rank[yRoot]) {
parent[xRoot] = yRoot; //y の木に結合
}
else if (xRoot != yRoot) { //同じ高さで根が違うとき
parent[yRoot] = xRoot; //x の木に結合
rank[xRoot]++; //x の木の高さを加算
}
}
/**<h1>属している木のルートを返す</h1>
* <p>検索したノードは経路圧縮が行われる。</p>
* @param x : ノード
* @return<b>int</b> : ルートのノード
*/
public int find(int x) {
if (x != parent[x]) { //根でないとき
parent[x] = find(parent[x]); //直接、根に繋ぐ(経路圧縮)
}
return parent[x];
}
/**<h1>同じ木に属しているかどうかを返す</h1>
* <p>同じルートを持っているかで判別する。</p>
* @param x : ノード1
* @param y : ノード2
* @return<b>boolean</b> : true = 同じ木に属している
*/
public boolean same(int x, int y) {
return find(x) == find(y);
}
}
内部的には静的配列でノードを定義しているが、LinkedList あたりにして、動的配列にするのもいいかもね。アルゴリズムそのままなので、「UnionFindTree」か「DisjointSet」でググれば同じようなものが一杯出てくると思うが、まぁ、英語でコメント書かれているものばかりなので、こちらを勉強用にする分には役に立つだろう。
書籍で気になったのは以下の部分だ。
void link(int x, int y) {
if (rank[x] > rank[y]) {
p[y] = x;
} else {
p[x] = y;
if (rank[x] == rank[y]) {
rank[y]++;
}
}
}
これは木を結合する処理だが、入れ子になっている条件分岐をよく見てみると、x, y が元々同じ木だったとき、「rank[x] == rank[y]」の条件に当てはまり、無駄にrankが増える。例題では問題ないし(同じ木のものは結合してない)、答えも正しく出るのだが、まぁ、テストケースによっては危うくなるかも知れない(データ上は rank が大きいが実際には小さかったりすると、正しく木の大きさを評価できないわけで…)。
ついでなので、以下は内部データを LinkedList にしてみたもの。ほぼ機械的に書き換えてみただけ。後からノードを追加できるメリットがある。
●LinkedList 版 UnionFindTree
import java.util.LinkedList;
import java.util.List;
/**<h1>素集合データ構造 (Disjoint Set)</h1>
* <p>互いに素な集合。
* <br>内部的には LinkedList で定義されている。</p>
*/
public class UnionFindTreeList {
private List<Integer> parent = new LinkedList<Integer>(); //親ノード
private List<Integer> rank = new LinkedList<Integer>(); //高さ or 次数
//コンストラクタ
/**<h1>空のコンストラクタ</h1>
* <p>newSet() でノードを新規追加する。</p>
*/
public UnionFindTreeList() {
}
/**<h1>ノード数指定のコンストラクタ</h1>
* <p>予めノードを生成する。</p>
* @param size : ノード数
*/
public UnionFindTreeList(int size) {
for (int i = 0; i < size; i++) {
parent.add(i);
rank.add(0);
}
}
/**<h1>ノードを新規に追加する。</h1>
* <p>最後に追加され、ルートとなる。</p>
* @return<b>int</b> : 追加されたノード
*/
public int newSet() {
final int x = parent.size();
parent.add(x);
rank.add(0);
return x;
}
/**<h1>ノードを作成する</h1>
* <p>作成されたノードはルートとなる。
* <br>元のサイズより大きいノードを指定した場合エラー。</p>
* @param x : ノード
*/
public void makeSet(int x) {
parent.set(x, x);
rank.set(x, 0);
}
/**<h1>x, y の属している木を結合する</h1>
* <p>同じ木の場合は何もしない。</p>
* @param x : ノード1
* @param y : ノード2
*/
public void union(int x, int y) {
final int xRoot = find(x);
final int yRoot = find(y);
if (rank.get(xRoot) > rank.get(yRoot)) {
parent.set(yRoot, xRoot); //x の木に結合
}
else if(rank.get(xRoot) < rank.get(yRoot)) {
parent.set(xRoot, yRoot); //y の木に結合
}
else if (xRoot != yRoot) { //同じ高さで根が違うとき
parent.set(yRoot, xRoot); //x の木に結合
rank.set(xRoot, rank.get(xRoot) + 1); //x の木の高さを加算
}
}
/**<h1>属している木のルートを返す</h1>
* <p>検索したノードは経路圧縮が行われる。</p>
* @param x : ノード
* @return<b>int</b> : ルートのノード
*/
public int find(int x) {
if (x != parent.get(x)) { //根でないとき
parent.set(x, find(parent.get(x))); //直接、根に繋ぐ(経路圧縮)
}
return parent.get(x);
}
/**<h1>同じ木に属しているかどうかを返す</h1>
* <p>同じルートを持っているかで判別する。</p>
* @param x : ノード1
* @param y : ノード2
* @return<b>boolean</b> : true = 同じ木に属している
*/
public boolean same(int x, int y) {
return find(x) == find(y);
}
/**<h1>全ノードを消去</h1>
* <p>内部の LinkedList をクリアする。</p>
*/
public void clear() {
parent.clear();
rank.clear();
}
}
newSet() と clear() は勝手に作ったメソッドだが、連番のノード追加と全消去しかできない。データ型も Integer で固定しているが、Long やジェネリクス型にしても良いだろう。しかしそのときはループ変数なども変更しなければならないかも知れない。
オーダーは O(log n) なので大量にデータがあるときには良いね。複雑な迷路の経路判定とか。
まぁ、データ構造は使えれば高速化できるものが多いので、知っていても損はないだろう。
(関連記事)
【Java】クラスカル法[最小全域木] (Kruskal's algorithm, Minimum Spannig Tree)
【Java】プリム法[最小全域木] (Prim's algorithm, Minimum Spannig Tree)
【Java】ダイクストラ法[単一始点最短経路] (Dijkstra's algorithm, Single Source Shortest Path)
【Java】ベルマン-フォード法[単一始点最短経路] (Bellman-Ford algorithm, Single Source Shortest Path)
【Java】ワーシャル-フロイド法[全点対間最短経路] (Warshall-Floyd algorithm, All Pairs Shortest Path)
- 関連記事
トラックバック
トラックバックURL
→http://fantom1x.blog130.fc2.com/tb.php/175-93b8e12b
この記事にトラックバックする(FC2ブログユーザー)
| h o m e |