AtCoder NoviStepsを埋めてみる(32) 燃やす埋める問題がおもしろすぎたの続きです。今回は最小費用流問題です。

最小費用流問題とは?

最小費用流問題とは、フローを流す量が与えられている場合、流すために掛かる総費用を最小にするためにはどのようにするとよいのかを決定する問題です。

ac-library-csharp というライブラリには McfGraph<T> クラスがあり、これを使えば簡単に最小費用流問題を解くことができます。ところがこのライブラリはコストに負の値を設定することができません(負のコストを設定すると例外が発生する)。なので、自分で実装することにします。

やることは、

始点から終点まで流せるパスでコストが最小であるものを取得する
そのパスに流せるだけ流す
辺の容量と逆辺の容量を更新する
与えられたフローに達するまで上記を繰り返す

です。

ただし注意しなければならないことがあります。単純に流せる量を記録しておくだけでは不十分です。以下の図のような場合を考えます。矢印があるところに書かれているのはコストではなく容量です。

これなら考えるまでもなく、答えは35です。0 → 1 → 3 と 20 流すことができて、そのあと 0 → 2 → 3 と 15 流すことができます。あわせて 35 が答えです。

ところが最初に 0 → 1 → 2 → 3 を選択してしまうと、このパスに 10 を流すことができるので、以下のように更新されることになります。

そのあと 0 → 1 → 3 と 10 流すことになります。すると以下のように更新されます。

さらに 0 → 2 → 3 と 5 流すことができるので流してみることにします。

これ以上流すことができないので 10 + 10 + 5 で 25 が答え・・・。あれ? おかしいですね。

逆辺とは?

このような問題を回避するために逆辺を定義します。逆辺とは逆向きの 容量 0 の辺のことです。

最初に 0 → 1 → 2 → 3 を選択した場合、以下のように更新されます。フローを流すと逆辺の容量は増えます。

次に 0 → 2 → 1 → 3 を選択した場合は、10 流すことができて以下のように更新されます。

次に 0 → 1 → 2 → 3 を選択した場合は、5 流すことができて以下のように更新されます。

次に 0 → 2 → 1 → 3 を選択した場合は、5 流すことができて以下のように更新されます。

次に 0 → 1 → 3 を選択した場合は、5 流すことができて以下のように更新されます。

これ以上は流すことはできません。全部足すと 10 + 10 + 5 + 5 + 5 = 35 となり正解と一致しています。

MinCostFlowNegative クラスの実装

負のコストも設定できるようにMinCostFlowNegative クラスを定義します。

まず内部の処理で辺の状態を管理するために InternalEdge クラスを定義します。

コンストラクタを示します。頂点数をフィールド変数に保存し、InternalEdge を格納するためのリストを初期化します。

辺を追加する処理を示します。辺オブジェクトを生成して互いの逆辺がわかるようにしています。

始点から終点にむけてフローを流すためにもっともコストが低いパスを探す処理を示します。戻り値は始点から終点まで移動するのに通らなければならない辺のリストと最小コストです。

s → t に最大で maxFlow 流したときの最小費用流を求める処理を示します。

ここでは

始点から終点まで流せるパスでコストが最小であるものを取得する
そのパスに流せるだけ流す
辺の容量と逆辺の容量を更新する
与えられたフローに達するまで上記を繰り返す

を実行しています。

戻り値は実際に流すことができた流量と最小費用です。

最小費用流を求めたあと、各辺にじっさいにどれだけ流すことができたのかを取得する処理を示します。

逆辺でない辺について、初期状態の容量と残容量を比較すればどれだけ流すことができたかわかるので、それを返すことにします。

F – 最小費用流

ためしに問題を解いてみることにします。

F – 最小費用流

問題の概要

V 個の街と E 本の水道管がある。
i 本目の水道管は街 u[i] から街 v[i] へ水を最大で c[i] だけ流すことができるが、水を 1 流すごとに費用が d[i] かかる。
街 1 から街 V へ水を F だけ流すとき、合計費用の最小値を求めよ。

そのまんまの最小費用流問題です。この問題には負のコストはありませんが、一応自作クラスでも普通に AC できます。

C – Moving Pieces

C – Moving Pieces

問題の概要

N 行 M 列からなる盤面がある。

マスの文字が ‘.’ であれば空マス、’#’ であれば障害物が置かれているマス、’o’ であれば駒が置かれているマスである。

駒を 1 つ選び 1 個下、もしくは 1 個右のマスに移動させる。
ただし他の駒もしくは障害物のあるマスに駒を移動させる操作はできない。
いうまでもなく駒が盤面を飛び出すような操作もできない。

できるだけ多くの回数操作をしたいと考えている。操作回数の最大値を求めよ。

この問題は工夫すれば非負数のコストを設定することで解くことができますが、テストも兼ねて負のコストを設定して解いてみることにします。

まず超頂点 S から ‘o’ があるマスへ容量 1、コスト 0 の辺を張ります。つぎに ‘#’ ではないすべてのマスから超頂点 T へ容量 1、コスト 0 の辺を張ります。そして ‘#’ ではないすべてのマスからその右側と下側にあるマスがそれぞれ ‘#’ でないならそれぞれ容量 ∞、コスト -1 の辺を張ります。これで最大流量を ‘o’ の個数にして S から T への最小費用流を求めると、最小費用の符号を反転させたものが最大の移動回数となります。