` \gdef\vec#1{\mathbf{#1}} \gdef\vecb{\vec{b}} \gdef\veczero{\vec{0}} \gdef\vecf{\vec{f}} \gdef\exG{\overleftrightarrow{G}} \gdef\label{\operatorname{label}} \gdef\ce{\operatorname{current\_edge}} \gdef\fe{\operatorname{first\_edge}} \gdef\ne{\operatorname{next\_edge}} `

Dinic 法とその時間計算量

要約

この記事では, 理論的な側面やその背景にはあまり立ち入らない. 定数倍なども特に気にせず, "教科書に書いてある Dinic 法" と "よくある Dinic 法の実装" の間を埋めることと, その計算量を解析することを目的としている.

昔書いた記事 最大流問題について, 最大流問題について その3 の一部を詳しく説明した感じです.

記号

O(n+m)=O(m)O(n + m) = O(m) と簡略にしたいので(必要ならば連結成分を取り出して), n=O(m)n = O(m) を仮定する.

Dinic 法の概要

Dinic 法は, 暫定解であるフロー f\vecf を持ち, 次の2つを ss から tt への残余パスが無くなるまで繰り返すアルゴリズムである.

dual step:2

残余ネットワーク GfG_\vecf 上で, ss から tt への最短経路 DAG, すなわち最短 ss-tt パスに含まれうる頂点/辺のみからなるグラフを求める. ここで, 各辺のコストは 1, つまり通る辺の個数を最小化する経路を求めるものとする.

primal step3,4

HH を前回の dual step で求めた DAG で初期化し, HH が空グラフになるまで, 次を繰り返す.

  1. H 上での ss-tt パスを一つ求める.
  2. そのパス上に流せるだけ流す.
  3. 残余容量が 00 になった辺と, ss から辿り着けない頂点, tt へ辿り着けない頂点, それらに隣接する辺を HH から取り除く.

この繰り返しの後, dual step で求めた DAG 上で流したフローを, 暫定解である GG 上のフロー f\vecf に反映する. これにより, 残余ネットワーク, 特に逆辺の残余容量も更新される.

計算量

さて, このアルゴリズムの計算量を調べよう.

いま, ひとつの dual-primal step に注目する. この dual step での ss から vv への最短路長(路が無いならば \infty)を label(v)\label(v), l=label(t)l = \label(t) とすると, ss から到達可能な任意の辺 (u,v)(u, v) に対し, label(u)+1label(v)\label(u) + 1 \ge \label(v) である. ss-tt 最短路 DAG を H0H_0 とする. このとき, ss から辿り着け, tt へ辿り着けるような辺 (u,v)(u, v)H0H_0 に含まれるためには, label(u)+1=label(v)\label(u) + 1 = \label(v) であることが必要である. 直後の primal step の後, HH 上のフローを f\vecf に反映する際に, GfG_\vecf の辺が追加/削除されるが,

の2つが成り立つ.

更新後の GfG_\vecfss-tt パスを一つとる. このパス上での label\label の値からなる列を考えると, 高々 11 ずつしか増加しない初項 00, 末項 ll の数列であるから, これが長さ ll 以下となるには, このパスに含まれる全ての辺 (u,v)(u, v)label(u)+1=label(v)\label(u) + 1 = \label(v) を満たし, 長さ ll となるしかない. しかし, このような辺は新たに GfG_\vecf に加わったものではないので, このパス更新前の GfG_\vecf の長さ llss-tt パス, 従って H0H_0 上の ss-tt パスであったことになり, primal step でその少なくとも一辺が削除されたことに矛盾する. 従って, 更新後の GfG_\vecf の任意の ss-tt パスの長さが ll より長く, 次の dual step では ss から tt への最短路長が真に長くなる事がわかった. ss-tt パスの長さは高々 n1n-1 であるから, dual-primal step は高々 n1n-1 回しか繰り返されないことがわかる.

