ei1333's page

ホーム > Wiki

素集合データ構造(Union-Find)

説明

集合を高速に扱うためのデータ構造。集合を合併する操作(unite), ある要素がどの集合に属しているか(find)を問い合わせる操作を行うことが出来る。

使い方

計算量

O(α(n))

α はアッカーマンの逆関数

実装例

Copy
struct UnionFind
{
  vector< int > data;

  UnionFind(int sz)
  {
    data.assign(sz, -1);
  }

  bool unite(int x, int y)
  {
    x = find(x), y = find(y);
    if(x == y) return(false);
    if(data[x] > data[y]) swap(x, y);
    data[x] += data[y];
    data[y] = x;
    return(true);
  }

  int find(int k)
  {
    if(data[k] < 0) return(k);
    return(data[k] = find(data[k]));
  }

  int size(int k)
  {
    return(-data[find(k)]);
  }
};

応用 1: 2部グラフの頂点彩色

Union-Find を用いると 2 部グラフ判定とその副作用として彩色が可能。頂点を倍長して偶奇に分ける。隣接頂点を同じ色にするときは, unite(u,v)unite(u+N,v+N), 異なる色にするときは unite(u+N,v)unite(u,v+N) をする。

Copy
struct Bipartite_Graph : UnionFind
{
  vector< int > color;

  Bipartite_Graph(int v) : color(v + v, -1), UnionFind(v + v) {}

  bool bipartite_graph_coloring()
  {
    for(int i = 0; i < color.size() / 2; i++) {
      int a = find(i);
      int b = find(i + (int) color.size() / 2);
      if(a == b) return (false);
      if(color[a] < 0) color[a] = 0, color[b] = 1;
    }
    return (true);
  }

  bool operator[](int k)
  {
    return (bool(color[find(k)]));
  }
};

応用 2: データ構造をマージする一般的なテク

データ構造をマージする一般的なテク(Weighted-Union-Heuristic) は, Union-Find の unite 操作で常に大きい木の根が全体の根になるよう連結する(union-by-rank) の考え方と同様。

補足: 経路圧縮, ランクによる統合の計算量

経路圧縮, ランクによる統合の 2 つの工夫をすると計算量は 1 クエリあたり O(α(n)) となるが, 経路圧縮あるいはランクによる統合片方だけ行うと O(logn) となる。[証明: アルゴリズムとデータ構造 p268-270]

応用 3: UnionFindに何かをもたせるやつ

左端と右端をもたせたUnionFind の実装例を以下にあげる。

Copy
struct UnionFindRange
{
  vector< int > data;
  vector< int > left, right;

  UnionFindRange(int sz)
  {
    data.assign(sz, -1);
    left.resize(sz);
    right.resize(sz);
    for(int i = 0; i < sz; i++) left[i] = i;
    for(int i = 0; i < sz; i++) right[i] = i;
  }

  bool unite(int x, int y)
  {
    x = find(x), y = find(y);
    if(x == y) return (false);
    if(data[x] > data[y]) swap(x, y);
    left[x] = min(left[x], left[y]);
    right[x] = max(right[x], right[y]);
    data[x] += data[y];
    data[y] = x;
    return (true);
  }

  pair< int, int > range(int k)
  {
    k = find(k);
    return {left[k], right[k]};
  };

  int find(int k)
  {
    if(data[k] < 0) return (k);
    return (data[k] = find(data[k]));
  }

  int size(int k)
  {
    return (-data[find(k)]);
  }
};

応用 4: 部分永続UnionFind

t 番目のクエリを処理した時点における頂点 x が含まれる連結成分の根や大きさを求めるクエリを O(logn) で行うことができる。

Copy
struct PartiallyPersistentUnionFind
{
  vector< int > data;
  vector< int > last;
  vector< vector< pair< int, int > > > add;
 
  PartiallyPersistentUnionFind() {}
 
  PartiallyPersistentUnionFind(int sz) : data(sz, -1), last(sz, 1e9), add(sz)
  {
    for(auto &vs : add) vs.emplace_back(-1, -1);
  }
 
  bool unite(int t, int x, int y)
  {
    x = find(t, x);
    y = find(t, y);
    if(x == y) return false;
    if(data[x] > data[y]) swap(x, y);
    data[x] += data[y];
    add[x].emplace_back(t, data[x]);
    data[y] = x;
    last[y] = t;
    return true;
  }
 
  int find(int t, int x)
  {
    if(t < last[x]) return x;
    return find(t, data[x]);
  }
 
  int size(int t, int x)
  {
    x = find(t, x);
    return -prev(lower_bound(begin(add[x]), end(add[x]), make_pair(t, 0)))->second;
  }
};

応用 5: 完全永続UnionFind

永続配列を使うとできる。

Copy
struct PersistentUnionFind
{
  PersistentArray< int, 3 > data;

  PersistentUnionFind() {}

  PersistentUnionFind(int sz)
  {
    data.build(vector< int >(sz, -1));
  }

  int find(int k)
  {
    int p = data.get(k);
    return p >= 0 ? find(p) : k;
  }

  int size(int k)
  {
    return (-data.get(find(k)));
  }

  PersistentUnionFind unite(int x, int y)
  {
    x = find(x);
    y = find(y);
    if(x == y) return *this;
    auto u = data.get(x);
    auto v = data.get(y);

    if(u < v) {
      auto a = data.mutable_get(x);
      *a += v;
      auto b = data.mutable_get(y);
      *b = x;
    } else {
      auto a = data.mutable_get(y);
      *a += u;
      auto b = data.mutable_get(x);
      *b = y;
    }
    return *this;
  }
};

問題例

参考資料