この記事でやること
グラフの橋、関節点、二重辺連結成分、二重頂点連結成分といった概念と、それらを求める際に用いる「lowlink」という手法を解説します。
注意事項
- この記事で扱うグラフは、全て単純な連結無向グラフとします。
- ソースコードは全てPython3.8を用いております。コピペはご自由にどうぞ。(AtCoder等で使用する際はPyPy3で提出して問題ありません。)
モチベーション
グラフが与えられたときに、ある2頂点についてそれら結ぶ互いに素な(同じ辺/頂点を通らない)2つ以上のパスが存在するか?という問題を考えます。
これはグラフの二重連結性に関する問題と言えますね。
この問題の場合は最大フローで解けそうですが、もっと複雑な問題になってくると計算量が壊れてしまいます。そこで、特に二重連結性に関する問題を効率よく解く手段が欲しいわけですね。
lowlinkの基本
前提:DFS木と後退辺・横断辺
lowlinkの話に入る前に、知っておく必要のある用語と命題について解説します。
DFS木
グラフにおいて適当な頂点を始点としてDFS(深さ優先探索)したとき、使用した辺を取ることで構成される、
を根とする木。特にこの記事では、DFS木上の各辺を葉方向の有向辺とします。
後退辺
グラフから全域木を取ります。このとき使われなかった辺
について、
と
が
における祖先と子孫の関係にあるとき、後退辺といいます。
横断辺
グラフから全域木を取ります。このとき使われなかった辺のうち後退辺でないものを横断辺といいます。

命題:グラフからDFS木を取ったとき、使われなかった辺は全て後退辺となる

証明
背理法で示します。
DFS木について、横断辺
が存在すると仮定します。
また、DFSにおける各頂点の行きがけ順(pre-order)を
で表すとします。
一般性を失わず、とします。
は横断辺であることから、
に訪れるのは
から帰ったあとであるはずです。
つまり、任意のの隣接点
について、
が成り立ちます。
ここで、より
となり、矛盾 ☐
ちなみに、全ての使われていない辺が後退辺となる全域木はDFS木ですが、実は全て横断辺となる全域木も存在します。どんな木でしょう?
参考↓
lowlinkでやること
ordの定義
先ほどの証明で使った定義通り、DFSにおける各頂点の行きがけ順(pre-order)を
で表すとします。
lowの定義
DFSによってと、以下で定義される
を求めておきます:
頂点
から後退辺を高々
回まで使って到達できる頂点の集合
について、
例です。定義と照らし合わせて確認してみてください。

このと
を用いて、二重連結性に関するアレコレを求めていく手法をlowlinkといいます。
ordとlowの求め方
まずDFSをしながらを埋めていき、
もその値で初期化します。その最中に後退辺
が見つかれば、
を
でchmin*1します。
次に、ボトムアップ順に各頂点について、
を各
の子
について
でchminしていくことで、
を求めることができます。
実装例
下記の実装例では、DFSをするついでに各頂点の子を表す二重配列、頂点のトポロジカルソート順を表す配列
の作成を行っています。
計算量は
lowlinkの活用例
ここからは、先ほど求めたと
を用いて、二重連結性に関するアレコレを求める方法を解説していきます。
橋と二重辺連結成分
橋(bridge)
削除するとグラフが連結でなくなるような辺。
どの閉路にも含まれない辺とも言い換えられます。
現実ネットワークで考えると「断線したらヤバい辺」にあたります。
二重辺連結成分(two-edge connected component)
グラフから橋を全て削除したときの連結成分。
任意の異なる2つの二重辺連結成分について、以下の性質が成り立ちます。
- 任意の異なる2頂点
について、辺について互いに素なパス
が少なくとも2つ存在する
- 任意の2頂点
について、辺について互いに素なパス
を2つ以上取ることはできない