次に, dual step は, 幅優先探索で O(m)O(m) で実現できる.

最後に, primal step の計算量を考えよう. HH は最短経路 DAG であったから, ss から出る辺を辿るだけで ss-tt パスを一つ求めることが出来る. HH の更新についても, 残余容量が 00 になった辺を削除した後, 入次数や出次数が 00 になった頂点とそれに接続する辺を削除することを繰り返せばよいから, 取り除く頂点と辺の数の線形時間, 従ってこの primal step 内で合計 O(m+n)O(m+n) で行える. フローを流す度に辺が少なくとも一本削除されるから, 合計で O(nm+m+n)=O(nm)O(nm + m + n) = O(nm) となる.

dual-primal step は高々 n1n-1 回行われ, dual step が O(m)O(m), primal step が O(nm)O(nm) であったから, 合計で O(n2m)O(n^2 m) である.

Current-Edge data structure を用いた実装方法

上では, HH を毎 dual-primal step で陽に作り, そこから頂点や辺を削除していく方針を見た. しかし, HHHH 上のフローを陽に保持することなく, G\exG と暫定解としての G\exG 上のフロー, いくつかの補助変数を用いて実装するのが 非常に 一般的である.

dual step:

dual step では陽に DAG を作らず, ss からの距離(もしくは tt への距離. この場合符号を反転するか大小関係を逆にして読むこと.) label(u)\label(u) を求める. すると, ss-tt パスは, その全ての辺で label(u)+1=label(v)\label(u) + 1 = \label(v) を満たすとき, またその時に限り, 所望の DAG に含まれる.

primal step: 前提として, 隣接リストなどのデータ構造を用い, 各頂点 uu に対し, G\exG 上の uu から出る辺 (u,v)(u, v) を適当に固定された順序でイテレート出来るものとする. すなわち, "最初の辺" を返す first_edge(v)\fe(v) と, "あるならば次の辺, 無いなら特別な値 \bot を返す" next_edge(e)\ne(e) が, それぞれ O(1)O(1) で出来るものとする.

残余容量が正で, label(u)+1=label(v)\label(u) + 1 = \label(v) である辺 e=(u,v)e = (u, v) を, admissible であると呼ぶ. Admissible であり, ss から admissible な辺のみを用いて辿りつける始点と tt に admissible な辺のみを用いて辿り着ける終点を持つ辺が, 概要の節で述べた HH の辺に対応する.

primal step では, dual step で求めた label()\label(\cdot) に加えて current_edge()\ce(\cdot) を用る. また, 概要の節でのアルゴリズムの "3. 残余容量が 00 になった辺と, ss から辿り着けない頂点, tt へ辿り着けない頂点を HH から取り除く." は, 遅延して実行する.

最初に, vtv \neq t に対しては current_edge(v)=first_edge(v)\ce(v) = \fe(v), v=tv = t に対しては current_edge(t)=\ce(t) = \bot と初期化する. この current_edge\ce を用いて, 概要の節の HH 上のパスを一つ求めるアルゴリズムは, 次のように実現できる.

  1. i=0,v0=si = 0, v_0 = s とする.5
  2. current_edge(vi)\ce(v_i) \neq \bot だが current_edge(vi)\ce(v_i) が admissible でないとき:
    1. current_edge(vi)\ce(v_i)next_edge(current_edge(vi))\ne(\ce(v_i)) で更新する.
    2. 2.へ戻る.
  3. current_edge(vi)\ce(v_i) \neq \bot のとき:
    1. current_edge(vi)=(vi,vi+1)\ce(v_i) = (v_i, v_{i+1}) となるよう vi+1v_{i+1} を定める
    2. iii+1i+1 に更新する
    3. 2.へ戻る.
  4. vi=sv_i = s, すなわち i=0i = 0 のとき:
    1. admissible な辺からなる ss-tt パスを発見出来なかったことを報告し, 終了する.
  5. vitv_i \neq t のとき:
    1. iii1i-1 で更新する.
    2. current_edge(vi)\ce(v_i)next_edge(current_edge(vi))\ne(\ce(v_i)) で更新する.
    3. 2.へ戻る.
  6. vi=tv_i = t のとき:
    1. パス s=v0,,vi=ts = v_0, \dots, v_i = t を報告し, 終了する.

