【AtCoder】ARC111D:Orientation(600点)
強連結な有向グラフの作り方がわからなかったので、備忘録として。
問題
前提知識
- Lowlink
問題の概要
頂点辺の単純な無向グラフと正整数列が与えられる。
の各頂点から到達できる頂点の数(自身も含む)が、ちょうどになるように各辺に向き付けせよ。
制約
- 与えられるグラフに自己ループや多重辺は存在しない(非連結はあり得る)
- 与えられる入力には必ず解が存在する
解法
問題の言い換え
内の辺を適当にとってみます。
実は、とが一致していなければ、即座に向き付け可能です。
もしならば、明らかに辺の向きはとするしかありません。とした場合、から辿れる頂点はからも辿れるため、となってしまうためです。
一方で、の場合には、とは相互に到達できる必要があります。
この条件がを満たす任意の辺について成立している必要があることから、結局以下の問題が解ければよいです。
からを満たす辺のみを取り出したグラフを新たにとする。
の各連結成分に対して、相互に到達可能(=強連結)になるように向き付けせよ。
(ただし、問題の制約上、強連結になるように向き付けできることが保証されている。)
強連結なグラフの作り方
結論から言うと、強連結にできることが保証されている任意のグラフに対して、以下の操作で構築可能です。
連結グラフの頂点を一つ選び(とする)、からDFSを行い、通過した頂点と辺のみを使った根付き木(DFS木)を作る。DFS木上の辺を「根→葉」、それ以外の辺(後退辺)を「葉→根」となるようにグラフに向き付けを行うと、この連結成分は強連結になっている。
公式解説では詳細な証明は省略されていたので、上記操作で構築できることの証明を以下で行います。
操作の証明
方針
橋が存在するグラフは、明らかに強連結にできないことに注目します。つまり、今回与えられるグラフには橋が存在しません。
- 上記操作後のグラフが強連結でないと仮定した場合、には橋が存在する
ということを証明することで、橋が存在しないにおいては、操作により必ず強連結にできることを証明します。
本体
まず、適当な頂点を根として選び、これをとします。
を始点としてDFSを行うことでDFS木を作り、辿った順番に頂点に番号をつけます。さらに、先程の操作の説明通り、DFS木上の辺を「根→葉」、後退辺を「葉→根」となるようにグラフに向き付けを行います。
ここで、Lowlinkという概念を導入します。詳細は省略しますが、定義は以下のとおりです。
なお、先程つけた頂点番号はと一致します。
頂点に初めて到達した時刻
頂点からDFS木上の辺を葉方向に0回以上、後退辺を高々1回以下辿って到達できる頂点
↓こんな感じ
さて、操作後のグラフが強連結でないと仮定します。すると仮定より、ある頂点から頂点には辿り着けないことがわかります。
(証明:DFS木の定義上、頂点からは任意の頂点に辿り着けることから、任意の頂点から頂点に辿り着けたら自動的に強連結になってしまうため。)
このとき、ある頂点について、が成立します。
(証明:の定義より、から頂点番号の頂点までは辿ることができるので、となる場合はより番号が小さい頂点に到達できることになってしまい、これを繰り返すと最終的にに到達できてしまうため。)
また、DFS木の作り方から、ある頂点について、DFS木上の辺が存在します。
この辺についてが成立していますが、
この式が成立しているとき、上の辺は橋になっているという有名な事実があります。
(「橋 列挙 アルゴリズム」などで検索すると色々出てきます。こちらなど→グラフ探索アルゴリズムとその応用)
よって、「上記操作で強連結にできない→橋が存在する」が証明できたため、この操作でうまくいくことが示されました。
コード(Python3)
def main(): # 入力 N, M = map(int, input().split()) AB = [list(map(int, input().split())) for _ in [0]*M] C = list(map(int, input().split())) # 隣接グラフGの構築 G = [[] for _ in [0]*N] for a, b in AB: G[a-1].append(b-1) G[b-1].append(a-1) # 辺に番号をつける edge = dict() for i in range(M): a, b = AB[i] edge[(a-1, b-1)] = i # 各頂点を始点にDFSを行い、両端のCが同じ辺のみを抽出、連結成分ごとに分解 # 両端のCが異なる辺は即座に向き付け可能 ans = [None]*M # 各頂点の向き visited = [False]*N # 探索済みかどうか G2 = [] # 各連結成分を保存 for s in range(N): if visited[s]: continue visited[s] = True stack = [s] # 探索用スタック H = set() # 連結成分の辺を保存、重複の可能性があるのでsetで while stack: u = stack.pop() for v in G[u]: # 決まらない場合 if C[u] == C[v]: # 辺を保存 H.add((u, v)) if not visited[v]: stack.append(v) visited[v] = True # 決まる場合 elif C[u] > C[v]: if (u, v) in edge.keys(): ans[edge[(u, v)]] = "->" else: ans[edge[(v, u)]] = "<-" else: if (u, v) in edge.keys(): ans[edge[(u, v)]] = "<-" else: ans[edge[(v, u)]] = "->" if H: # リストに直して格納 G2.append(list(H)) # 各連結成分について、DFSを行い木を作る edge_count = [0]*N # 何番目の辺まで見たか for H in G2: # Hは隣接グラフになっていないので、隣接グラフH_graphを作る H_graph = [[] for _ in [0]*N] for u, v in H: H_graph[u].append(v) H_graph[v].append(u) root = H[0][0] # 適当な頂点を根とする dist = [-1]*N # 根からの距離 dist[0] = 0 stack = [root] # DFSを行う while stack: u = stack[-1] # 全ての辺を見終わっていた場合 if edge_count[u] >= len(H_graph[u]): stack.pop() continue # 次の探索先を選ぶ v = H_graph[u][edge_count[u]] edge_count[u] += 1 # 探索先が未訪問だった場合 if edge_count[v] == 0: visited[v] = True dist[v] = len(stack) stack.append(v) # DFS木の根方向から葉方向に向き付け if (u, v) in edge.keys(): ans[edge[(u, v)]] = "->" else: ans[edge[(v, u)]] = "<-" # 後退辺は根方向に向き付け for u, v in H: if (u, v) not in edge.keys(): v, u = u, v # 向き付け済みの場合はスルー if ans[edge[(u, v)]] is not None: continue if dist[u] > dist[v]: ans[edge[(u, v)]] = "->" else: ans[edge[(u, v)]] = "<-" # 出力 for elm in ans: print(elm) main()