Union-Findデータ構造の基礎アルゴリズム

Union-Findデータ構造

基本的なテンプレート実装から始めましょう。

この種の問題は比較的単純で、主要な関数を正しく実装すれば解決できます。

int findRoot(int node){return (node == parent[node] ? node : parent[node] = findRoot(parent[node]));}

豆知識:多くの人はこの関数をFindfindと命名しますが、私の場合はなぜfindRootという名前を使用しているのでしょうか?当初はFindとしていましたが、より明確にしたいと思いFind_Parentを使用していましたが、名称が長すぎると感じたため、FindParentの頭文字を採用しfindRootとなりました。短くて覚えやすく、意味も明確で珍しい名前です!

拡張領域Union-Find

これは「敵の敵は味方」という概念を応用したものです。

例題:囚人の配置問題

この問題では貪欲法が有効です。衝突度が高いペアは同じグループに入れたくない。

各囚人をノードnodeとして表現し、その反対状態をnode + totalとします。

もしnodeAnodeBを同じグループにしたくない場合、それぞれの反対状態と結びつけます。つまりエッジ(nodeA, nodeB + total)(nodeA + total, nodeB)を作成します。

同じグループに配置せざるを得なくなるのは、nodeAnodeA + totalが同じ集合に属している場合です。このとき、出力して終了します。

#include<bits/stdc++.h>
using namespace std;
const int MAX_N = 2e4+5, MAX_M = 1e5+5;
struct Edge{int src, dst, conflict;}edges[MAX_M];
int numPrisoners, numConflicts, group[2*MAX_N];

bool compareEdges(Edge e1, Edge e2){return e1.conflict > e2.conflict;}

int findRoot(int node){
    return (group[node] == node ? node : group[node] = findRoot(group[node]));
}

void unionGroups(int node1, int node2){
    if(findRoot(node1) != findRoot(node2))
        group[findRoot(node2)] = findRoot(node1);
    return;
}

int main(){
    cin >> numPrisoners >> numConflicts;
    for(int i = 1; i <= 2 * numPrisoners; i++)
        group[i] = i;
    
    for(int i = 1; i <= numConflicts; i++)
        cin >> edges[i].src >> edges[i].dst >> edges[i].conflict;
    
    sort(edges + 1, edges + numConflicts + 1, compareEdges);
    
    for(int i = 1; i <= numConflicts; i++){
        unionGroups(edges[i].src + numPrisoners, edges[i].dst);
        unionGroups(edges[i].src, edges[i].dst + numPrisoners);
        
        if(findRoot(edges[i].src) == findRoot(edges[i].src + numPrisoners) || 
           findRoot(edges[i].dst) == findRoot(edges[i].dst + numPrisoners)){
            cout << edges[i].conflict << "\n";
            return 0;
        }
    }
    cout << "0\n";
    return 0;
}

練習問題:ドアの問題

この問題も前述の問題と類似しています。

開いているドアに対しては(nodeX, nodeY)(nodeX + m, nodeY + m)を結合し、閉じているドアに対しては(nodeX, nodeY + m)(nodeX + m, nodeY)を結合します。

もしnodeZnodeZ + mが同じ集合に含まれている場合、解は存在しません。

#include<bits/stdc++.h>
using namespace std;
const int MAX_NODES = 2e5+5;
int nodeCount, switchCount, connections[2][MAX_NODES], counter[MAX_NODES], parent[MAX_NODES];
bool initialState[MAX_NODES], isValid;

int findRoot(int node){
    return (parent[node] == node ? node : parent[node] = findRoot(parent[node]));
}

void unionNodes(int node1, int node2){
    parent[findRoot(node2)] = findRoot(node1);
    return;
}

int main(){
    cin >> nodeCount >> switchCount;
    isValid = true;
    
    for(int i = 1; i <= nodeCount; i++)
        cin >> initialState[i];
    
    memset(counter, -1, sizeof(counter));
    
    for(int i = 1; i <= switchCount; i++){
        int connectionNum;
        cin >> connectionNum;
        while(connectionNum--){
            int tempNode;
            cin >> tempNode;
            connections[++counter[tempNode]][tempNode] = i;
        }
    }
    
    for(int i = 1; i <= 2 * switchCount; i++)
        parent[i] = i;
    
    for(int i = 1; i <= nodeCount; i++){
        if(!initialState[i]){
            unionNodes(connections[0][i], connections[1][i] + switchCount);
            unionNodes(connections[0][i] + switchCount, connections[1][i]);
        } else {
            unionNodes(connections[0][i], connections[1][i]);
            unionNodes(connections[0][i] + switchCount, connections[1][i] + switchCount);
        }
    }
    
    for(int i = 1; i <= switchCount; i++)
        if(findRoot(i) == findRoot(i + switchCount))
            isValid = false;
    
    if(isValid)
        cout << "YES\n";
    else 
        cout << "NO\n";
    
    return 0;
}

食糧連鎖問題

この問題は三重拡張が必要で、nodenode + totalnode + 2 * totalの三つの状態が存在します。三種類の動物が循環的に捕食関係にあるため、三倍の拡張が必要です。

まとめ

Union-Find構造自体は単純ですが、さまざまな応用や変形を加えることで複雑さが増します。さまざまな手法や状況に対応する必要があります。

様々な応用方法や革新を継続的に探求することが重要です。

タグ: Union-Find disjoint-set graph-algorithms data-structures algorithm-design

7月21日 01:43 投稿