二重連結成分分解の方法
グラフから橋と二重辺連結成分を列挙する方法を考えます。
適当な頂点からDFSしてと
を求めておきましょう。
橋の判定
後退辺は明らかに橋ではないので、木上の辺のみ見ていきます。
辺が閉路に含まれる
から後退辺を高々
回まで使って
または
の祖先に到達できる
より、閉路に含まれない、すなわち橋であるための必要十分条件は
辺が橋である
となります。

二重辺連結成分の列挙
橋を通らないようにDFSして連結成分を求めていけばいいですね。

実装例
各連結成分の頂点集合を二重配列で持つ、元グラフの頂点から連結成分を逆引きするための配列
を求めたいです。
根から始め、橋があれば「その先の頂点を始点としてまたDFSしますよー」というフラグを追加する、という感じの方針で実装します。
二重連結成分分解のついでに、連結成分を1点に縮約することで得られる、連結成分を頂点、橋を辺とした木を構築します。
(この木を作っておくことで、一般的に木で扱えるような色々なテクニックを用いて問題を効率よく解けたりします。)
この木を表現するために隣接リストも追加で作ります。
計算量は
例題
解説
二重辺連結成分分解して縮約した木を考えます。この木上の辺は全て橋なので、異なる2頂点間を結ぶパスで、辺について素なものは高々1つしか取れません。よって、(を木上の対応する頂点で置きなおしたとして)
というトレイルで同じ辺を通らないためには
が
パス上に存在することが必要十分条件となります。
この判定方法は色々考えられますが、私の提出コードではオイラーツアー*2を使用しました。

ACコード
Submission #34875923 - AtCoder Regular Contest 039
関節点と二重頂点連結成分
関節点(articulation point)
削除するとグラフが連結でなくなるような頂点。
現実ネットワークで考えると「ダウンしたらやばい中継点」にあたります。
二重頂点連結成分(biconnected component)
頂点の部分集合が二重頂点連結成分である
任意の頂点
について、
は連結であり、かつ
は極大である
最後の「極大である」というのは、前半の二重頂点連結性に関する条件を満たすについて
なら
の方を二重頂点連結成分としますよー、という意味です。

この定義から、関節点は複数の二重頂点連結成分に属する場合もあることに注意してください。

二重頂点連結成分分解の方法
グラフから関節点と二重頂点連結成分を列挙する方法を考えます。
適当な頂点からDFSしてと
を求めておきましょう。
関節点の判定
関節点を考える上で重要なのは「”どの頂点に対して”関節点なのか」という点。
まず、根について考えます。
少し観察すれば直ちに下の事実が得られるはずです。
根が子
に対して関節点
根
の出次数が
以上

次に、根以外の頂点について考えます。
DFS木上で根以外のある頂点を削除したとき、それによって根側から分離される頂点はどれか?を見る必要があります。
では、頂点を削除した際に子
が分離されるとはどういうときでしょうか?
橋を列挙したときと同様に考えると、から
の祖先である頂点に後退辺を高々1つだけ到達できれば分離されないことになるので
根でない頂点が子
に対して関節点
となります。

二重頂点連結成分の列挙
二重頂点連結成分は辺集合で見ると重複無しに分解できます。となると辺基準の分解しておくと嬉しい場合もある(かもしれない、要出典)ので、辺基準の分解と頂点基準の分解を両方行うことにします。

辺基準の分解で考えると、二重連結成分分解は「木上の、親側の端点を削除したら子側の端点が根から分離されてしまう辺」すなわち下図の緑の辺からスタートして、緑の辺を通らないようにDFSすることで実現できるとわかるでしょう。

