案外この形でまとめている記事が見つからなかったのと、ライブラリの上手な作り方を思いついてしまいまったのとで、執筆意欲です。
他の既存記事
Low-link と関節点・橋、2-辺連結成分分解まではかなり記事があるのですが、 2-連結成分分解はなかなか見つかりませんね…… みなさま low-link 系のおすすめ記事がありましたら、自薦・他薦問わず教えていただけると嬉しいです。
- 2-連結成分分解をします。アルゴリズムは当記事と同じなようです。二重頂点連結成分分解(Bi-Connected-Components) | Luzhiled’s memo
- 2-辺連結成分分解をします。商グラフも作ってくれるようです。便利ですね。二重辺連結成分分解(Two-Edge-Connected-Components) | Luzhiled’s memo
- low-link の計算と、2-辺連結成分分解です。二辺連結成分分解 - 薄いブログ
用語の注意
ここでいう 2-連結成分というのは、2-頂点連結成分、あるいは 2-点連結成分などと呼ばれるもののことです。
ソースコードへのリンク (Rust)
設計の特徴
この設計の一番の特徴は、橋・関節点、2-連結成分・2-辺連結成分分解を前計算せず、聞かるたびに判定や列挙を行う点です。計測をしていないのでどれくらい速い or 遅いのかはわかりませんが、こうすることで一回の DFS ですべてを計算する設計に比べると関心がよく分離できるように思われます。
Low-link について
説明が面倒なので定義を貼ります。
pub struct LowLink { g: Vec<Vec<usize>>, // 隣接リストです。未ビルドならば無向グラフ、ビルド済みならば木辺と後退辺のみが入っています。 sorted: Vec<usize>, // [i]: pre-order で i 番目である頂点の番号 ord: Vec<usize>, // [i]: 頂点 i の pre-order における順位 low: Vec<usize>, // [i]: 頂点 i から木辺を任意回、後退辺を高々1回辿って行ける頂点の pre-order の最小値 parent: Vec<usize>, // [i]: 頂点 i が根なら i、さもなくの親頂点の番号 built: bool, // ビルド済みなら `true` }
注意点です。
- 入力は連結でなくても大丈夫です。
- 根のリストは管理せず、代わりに根のリストが根のリストとしてあり得るもののうち辞書順最小であることを保証します。
- 関節点や橋、2-連結成分、2-辺連結成分は管理していません。
構築について
面倒なのでコードを貼ります。
ポイントは
- 構築時に(下向きの)木辺と後退辺以外を消しておくと後々なにかと便利
- pre-order と親だけ先に計算しておけば low-link は後ろから DP できる
といったあたりでしょうか。
impl LowLink { pub fn build(&mut self) { assert!(!self.built); self.built = true; let n = self.g.len(); self.ord.resize(n, !0); self.parent.resize(n, !0); // Pre-order (`sorted`, `ord`), `parent` の計算 let mut used = vec![false; n]; for i in 0..n { if !used[i] { self.parent[i] = i; used[i] = true; build( i, i, &mut self.g, &mut self.parent, &mut self.sorted, &mut self.ord, &mut used, ); } } // Low-link (`low`) の計算 self.low = self.ord.clone(); for &x in self.sorted.iter().rev() { for &y in &self.g[x] { self.low[x] = self.low[x].min(if self.ord[x] < self.ord[y] { self.low[y] } else { self.ord[y] }); } } } } fn build( x: usize, p: usize, g: &mut [Vec<usize>], parent: &mut [usize], sorted: &mut Vec<usize>, ord: &mut [usize], used: &mut [bool], ) { ord[x] = sorted.len(); sorted.push(x); let mut i = 0; while i < g[x].len() { let y = g[x][i]; if y == p { g[x].swap_remove(i); continue; } else if replace(&mut used[y], true) { if ord[x] < ord[y] { g[x].swap_remove(i); continue; } } else { parent[y] = x; build(y, x, g, parent, sorted, ord, used); } i += 1; } }
関節点・橋、そして 2-連結成分・2-辺連結成分分解について
簡単のため、pre-order と頂点番号が一致している、すなわちフィールド sorted
, ord
は恒等置換であるとしておきましょう。また TeX で書きやすくするため、low[x]
を と表します。
また、通常の1回の DFS ですべてを計算する方法と異なり、木辺なのか後退辺なのかをどう判定するのかについて考える必要がありますが、これは実はとても簡単です。無向辺 について考えたいとしましょう。必要なら と を入れ替えることにより、有向辺 が逆辺削除済みグラフに入っているとしてよいです。このとき、 が木辺である条件は 、後退辺である条件は です。
関節点
頂点 が関節点である条件は、ご存知の通り となる(有向)木辺 が存在することです。
橋
辺 が橋である条件は、ご存知の通り が木辺であり、かつ適宜 と を入れ替えることにより としたときに、 が成り立つことです。
2-連結成分分解
ここが一番むずかしいところですよね。かくいう私もこちらの問題に出会って、「なるほどです low-link ですね、ええ。関節点でスパパパーンと分けて終了です。(ヘラヘラ)」「いえ難しいではありませんか!!!」をしていました。
構築時と同じ DFS を再びして、通った辺を片っ端からスタックに入れていき、「2-連結成分から抜けた」(or 根から抜けた)ときにスタックの適切な箇所以降を切り取って答えに入れていけばよいです。(結局もう一回 DFS しないと行けないの微妙と言われてしまうとそうかもです。)
impl LowLink { pub fn biconnected_components(&self) -> Vec<Vec<[usize; 2]>> { assert!(self.built); let n = self.g.len(); let mut stack = Vec::new(); let mut cmp = Vec::new(); for i in (0..n).filter(|&i| self.parent[i] == i) { self.__biconnected_components_dfs(i, &mut stack, &mut cmp); } cmp } fn __biconnected_components_dfs( &self, x: usize, stack: &mut Vec<[usize; 2]>, cmp: &mut Vec<Vec<[usize; 2]>>, ) { for &y in &self.g[x] { stack.push([x, y]); if self.ord[x] < self.ord[y] { self.__biconnected_components_dfs(y, stack, cmp); if self.parent[x] == x || self.ord[x] <= self.low[y] { let mut c = Vec::new(); while let Some([u, v]) = stack.pop() { c.push([u, v]); if [u, v] == [x, y] { break; } } cmp.push(c); } } } } }
2-辺連結成分分解
2-連結成分分解と似ていますが、スタックに入れるのが頂点、スタックから掠め取るタイミングが橋を戻るときです。よくある「橋を通らないように DFS」という方針と実質的には大差ないと思います。
impl LowLink { pub fn two_edge_components(&self) -> Vec<Vec<usize>> { assert!(self.built); let n = self.g.len(); let mut stack = Vec::new(); let mut cmp = Vec::new(); for i in (0..n).filter(|&i| self.parent[i] == i) { self.__two_edge_components_dfs(i, &mut stack, &mut cmp); cmp.push(take(&mut stack)); } cmp } fn __two_edge_components_dfs( &self, x: usize, stack: &mut Vec<usize>, cmp: &mut Vec<Vec<usize>>, ) { stack.push(x); for &y in &self.g[x] { if self.ord[x] < self.ord[y] { self.__two_edge_components_dfs(y, stack, cmp); if self.ord[x] < self.low[y] { let mut c = Vec::new(); while let Some(z) = stack.pop() { c.push(z); if z == y { break; } } cmp.push(c); } } } } }