AtCoder Beginners Contest 120 で連結成分のsizeを計算するdisjoint-setの問題が出題された. それまでrankを用いたUnion-Findしか経験がなかったので, この機会にまとめる.
Union-Find
Union-Findは要素が同一の集合に含まれるかどうかを判定するデータ構造.
Query | Process |
---|---|
unite(x, y) | X(x X)とY(y Y)をマージする |
same(x, y) | X == Y か判定する |
の操作を
各要素を1つのノード, 集合を木と表現する. 要素が同一の集合に含まれる場合, それらの要素は同一の木のノードである.
上記操作をならし計算量で行う1ためのマージ戦略が以下の経路圧縮とrankである.
経路圧縮
要素が同一の木のノードであるか判定する場合, 各要素のrootが同一かどうか見ればよい.
ただその場合木の高さだけ再帰的に親ノードを辿る必要がある.
そこで, 親ノードを調べる際に調べたノードをrootに直接つなぎ直すことで,次回以降の判定時の計算量をに削減する.
Rank
操作時に木の高さが高い方を親として, 低い方を高い方に繋げるようにする.
ここで, 経路圧縮と併用する場合には圧縮前の高さでrankを持つ必要がある. 経路圧縮は操作時に行うため, の段階では必ずしも木の高さがとなっている保証はない.
以下は経路圧縮のみの実装例.
struct UnionFind {
par: Vec<usize>,
}
impl UnionFind {
fn new(n: usize) -> Self {
UnionFind {
par: (0..n).collect(),
}
}
fn unite(&mut self, x: usize, y: usize) {
let px = self.find(x);
let py = self.find(y);
if px == py { return; }
self.par[py] = px;
}
fn same(&mut self, x: usize, y: usize) -> bool {
self.find(x) == self.find(y)
}
fn find(&mut self, x: usize) -> usize {
if self.par[x] == x { x }
else {
// 経路圧縮
let p = self.par[x];
self.par[x] = self.find(p);
self.par[x]
}
}
}
Decayed Bridges
本題.
頂点辺の無向単純グラフの辺を順番に取り除いたとき, 時刻でアクセスできない頂点が何組あるか.
かんがえたこと
Union-Findでは要素を統合することはできても分割操作は行えない. 辺を除去する操作を考える場合にはグラフ全体を保持する必要が出てきてしまう. でもで隣接グラフを持つのはエグいし連結判定が厳しい.
では操作を巻き戻すとどうなるかを考えた.
まず辺をすべて取り除いた状態から開始し,辺を追加していく.
このアプローチならUnion-Findで管理できる.
辺を追加することで頂点と頂点が新たに連結されたとすると, アクセスできる頂点ペアは個増える. この連結された頂点の増加判定は操作を操作の前後に行うことで行える.
が, Union-Findで集合の要素数を持つケースを初めて見たのでやや手間取った. 本番では構造体に要素数siz
をVec
で保持させて時に更新してなんとかなった.
この辺が存在した場合の連結する頂点数を考えると, なのでここから引いてくと楽.
解答
fn main() {
let (n, m) = {
let i = read::<usize>();
(i[0], i[1])
};
let mut ab = Vec::new();
for _ in 0..m {
let (a, b) = {
let i = read::<usize>();
(i[0], i[1])
};
ab.push((a, b));
}
let mut uf = UnionFind::new(n + 1);
let mut cost = n * (n - 1) / 2;
let mut ans = Vec::new();
for (a, b) in ab.into_iter().rev() {
ans.push(cost);
if uf.same(a, b) { continue; }
let pa = uf.find(a);
let pb = uf.find(b);
let c = uf.siz[pa] * uf.siz[pb];
uf.unite(a, b);
cost -= c;
}
for a in ans.iter().rev() {
println!("{}", a);
}
}
所感
unite by rankとunite by sizeは本質的には同じなので落ち着いて対処したかった.
またrankに符号付き整数を用いて, 正負でsize or rankを表すテクもあることを知った.
basic (Bridge/ABC075)
weighted(Potential) Union-Find (People on a line/ABC087)
永続UF (Stamp Rally/AGC002)
わからん (Nuske vs Phantom Thnook/AGC015)
Footnotes
-
. 経路圧縮とrankどちらかのみの場合はならし計算量. ↩
Comments