頂点基準で分解する場合は、辺基準で分解中に通った頂点を順次記録していけばよいですね。
実装例
辺基準で分解した際の各連結成分の辺集合を二重配列で持つ、頂点基準で分解した際の各連結成分の頂点集合を二重配列で持つ
、各頂点が関節点であるかどうかを表すbool値配列
を作っています。
二重辺連結成分分解と同様、根から始め、緑の辺があれば「その先の頂点を始点としてまたDFSしますよー」というフラグを追加する、という感じの方針で実装しました。
注意点として、DFSを開始するごとにそのパートの連結成分を格納する空listを用意していますが、根が関節点(出次数が2以上)の場合のみそのパートでは連結成分が構成されないという例外があるので、そのときは一時的に作ったlistをすぐにpop()しています。
計算量は
(を作らない、つまり、後退辺を無視するなら
)
block-cut tree
二重辺連結成分分解では連結成分を縮約するだけで木が構築できました。では、二重頂点連結成分分解でも同様な「意味のある木」を構築したいです。
しかし、そのままだと関節点が複数の連結成分に共有されていて縮約は難しそうですね。そこで、「連結成分そのもの」を表す頂点を用意し、各関節点についてそれが含まれる連結成分を表す頂点を辺で結ぶことにします。
このようにして作られる木をblock-cut treeと呼ぶそうです。

実装例
データの表現方法としては、関節点の個数をとおき、木上の頂点について関節点について順に
の番号を割り振り、連結成分を表す頂点は
の番号を割り振って隣接リスト
を作っています。
また、各木の頂点が持つ元グラフに関する頂点集合を
- 関節点 ⇒ それ1つの単元集合
- 連結成分を表す頂点 ⇒ 元の連結成分から関節点を除いた頂点集合(空集合になる可能性もある)
と定義して二重配列で持った、また元のグラフの頂点から木上の頂点を逆引きする配列
を作っています。
具体的な方法はコードで確認して頂きたいのですが、先ほど求めたと
を使うことで簡単に作れます。
計算量は
(にて
を作らない、つまり、後退辺を無視するなら
)
例題
解説
まず、以下のように座標または
座標が等しい頂点を辺で結ぶことでグラフを構築します。

このグラフにおいてマッチングを考えるとき、頂点数が偶数の連結成分には完全マッチングが存在することが示せます。(証明は公式解説を参照してください。)
よって、この問題は各頂点を取り除いたときに全ての連結成分の頂点数が偶数になるかを判定する問題に帰着されます。
ここで、例えば全ての頂点の座標が等しいようなケースで辺の本数が
となってしまい、扱うことができません。
そこで、「頂点を1つ取り除いたときの連結性を判定する」という問題の性質から三重以上の頂点連結成分は二重まで緩めても解に影響が出ないことを利用して、下図のように辺の本数をに落とします。

また、全頂点数が奇数なので、頂点数が奇数の連結成分は少なくともつ存在しますが、
つ以上存在する場合は明らかに
つの頂点を削除するだけで全ての連結成分の頂点数を奇数にすることはできないので、頂点数が奇数の連結成分がただ
つ存在する場合のみ考えます。
さらに、そのような場合において頂点数が偶数の連結成分から頂点を取り除くのも明らかに不適なので、ただつの頂点数が奇数の連結成分にのみ着目します。
まず、「頂点を取り除いたときに連結でなくなるかどうか」が知りたいので、とりあえず二重頂点連結成分分解し、block-cut treeを作っておきましょう。

この連結成分において、関節点でない頂点をつ削除すると、連結性は崩れずに頂点数が
減るので、全ての連結成分の頂点数が偶数となります。では、関節点を取り除くとどうなるでしょうか?
これは実際に木から関節点を取り除いてみることですぐに結果がわかります。
すなわち、いくつかの部分木に分離するので、そのサイズの偶奇を確かめ、その中につでも奇数のものがあれば不適、そうでないなら適切となります。

あとは部分木のサイズが求まればよいので、根を適当に固定した木dpをすることで解くことができます。
以上より、全体で解けました。
ACコード
Submission #34866716 - AtCoder Regular Contest 045
(MLEとの戦いの結果、を求めないといったような実装例からの変更点が存在します。)
ソースコード全体
類題
https://onlinejudge.u-aizu.ac.jp/problems/3022
参考資料
lowlink
グラフにおける橋(bridge)を検出するアルゴリズム | アルゴリズムロジック
Biconnected component - Wikipedia
https://web.iitd.ac.in/~bspanda/biconnectedMTL776.pdf
オイラーツアー