2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Union-Findのデータ構造【競技プログラミング】

Last updated at Posted at 2024-08-01

はじめに

  • 最近、データ構造づいていた(スタック、キュー、ヒープ)けど、今回はUnion-Findをswiftに導入してみる。
  • ヒープは、溜め込んだデータの内、最大(最小)のデータを取り出す計算量がO(1)と極めて小さく、データを溜め込むときの計算量もO(logN)と比較的小さかった。
  • Union-Findの特徴は、非常に小さい計算量で、データを「グループ分け」する事が出来ること。
  • まず、初期化時点で全てのデータ(n個の場合、0..<nのインデックスが付された要素)が存在し、データの追加や削除はされない。メソッドは、unite(同じグループにする)とisSame(同じグループかどうか判定する)の2つのみ。統合(unite)は出来るが、分離は出来ない。また、同じグループかどうかが分かるだけであり、グループ名は得られない。

Union-Findの考え方

  • uniteとisSameを少ない計算量で実現するのが、Union-Find木であるが、なぜ、treeだと、少ない計算量で計算できるのかは、下記のAtCoderのスライドを見て欲しい。p5のように、配列を使って、「同じ数字なら同じグループ」のようなアルゴリズムだと、2つの要素をuniteしたとき、片方のグループに割り当てた数字を全て塗り返さなくてはならず、O(n)の計算量が必要となり、無駄に計算量を喰ってしまう。
  • これにたいし、p7のようにtree構造を考え、p9のように「根が同じであれば、同じグループである」とすれば、uniteの計算量は小さくできる。

Union-Find木を使った実装化

  • c++用のスライドのコードをswiftで実装する
struct UnionFind {
    
    private var par:[Int] = [] // treeの親ノードを格納する。
    
    init(_ n:Int) {
        for i in 0..<n {par.append(i)} 
    }
    
    mutating func root(_ i:Int)->Int {
        if par[i] == i {return i} // 自身が根なら自身を返す
        par[i] = root(par[i])  // 再帰計算の途中で経路圧縮もついでに行う。
        return par[i]        // 再帰計算で値を返す
    }
    
    mutating func unite(_ i:Int,_ j:Int){
        let (ri,rj) = (root(i),root(j))
        if ri == rj {return} // 根が同じなら何もしない
        par[ri] = rj       // ri(iの根)の根をrjとする
    }
    
    mutating func isSame(_ i:Int,_ j:Int)->Bool { root(i) == root(j) }
}

var uf = UnionFind(10)
print(uf.isSame(0,9)) // false
uf.unite(0,5) // 0と5が同じグループ
print(uf.isSame(0,9)) // false
uf.unite(5,9) // 5と9が同じグループ
print(uf.isSame(0,9)) // true -- 0と9が同じグループ

高機能化

  • 前記のコードで、配列par[i]は、番号iの親ノードの番号を格納していた。
  • 当然、番号iが最上位の親ノード(根)であれば、par[i]には、iが格納される。
  • だけど、根の要素に根の番号が格納されているのはMOTTAINAI!と、地球上の誰かがふと思った。…地球上の誰かがふと思ったのだ…配列(みんな)の未来を守らねばと…
  • そんなわけで、iが根であるとき、par[i]を負数とし、その負数の絶対値は根に属するノードの数とすることにします。つまり、根の要素の値を見ることで、そのグループに属する数が分かることになります。
  • この思想で前記のコードを書き換える。まず、配列parの初期値は全て「-1」となる。つまり、全ての要素が根(負数)であり、その根に属するノード数は根自身のみの1(負数の絶対値)となる。
struct UnionFind {
    
    private var par:[Int] = [] // treeの親ノードを格納する。
    var parent:[Int] { par }
    
    init(_ n:Int) {
        par = [Int](repeating:-1,count:n)
    }
    
    mutating func root(_ i:Int)->Int {
        if par[i] < 0 {return i} // 自身が根なら自身を返す
        par[i] = root(par[i])  // 再帰計算の途中で経路圧縮もついでに行う。
        return par[i]        // 再帰計算で値を返す
    }
    
    mutating func unite(_ i:Int,_ j:Int){
        let (ri,rj) = (root(i),root(j))
        if ri == rj {return} // 根が同じなら何もしない
        
        // riの根をrjとする。
        par[rj] += par[ri] // 根rjの仲間の数に、riの仲間の数を加える
        par[ri] = rj       // ri(iの根)の根をrjとする
    }
    
    mutating func isSame(_ i:Int,_ j:Int)->Bool { root(i) == root(j) }
}
// 入力
var uf = UnionFind(10)
uf.unite(0,5) // 0と5が同じグループ
uf.unite(5,9) // 5と9が同じグループ
uf.unite(2,3) // 2と3が同じグループ
uf.unite(2,4) // 2と4が同じグループ

for i in 0..<10 {
    print(i,uf.parent[i],uf.root(i)) //ノード番号、親番号、根番号
}

// 出力
0 5 9 //ノード0の親は5で、根は9
1 -1 1
2 3 4
3 4 4
4 -3 4
5 9 9 //ノード5の親は9で、根は9
6 -1 6
7 -1 7
8 -1 8
9 -3 9 //ノード9は根で、仲間は3つ

実装したUnion-Findの活用方法

  • グループ化が高速で実行できたとして、何が嬉しいの?
  • というのが普通の感想だと思うので、活用例を示そうと思う。
  • とは言え、活用例もAtCoderの問題を解くことだから、「活用方法」と呼ぶのは恥ずかしいかも。
    スクリーンショット 2024-08-01 22.45.07.jpg
  • 上記の問題は、最大のお友達集団の人数を調べることが回答に繋がる。最大のお友達集団の全員を別々のグループに分けるための最小のグループ数は、当然、最大のお友達集団の人数と一致する。
  • 入力値
10 4 //ノード数10、お友達ペア4
3 1 //ペア① 3と1は友達
4 1 //ペア② 4と1は友達
5 9 //ペア③ 5と9は友達
2 6 //ペア④ 2と6は友達
  • 回答用コード
let (N,M) = [readLine()!.split(separator:" ").map{Int($0)!}].map{($0[0],$0[1])}[0]

var uf = UnionFind(N)

for _ in 0..<M {
    // 問題文のノード番号から1を引いて、配列のindexにする。
    let (a,b) = [readLine()!.split(separator:" ").map{Int($0)!}].map{($0[0]-1,$0[1]-1)}[0] 
    uf.unite(a,b)
}

var ans = 0
for i in 0..<N {
    ans = max(ans,-uf.parent[i])
}

print(ans)

さいごに

  • 活用例で示したAtCoderの問題は、Union-Findを知っていれば、すごく簡単な問題に思えるけど、ABCコンテストが6問だった時代のd問題なんだよね。だから、実は難易度高めの問題。
  • そんな問題も簡単に解けちゃうUnion-Findは強力なツールじゃないだろうか(やや自信なし)
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?