このアルゴリズムにおける重要な不変条件として,

がある. この不変条件が正しいことは, current_edge(vi)\ce(v_i) を更新する箇所, 具体的には 2.1. と 5.2. で保たれることと, ある時点で admisslbe で無い辺は以後 admissible にならないことを確認すればわかる. また, このアルゴリズム自体が正しいことは, 不変条件から示せる.

から, このアルゴリズムの計算量は, 最終的に報告するパスの長さと current_edge()\ce(\cdot) を変更した回数の和に関する線形時間である.

さて, このアルゴリズムで報告されたパスに対して, 含まれる辺の残余容量の最小値と同じだけフローを f\vecf に追加し, これによって残余容量が 00 となった辺 e=(u,v)e=(u, v) に対し, current_edge(u)\ce(u)next_edge(e)\ne(e) で更新する.6 すると, 次のパスを探索する際は, 上のアルゴリズムを current_edge()\ce(\cdot) を再度初期化することなく適用することができる. このことの正しさは, やはり不変条件が保たれることからわかる.

Admissible な辺からなる ss-tt パスが無くなるまでこれを実行すると, current_edge()\ce(\cdot) は合計高々 mm 回変更される7が, 6.1. によってパスを得る度に一度変更するから, このアルゴリズムが実行されるのは高々 m+1m+1 回である. また, 各実行で O(n)O(n) に加えて current_edge()\ce(\cdot) の変更回数に関する線形時間かかるから, 合計 O(mn+m)=O(nm)O(m n + m) = O(nm) かかる.

dual-primal step が高々 n1n-1 回なのは前の解析と変わらないから, 合計 O(n2m)O(n^2 m) である.

Current-Edge data structure の実装を間違えた場合

次の C++ コードは, 上のアルゴリズムの非常によくある実装の一部を抜き出したものである.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Flow primal(const Flow current_path_cap, const size_t v) {
  if (v == target) return current_path_cap;
  for (size_t &i = current_edge[v]; i < edges[v].size(); ++i) {
    auto &e = edges[v][i];
    if (e.flow < e.capacity && label[e.to] == label[e.from] + 1) {
      // recurse with e
      const Flow f = primal(std::min(e.capacity - e.flow, current_path_cap), e.to);
      if (f == 0) continue;
      e.flow += f;
      e.reverse->flow -= f;
      return f;
    }
  }
  return 0;
}

この size_t & i = current_edge[v]&非常に 重要である. これを忘れた場合, 前節で説明した Current-Edge data structure を使わず, 全ての残余パスを調べることになる. ss-tt パスの本数は nn の指数オーダーありうるので, 指数オーダーのアルゴリズムになってしまう.

このような間違った実装法で Θ(2n/2)\Theta(2^{n/2}) になるようなインスタンスを生成するプログラム, n=100n = 100 で生成されたインスタンスと, 実際に間違った実装法での実装が ここ に置いてある. primal step, dual step はそれぞれ ss 側と tt 側から出来るため, 44 通りの実装方法があるが, そのいずれでも Θ(2n/2)\Theta(2^{n/2}) となっているハズなので, 自分の実装が不安な場合はこの生成器を使って試してみるとよいだろう.

動的木を使った実装

この節は読み飛ばしても差支えない.

