🏖️

燃やす埋める問題(最大フロー最小カット問題)について整理する [理解編]

2022/07/11に公開

はじめに

今回は、燃やす埋める問題と呼ばれるタイプの問題(Project Selection Problemと呼ばれることもある)を考えていきます。グラフ理論(離散数学)の中の最大フロー最小カットという分野が関連していますので、そのあたりも一緒に整理していきます。問題はAtCoderの過去問題からピックアップして解いていく予定です。

最小カットとは

s, t を端点とする有向グラフがあり、それぞれの辺に容量が設定されている場合を考えます。

ここで、s, t 以外の頂点を sのグループ、tのグループに分けたとき、sを含む領域からtを含む領域に流れる有向辺の容量和の最小値を「最小カット」と言います。下図では 5+3+4=12 が最小カットです。

この最小カットは、s から t へ各辺の容量以内で流すことのできるフローの最大値(最大フロー)と一致することが分かっています(最大フロー最小カットの定理と言う)。

もっと詳しく知りたい人は、下記のサイトがとても良いです。「最小カット = 最大フロー」 となる理由についても簡単に記載されています。

https://www.momoyama-usagi.com/entry/math-risan15#i-6

燃やす埋める問題とは

A, B, C の3つのもの(ゴミ)を「燃やす」か「埋める」必要がある。
以下の条件下で受け取れる報酬の最大値を求めなさい。

  • A, B, C を 「燃やす」 とそれぞれ 50円、100円、130円の報酬が受け取れる。
  • A, B, C を 「埋める」 と 90円の報酬が受け取れる。
  • B, C を 両方「燃やす」ことは禁止されている(選択肢による依存関係がある)。

<受け取れる報酬>

もの A B C
燃やす 50円 100円 130円
埋める 90円 90円 90円

直感的に、A=埋める、B=埋める、C=燃やす で310円が報酬の最大値になるのは分かりますね。この問題のように、複数の小問題に対して選択肢とコスト・報酬が与えられ、選択肢同士に依存関係があるような問題は、グラフに置き換えて、最大フロー・最小カットを考えると良いです。まずは、選択肢間の依存関係を除いてグラフを考えてみます。カットされる辺が各小問題に対する選択となります。

最小カットは、各辺のコスト最小値を求める考え方になりますので、各選択肢の最大値を基準(無条件でその報酬が受け取れると仮定)にして、各選択肢にコストを設定します。

選択肢間の依存関係については、新しくコスト無限大の辺を追加して調整します。B=燃やす、C=燃やす をカットした場合に、コスト無限大の辺をカットしないと、s から t への流れがカットできない(s と t を別グループに分けられない)ようにします。上手くいくように B に出入りする辺の順番を変更しています。

真ん中の無限大の辺があるため、B=燃やす、C=燃やす をカットするだけでは、s から t への流れがカットできません。

最小カットは、{s, A} と {B, C, t} というグループに分けたときですので、無条件で得られる320円から、A=埋める(コスト=0)B=埋める(コスト=10)C=燃やす(コスト=0) として、合計コストの10円を引いた 310円 が最大値となります。

最小カットは最大フローと等しいので、最大フローを求めることができれば最小カットも求めることができます。これで燃やす埋める問題を、最大フローを求める問題に置き換えることができました。

下記のサイトも参考になります。

https://www.slideshare.net/shindannin/project-selection-problem

最大フローを求めるアルゴリズム

最大フローを求める方法としては、

  • Fold-Fullkerson法
  • Dinic法

が知られています。Dinic法については理解しきれていないので、今回はFold-Fullkerson法を使っていこうと思います。

Ford-Fullkerson(フォード・ファルカーソン)法

アルゴリズムの流れとしては、次の通りです。

  1. BFS(幅優先探索)で、s(source) から t(sink) へ辿り着くことができるパスを探します。s(source)に近い順に level を記録しておきます。
  2. 先ほど決定したlevel順に経路をもつパスについて、DFS(深さ優先探索)を行い、フローを流して、残余ネットワークを更新していきます。上階層から下階層へはこれまでの残余ネットワークの最小値を渡していき、一番下の階層から戻ってくるときに、残余ネットワークの更新を行っています。
  3. 1に戻って、s(source) から t(sink)へ辿り着けるパスを探します。残余ネットワークが更新されているので、t(sink)に辿り着けるパスがもうないかもしれません。仮にパスがなければ終了となります。

