Dinic 法とその時間計算量
要約
- 一般に, 頂点 辺のグラフと実数の辺容量 が与えられたとき, このネットワーク上での Dinic 法の計算量は である.
- 辺容量が整数のとき,
- 最大流を とすると, でもある.
- 辺容量の平均値が のとき, でもある.
- 辺容量の最大値が で多重辺が無いとき, でもある.
- 各頂点を通れるフロー量の平均値が 以下, すなわち なとき, 計算量は でもある.1
- 二部マッチングのときは, 上が で成立し, .
- 容量が整数は必須だけれど, 他の条件は高々定数個の例外があっても OK.
- 動的木を使うと, 一般のグラフで になる.
- 実装をミスると指数オーダー.
この記事では, 理論的な側面やその背景にはあまり立ち入らない. 定数倍なども特に気にせず, "教科書に書いてある Dinic 法" と "よくある Dinic 法の実装" の間を埋めることと, その計算量を解析することを目的としている.
昔書いた記事 最大流問題について, 最大流問題について その3 の一部を詳しく説明した感じです.
記号
- : に逆辺を追加したやつ.
- : に関する残余ネットワーク. に容量を, 順辺は , 逆辺は で入れたやつ.
- : の辺の数. つまり元のグラフの倍.
と簡略にしたいので(必要ならば連結成分を取り出して), を仮定する.
Dinic 法の概要
Dinic 法は, 暫定解であるフロー を持ち, 次の2つを から への残余パスが無くなるまで繰り返すアルゴリズムである.
dual step:2
残余ネットワーク 上で, から への最短経路 DAG, すなわち最短 - パスに含まれうる頂点/辺のみからなるグラフを求める.
ここで, 各辺のコストは 1
, つまり通る辺の個数を最小化する経路を求めるものとする.
を前回の dual step で求めた DAG で初期化し, が空グラフになるまで, 次を繰り返す.
- H 上での - パスを一つ求める.
- そのパス上に流せるだけ流す.
- 残余容量が になった辺と, から辿り着けない頂点, へ辿り着けない頂点, それらに隣接する辺を から取り除く.
この繰り返しの後, dual step で求めた DAG 上で流したフローを, 暫定解である 上のフロー に反映する. これにより, 残余ネットワーク, 特に逆辺の残余容量も更新される.
計算量
さて, このアルゴリズムの計算量を調べよう.
いま, ひとつの dual-primal step に注目する. この dual step での から への最短路長(路が無いならば )を , とすると, から到達可能な任意の辺 に対し, である. - 最短路 DAG を とする. このとき, から辿り着け, へ辿り着けるような辺 が に含まれるためには, であることが必要である. 直後の primal step の後, 上のフローを に反映する際に, の辺が追加/削除されるが,
- に追加されうる辺 は, の逆辺, 従って となる辺のみである.
- 上の任意の - パスは, その少なくとも一辺が から削除される.
の2つが成り立つ.
更新後の の - パスを一つとる. このパス上での の値からなる列を考えると, 高々 ずつしか増加しない初項 , 末項 の数列であるから, これが長さ 以下となるには, このパスに含まれる全ての辺 で を満たし, 長さ となるしかない. しかし, このような辺は新たに に加わったものではないので, このパス更新前の の長さ の - パス, 従って 上の - パスであったことになり, primal step でその少なくとも一辺が削除されたことに矛盾する. 従って, 更新後の の任意の - パスの長さが より長く, 次の dual step では から への最短路長が真に長くなる事がわかった. - パスの長さは高々 であるから, dual-primal step は高々 回しか繰り返されないことがわかる.
次に, dual step は, 幅優先探索で で実現できる.
最後に, primal step の計算量を考えよう. は最短経路 DAG であったから, から出る辺を辿るだけで - パスを一つ求めることが出来る. の更新についても, 残余容量が になった辺を削除した後, 入次数や出次数が になった頂点とそれに接続する辺を削除することを繰り返せばよいから, 取り除く頂点と辺の数の線形時間, 従ってこの primal step 内で合計 で行える. フローを流す度に辺が少なくとも一本削除されるから, 合計で となる.
dual-primal step は高々 回行われ, dual step が , primal step が であったから, 合計で である.
Current-Edge data structure を用いた実装方法
上では, を毎 dual-primal step で陽に作り, そこから頂点や辺を削除していく方針を見た. しかし, や 上のフローを陽に保持することなく, と暫定解としての 上のフロー, いくつかの補助変数を用いて実装するのが 非常に 一般的である.
dual step:
dual step では陽に DAG を作らず, からの距離(もしくは への距離. この場合符号を反転するか大小関係を逆にして読むこと.) を求める. すると, - パスは, その全ての辺で を満たすとき, またその時に限り, 所望の DAG に含まれる.
primal step: 前提として, 隣接リストなどのデータ構造を用い, 各頂点 に対し, 上の から出る辺 を適当に固定された順序でイテレート出来るものとする. すなわち, "最初の辺" を返す と, "あるならば次の辺, 無いなら特別な値 を返す" が, それぞれ で出来るものとする.
残余容量が正で, である辺 を, admissible であると呼ぶ. Admissible であり, から admissible な辺のみを用いて辿りつける始点と に admissible な辺のみを用いて辿り着ける終点を持つ辺が, 概要の節で述べた の辺に対応する.
primal step では, dual step で求めた に加えて を用る. また, 概要の節でのアルゴリズムの "3. 残余容量が になった辺と, から辿り着けない頂点, へ辿り着けない頂点を から取り除く." は, 遅延して実行する.
最初に, に対しては , に対しては と初期化する. この を用いて, 概要の節の 上のパスを一つ求めるアルゴリズムは, 次のように実現できる.
- とする.5
- だが が admissible でないとき:
- を で更新する.
- 2.へ戻る.
- のとき:
- となるよう を定める
- を に更新する
- 2.へ戻る.
- , すなわち のとき:
- admissible な辺からなる - パスを発見出来なかったことを報告し, 終了する.
- のとき:
- を で更新する.
- を で更新する.
- 2.へ戻る.
- のとき:
- パス を報告し, 終了する.
このアルゴリズムにおける重要な不変条件として,
- 辺 が admissible かつ から への admissible な辺から成るパスがあるならば, それは 以降の辺である. (逆は必ずしも真ではない)
がある. この不変条件が正しいことは, を更新する箇所, 具体的には 2.1. と 5.2. で保たれることと, ある時点で admisslbe で無い辺は以後 admissible にならないことを確認すればわかる. また, このアルゴリズム自体が正しいことは, 不変条件から示せる.
- 3.2. で に加えた は, 6.1. で報告するパスの長さか, 5.1. で減らされる に寄与する.
- 2.1, 5.1. で を変更している.
- 1., 4., 6. はそれぞれ高々1回しか実行されない.
から, このアルゴリズムの計算量は, 最終的に報告するパスの長さと を変更した回数の和に関する線形時間である.
さて, このアルゴリズムで報告されたパスに対して, 含まれる辺の残余容量の最小値と同じだけフローを に追加し, これによって残余容量が となった辺 に対し, を で更新する.6 すると, 次のパスを探索する際は, 上のアルゴリズムを を再度初期化することなく適用することができる. このことの正しさは, やはり不変条件が保たれることからわかる.
Admissible な辺からなる - パスが無くなるまでこれを実行すると, は合計高々 回変更される7が, 6.1. によってパスを得る度に一度変更するから, このアルゴリズムが実行されるのは高々 回である. また, 各実行で に加えて の変更回数に関する線形時間かかるから, 合計 かかる.
dual-primal step が高々 回なのは前の解析と変わらないから, 合計 である.
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 を使わず, 全ての残余パスを調べることになる.
- パスの本数は の指数オーダーありうるので, 指数オーダーのアルゴリズムになってしまう.
このような間違った実装法で になるようなインスタンスを生成するプログラム, で生成されたインスタンスと, 実際に間違った実装法での実装が ここ に置いてある. primal step, dual step はそれぞれ 側と 側から出来るため, 通りの実装方法があるが, そのいずれでも となっているハズなので, 自分の実装が不安な場合はこの生成器を使って試してみるとよいだろう.
動的木を使った実装
この節は読み飛ばしても差支えない.
前節の は, を葉の一つ, な頂点を根とする内向き森になっている. この森を GetRoot, GetLastEdgeInPath8, Link, Cut, GetValue9, AddToPath10, MinNodeOfPath11 を でサポートする動的木, 例えば Sleator-Tarjan Link/Cut tree で管理すると, について一度 を更新する(Link/Cut)か admissible なパスにフローを流す(MinNodeOfPath + AddToPath)ことができる.
具体的には, 動的木を辺数 で初期化し, 各頂点には開始時点の残余容量で値を付与した後, 次を行う.
- とする.
- だが が admissible でないとき:
- を で更新する.
- 2.へ戻る.
- のとき:
- する
- 1.へ戻る.
- のとき:
- admissible な辺からなる - パスを発見出来なかったことを報告し, 終了する.
- のとき:
- とする.
- する.
- を で更新する.
- 1.へ戻る.
- のとき:
- とし, とする.
- する.
- とし, , とする.
- ならば 1.へ戻る.
- する.
- を で更新する.
- 6.3.へ戻る.
このアルゴリズムの終了時点での各頂点に付与された値に応じて, 暫定解のフロー量を更新する. このアルゴリズムは primal step を で実装し, 全体として の最大流アルゴリズムを与えている.
特殊なネットワーク上での計算量
!!!以下, 辺容量の整数性を仮定する!!!
動的木を用いた実装については一旦忘れて, 特殊なグラフ上で Current-Edge data structure を用いた実装を実行したときに, Dinic 法の計算量がより小さくなることを見る.
最大流量に関する計算量
辺容量が整数であるから, フローを更新する際, その値は少なくとも 増える. 従って, 最大流量が であるとすると, フローを更新できるのは高々 回である. dual-primal step 一回につき, 少なくとも一回フローを更新できるから, dual step は合計で .
一方, primal step はパスを発見する度にフローを更新する. この計算量は, 発見するパスの長さと の更新回数の和に関する線形時間であった. ここで, パスの長さは , パスを発見する回数は全 dual-primal step 合わせて であり, の更新回数は一つの primal step につき であったから, 全て合わせて である.
辺容量が高々定数/辺容量の平均値が高々定数
最大流量に関する計算量の議論は, Dinic 法の実行中でも成立する. すなわち, 実行中のある時点で, 残余グラフ上の最大流量が となったとき, その後 でアルゴリズムは終了する. 最大流-最小カット定理から, 実行中のある時点でのある残余カットの容量が であるとき, その後 でアルゴリズムが終了することも言える.
さて, 辺容量の平均が であるとする. primal step で発見したパスに属する各辺からは, 残余容量が少なくとも 減らされる. 残余容量の合計は であるから, 各 primal step は である.
一方, を任意にひとつ選び, であるような最初の dual-primal step が開始した時点を考える. , とすると, 残余容量が正であるような辺 は であるから, による残余カット容量は と の間の辺の残余容量の和である. 従って, による残余カット容量の( を動かしたときの)和は辺容量の和以下, すなわち高々 である. は で - カットを与えるから, この 個のカットの残余カット容量の最小値は 以下である.
以上から, 各辺の容量が高々 であるとき, であるような最初の dual-primal step に至るまでにかかる時間計算量は であり, この後最大流に至るまでの時間計算量は であるから, 合計で であり, とすれば であることがわかった.
更に, 各辺の容量が高々 で, 多重辺が無いとする. に対する を, のようにペアに分割すると, 少なくとも1つのペア の要素数の合計が である. 従って, このペア間の辺の残余容量の合計, すなわち による残余カット容量は である. よって, 各辺の容量が高々 で多重辺が無いとき, Dinic 法が であり, とすれば であることがわかった.
頂点容量の平均値が高々定数
頂点の容量を, その頂点へ入る辺の容量の和と, その頂点を出る辺の容量の和の小さい方とし, その(頂点を動かした時の)平均値を とする. すなわち, とする. 前節と同様, のときの や を考える. とし, とすると, による残余カット容量は で上から抑えられる. これの( を動かしたときの)和は高々 だから, 少なくとも一つの で による残余カット容量が高々 になる.
よって, 各頂点の容量が高々 であるとき, Dinic 法が であり, とすれば であることがわかった.
二部グラフ 上の二部マッチングを, 頂点 , と から , から への辺を加えたグラフ上で, 辺容量 として最大流問題に帰着する場合, へ入る辺, から出る辺の容量和はそれぞれ であり, へ入る辺, から出る辺の容量和は であるから, 上で とした場合の計算量 を得る.
例外がある場合
上の証明は, 辺容量や頂点容量が を超えたり, 多重辺がある場合でも, そのような頂点/辺が高々有限個であるときは, ちょっと変更すればオーダーとしては同じものが得られる. 定数個の, 条件を満たさない超頂点を加えた場合などに役に立つだろう.
GCD
容量に関しては, min と加減算程度しか使わないため, 容量の最大公約数 で各容量を割ってもアルゴリズムの挙動は変わらない. これを用いると, 例えば容量が とは限らない定数の場合でも, として上の結果を利用してよいことがわかる.
余談
上の証明を見ていればわかるように, 多くのフローが序盤の dual-primal step で流れる事が期待される. 簡単なインスタンス, 例えばランダムグラフは, 経験的に, この性質を強くもつものが多い. 12 一方で, 計算量には, パスが見つかった際に流すフローで, そのパスのどれだけの辺を消滅させられるかも関わってくる. こちらは, 上の容量が高々定数な場合のように, フロー空間が縮退しているインスタンスが簡単になる.
参考文献
-
: から出る辺全体の集合, : へ入る辺全体の集合. ↩
-
これが dual step と呼ばれているのを見たことはない. しかし, 線形計画問題として定式化したときの双対の 緩和を考え, 主の解を更新せずに双対の解を更新することで, なるべく小さい に対する -optimality を満たすようにする操作である. ↩
-
dual step に対応し, こちらは -optimality を保ちつつ主問題の目的関数値を増大させている. ↩
-
これは dual step で求めた DAG の上でブロッキングフローを求める操作である. ↩
-
今回は から出発するようにしたが, から始めてもよい. その場合さまざまなものが逆転する. ↩
-
は更新しなくても, 計算量は変わらない. ここでは, 計算量解析の簡単さを優先した. ↩
-
は辺のイテレータで, 各辺一度しか出てこない. ↩
-
に対し から を含む木の根へのパスに含まれる辺で, 最も根に近いもの. ↩
-
各頂点には値(具体的には の残余容量)が付与されている. に対し, この付与された値を返す. ↩
-
に対し から を含む木の根へのパスに含まれる頂点に付与された値に を足し込む. ↩
-
に対し から を含む木の根へのパスに含まれる頂点のうち, 付与された値が最も小さいものを返す. 複数あるならば, 最も根に近いものを返す. ↩
-
個人の感想です. ↩