前節の current_edge()\ce(\cdot) は, ss を葉の一つ, current_edge(v)=\ce(v) = \bot な頂点を根とする内向き森になっている. この森を GetRoot, GetLastEdgeInPath8, Link, Cut, GetValue9, AddToPath10, MinNodeOfPath11O(logn)O(\log n) でサポートする動的木, 例えば Sleator-Tarjan Link/Cut tree で管理すると, O(logn)O(\log n) について一度 current_edge()\ce(\cdot) を更新する(Link/Cut)か admissible なパスにフローを流す(MinNodeOfPath + AddToPath)ことができる.

具体的には, 動的木を辺数 00 で初期化し, 各頂点には開始時点の残余容量で値を付与した後, 次を行う.

  1. v=GetRoot(s)v = \operatorname{GetRoot}(s) とする.
  2. current_edge(v)\ce(v) \neq \bot だが current_edge(v)\ce(v) が admissible でないとき:
    1. current_edge(v)\ce(v)next_edge(current_edge(v))\ne(\ce(v)) で更新する.
    2. 2.へ戻る.
  3. current_edge(v)\ce(v) \neq \bot のとき:
    1. Link(current_edge(v))\operatorname{Link}(\ce(v)) する
    2. 1.へ戻る.
  4. v=sv = s のとき:
    1. admissible な辺からなる ss-tt パスを発見出来なかったことを報告し, 終了する.
  5. vtv \neq t のとき:
    1. e=(u,v)=GetLastEdgeInPath(s)e = (u, v) = \operatorname{GetLastEdgeInPath}(s) とする.
    2. Cut(e)\operatorname{Cut}(e) する.
    3. current_edge(u)\ce(u)next_edge(e)\ne(e) で更新する.
    4. 1.へ戻る.
  6. v=tv = t のとき:
    1. u=MinNodeOfPath(s)u = \operatorname{MinNodeOfPath}(s) とし, f=GetValue(u)f = \operatorname{GetValue}(u) とする.
    2. AddToPath(s,f)\operatorname{AddToPath}(s, -f) する.
    3. u=MinNodeOfPath(s)u = \operatorname{MinNodeOfPath}(s) とし, e=current_edge(u)e = \ce(u), f=GetValue(u)f = \operatorname{GetValue}(u) とする.
    4. f0f \neq 0 ならば 1.へ戻る.
    5. Cut(e)\operatorname{Cut}(e) する.
    6. current_edge(u)\ce(u)next_edge(e)\ne(e) で更新する.
    7. 6.3.へ戻る.

このアルゴリズムの終了時点での各頂点に付与された値に応じて, 暫定解のフロー量を更新する. このアルゴリズムは primal step を O(mlogn)O(m \log n) で実装し, 全体として O(nmlogn)O(n m \log n) の最大流アルゴリズムを与えている.

特殊なネットワーク上での計算量

!!!以下, 辺容量の整数性を仮定する!!!

動的木を用いた実装については一旦忘れて, 特殊なグラフ上で Current-Edge data structure を用いた実装を実行したときに, Dinic 法の計算量がより小さくなることを見る.

最大流量に関する計算量

辺容量が整数であるから, フローを更新する際, その値は少なくとも 11 増える. 従って, 最大流量が FF であるとすると, フローを更新できるのは高々 FF 回である. dual-primal step 一回につき, 少なくとも一回フローを更新できるから, dual step は合計で O(Fm)O(F m).

一方, primal step はパスを発見する度にフローを更新する. この計算量は, 発見するパスの長さと current_edge()\ce(\cdot) の更新回数の和に関する線形時間であった. ここで, パスの長さは O(n)O(n), パスを発見する回数は全 dual-primal step 合わせて O(F)O(F) であり, current_edge()\ce(\cdot) の更新回数は一つの primal step につき O(m)O(m) であったから, 全て合わせて O(Fm+Fn+Fm)=O(Fm)O(F m + F n + F m) = O(F m) である.

辺容量が高々定数/辺容量の平均値が高々定数