残余ネットワークって何? となってしまう人は、上で紹介したサイトで、最大フロー・最小カットについて確認することをオススメします。今回は試しに以下のようなネットワークで、最大フローを計算しようと思います。青字は逆向きのフロー(一度流したフローを戻すという選択肢のために準備します)を表しています。

まず始めにBFS(幅優先探索)で、各頂点の level が決まります。

頂点 0 1 2 3 4
level 0 1 1 2 2

level の序列がある流れ(level 小→大 へ流れる)を考えていきますので、0→2→4 という流れを考えます。深い方から残余ネットワークを更新することになります。DFS(深さ優先探索)を行う際に、上階層からは残余ネットワークの最小値を渡していきますので、0→2 の残余 3 を反映してフローは 3 だけ流れることになります。

もう一度、BFS(幅優先探索)で、各頂点の level を調べます。

頂点 0 1 2 3 4
level 0 1 2 2 3

今度は level が変わりました。これは、0→2 への残余ネットワークが 0 になったため、頂点2へは頂点1を介していく必要があります。

一度、頂点0まで戻ってきましたが、0→1→2→4 のパスだけでなく、0→1→3→4 のパスも、level の序列があるパスです。

最後に BFS(幅優先探索)で、各頂点の level を調べますが、t (sink) である頂点4へのパスが存在しません。つまり level が -1 のままの場合には、到達不可能と考えこれ以上の更新はできません。

Pythonでの実装

from  collections import deque
N = 3; INF = 10**18

# 頂点間ネットワークの初期設定です。
table = [[0,8,3,0,0],[0,0,3,4,0],[0,0,0,0,5],[0,0,0,0,3],[0,0,0,0,0]]

BFS(幅優先探索)で各頂点へ移動可能かどうか、何番目に到達する頂点かを調べます。

def BFS(v):
    level[v] = 0
    q=deque()
    q.append(v)

    while q:
        v = q.popleft()
        for u in range(N+2):
            if level[u] < 0 and table[v][u] > 0:
                level[u] = level[v] + 1
                q.append(u)

DFS(深さ優先探索)では、下階層にフローの大きさを渡していきます。return で返ってくるのは、下位でのフローの最小値となっています。

def DFS(fr,flw):
    if fr == N+1:
        return flw
    
    for to in range(N+2):
        if table[fr][to] > 0 and level[fr] < level[to]:
            Dflw = DFS(to,min(flw,table[fr][to]))
            if Dflw > 0:
                table[fr][to] -= Dflw
                table[to][fr] += Dflw
                return Dflw
    return 0

MaxFlwの初期値を 0 とおいて、まずは level を初期化して、BFS でパスを探します。頂点 4 へのパスが存在するならば、DFS(深さ優先探索)を行い、MaxFlwの値に df=DFS() の計算値を加算します。

MaxFlw = 0
while True:
    level = [-1]*(N+2)
    BFS(0)
    if level[N+1] < 0 : break

    while True:
        df = DFS(0,INF)
        if df > 0:
            MaxFlw += df
        else:
            break
print(MaxFlw)

Dinic(ディニッツ)法

今回用いた Ford-Fullkerson法よりも無駄が少ないようですが、理解できていないので、また別の機会にしたいと思います。

問題演習へ続く

複数の小問題に対して選択肢とコスト・報酬が与えられる問題は、グラフを考えると最小カット・最大フロー問題へ置き換えることができます。Ford-Fullkerson法や、Dinic法を用いれば最大フローを求められるので、問題を解くことができます。長くなってしまったので、問題演習は別記事にします。以下の問題を考えていく予定です。

  • ARC085-E問題 MUL
  • ABC193-F問題 Zebraness
  • 典型90問-040 Get More Money

https://zenn.dev/kiwamachan/articles/3dd867719729d6

Discussion