最大流量に関する計算量の議論は, Dinic 法の実行中でも成立する. すなわち, 実行中のある時点で, 残余グラフ上の最大流量が F(1)F (\ge 1) となったとき, その後 O(Fm)O(F m) でアルゴリズムは終了する. 最大流-最小カット定理から, 実行中のある時点でのある残余カットの容量が F(1)F (\ge 1) であるとき, その後 O(Fm)O(F m) でアルゴリズムが終了することも言える.

さて, 辺容量の平均が kk であるとする. primal step で発見したパスに属する各辺からは, 残余容量が少なくとも 11 減らされる. 残余容量の合計は O(km)O(k m) であるから, 各 primal step は O(km+m)=O(km)O(k m + m) = O(km) である.

一方, l{1,,n1}l \in \set{1,\dots,n-1} を任意にひとつ選び, llabel(t)l \ge \label(t) であるような最初の dual-primal step が開始した時点を考える. Vi={v|label(t)=i},Wi={v|label(t)i}=j=0iUjV_i = \set{ v \setmid \label(t) = i}, W_i = \set{ v \setmid \label(t) \le i } = \cup_{j=0}^{i} U_j, とすると, 残余容量が正であるような辺 (u,v)(u, v)label(u)+1label(v)\label(u) + 1 \ge \label(v) であるから, WiW_i による残余カット容量は ViV_iVi+1V_{i+1} の間の辺の残余容量の和である. 従って, WiW_i による残余カット容量の(ii を動かしたときの)和は辺容量の和以下, すなわち高々 kmkm である. WiW_i0i<l0 \le i < lss-tt カットを与えるから, この ll 個のカットの残余カット容量の最小値は km/lkm / l 以下である.

以上から, 各辺の容量が高々 kk であるとき, llabel(t)l \ge \label(t) であるような最初の dual-primal step に至るまでにかかる時間計算量は O(kml)O(kml) であり, この後最大流に至るまでの時間計算量は O((km/l)m)O((km / l) m) であるから, 合計で O(kml+(km/l)m)O(kml + (km/l) m) であり, l=ml = \sqrt{m} とすれば O(km3/2)O(k m^{3/2}) であることがわかった.

更に, 各辺の容量が高々 kk で, 多重辺が無いとする. 0i<l0 \le i < l に対する ViV_i を, (V0,V1),(V2,V3),(V_0, V_1), (V_2, V_3), \dots のようにペアに分割すると, 少なくとも1つのペア (Vi,Vi+1)(V_i, V_{i+1}) の要素数の合計が O(2n/l)=O(n/l)O(2n / l) = O(n / l) である. 従って, このペア間の辺の残余容量の合計, すなわち WiW_i による残余カット容量は O(k(n/l)2)O(k (n / l)^2) である. よって, 各辺の容量が高々 kk で多重辺が無いとき, Dinic 法が O(kml+k(n/l)2m)O(kml + k (n/l)^2 m) であり, l=n2/3l = n^{2/3} とすれば O(kn2/3m)O(k n^{2/3} m) であることがわかった.

頂点容量の平均値が高々定数

頂点の容量を, その頂点へ入る辺の容量の和と, その頂点を出る辺の容量の和の小さい方とし, その(頂点を動かした時の)平均値を kk とする. すなわち, k=avgvmin(eδ+(v)ue,eδ(v)ue)k = \operatorname{avg}_v \min(\sum_{e \in \delta^+(v)} u_e, \sum_{e \in \delta^-(v)} u_e) とする. 前節と同様, l=label(t)l = \label(t) のときの ViV_iWiW_i を考える. Ai={vVi|eδ+(v)ue<eδ(v)ue},Bi=ViAi A_i = \set{ v \in V_i \setmid \sum_{e \in \delta^+(v)} u_e < \sum_{e \in \delta^-(v)} u_e }, \quad B_i = V_i \setminus A_i とし, Ui=Wi1AiU_i = W_{i-1} \cup A_i とすると, UiU_i による残余カット容量は eδ+(Ai)ue+eδ(Bi)ue\sum_{e \in \delta^+(A_i)} u_e + \sum_{e \in \delta^-(B_i)} u_e で上から抑えられる. これの(ii を動かしたときの)和は高々 knkn だから, 少なくとも一つの iiUiU_i による残余カット容量が高々 kn/lkn / l になる.

よって, 各頂点の容量が高々 kk であるとき, Dinic 法が O(kml+k(n/l)m)O(kml + k(n/l)m) であり, l=nl = \sqrt{n} とすれば O(knm)O(k \sqrt{n} m) であることがわかった.

二部グラフ (U,V)(U, V) 上の二部マッチングを, 頂点 ss, ttss から uUu \in U, vVv \in V から tt への辺を加えたグラフ上で, 辺容量 11 として最大流問題に帰着する場合, ss へ入る辺, tt から出る辺の容量和はそれぞれ 00 であり, uUu \in U へ入る辺, vVv \in V から出る辺の容量和は 11 であるから, 上で k=1k = 1 とした場合の計算量 O(nm)O(\sqrt{n} m) を得る.

例外がある場合

上の証明は, 辺容量や頂点容量が kk を超えたり, 多重辺がある場合でも, そのような頂点/辺が高々有限個であるときは, ちょっと変更すればオーダーとしては同じものが得られる. 定数個の, 条件を満たさない超頂点を加えた場合などに役に立つだろう.

GCD

容量に関しては, min と加減算程度しか使わないため, 容量の最大公約数 g=GCDeueg = \operatorname{GCD}_{e} u_e で各容量を割ってもアルゴリズムの挙動は変わらない. これを用いると, 例えば容量が 11 とは限らない定数の場合でも, k=1k=1 として上の結果を利用してよいことがわかる.

余談

上の証明を見ていればわかるように, 多くのフローが序盤の dual-primal step で流れる事が期待される. 簡単なインスタンス, 例えばランダムグラフは, 経験的に, この性質を強くもつものが多い. 12 一方で, 計算量には, パスが見つかった際に流すフローで, そのパスのどれだけの辺を消滅させられるかも関わってくる. こちらは, 上の容量が高々定数な場合のように, フロー空間が縮退しているインスタンスが簡単になる.

参考文献

  1. δ+(v)\delta^+(v): vv から出る辺全体の集合, δ(v)\delta^-(v): vv へ入る辺全体の集合. 

  2. これが dual step と呼ばれているのを見たことはない. しかし, 線形計画問題として定式化したときの双対の ε\varepsilon 緩和を考え, 主の解を更新せずに双対の解を更新することで, なるべく小さい ε\varepsilon に対する ε\varepsilon-optimality を満たすようにする操作である. 

  3. dual step に対応し, こちらは ε\varepsilon-optimality を保ちつつ主問題の目的関数値を増大させている. 

  4. これは dual step で求めた DAG の上でブロッキングフローを求める操作である. 

  5. 今回は ss から出発するようにしたが, tt から始めてもよい. その場合さまざまなものが逆転する. 

  6. current_edge(u)\ce(u) は更新しなくても, 計算量は変わらない. ここでは, 計算量解析の簡単さを優先した. 

  7. current_edge()\ce(\cdot) は辺のイテレータで, 各辺一度しか出てこない. 

  8. vv に対し vv から vv を含む木の根へのパスに含まれる辺で, 最も根に近いもの. 

  9. 各頂点には値(具体的には current_edge(v)\ce(v) の残余容量)が付与されている. vv に対し, この付与された値を返す. 

  10. vv に対し vv から vv を含む木の根へのパスに含まれる頂点に付与された値に xx を足し込む. 

  11. vv に対し vv から vv を含む木の根へのパスに含まれる頂点のうち, 付与された値が最も小さいものを返す. 複数あるならば, 最も根に近いものを返す. 

  12. 個人の感想です.