Day 135-Q3 — 最大重みクロージャ問題(Project Selection Problem)

2026-08-27 赤色 Master / Phase 8+ ★★★★★★★★☆ 依存関係=無限容量辺、最小カットで最大利益を求める

問題

$N$個のプロジェクトがあり、プロジェクト$i$を実行すると利益$w_i$(正または負)が得られる。$M$個の依存関係$(a_i,b_i)$が与えられ、それぞれ「$a_i$を選ぶなら$b_i$も必ず選ぶ」ことを意味する。

依存関係をすべて満たす部分集合$S\subseteq\{1,\dots,N\}$(空集合可)の中で、利益の総和$\sum_{i\in S}w_i$を最大化せよ。

入力形式

N M
w_1 w_2 ... w_N
a_1 b_1
...
a_M b_M

制約

$1 \le N \le 300$
$0 \le M \le 3\times10^4$
$-10^9 \le w_i \le 10^9$
$1 \le a_i,b_i \le N,\ a_i \ne b_i$

入出力例

入力例1

3 2
10 -5 -3
1 2
1 3

出力例1

2

説明: プロジェクト1を選ぶと2,3も選ばねばならず利益 $10-5-3=2$。選ばなければ利益0。よって最大は2。

概念図

S-i-T ネットワークと最小カット S 1 w=10 2 w=-5 3 w=-3 T cap=10 cap=∞ (依存1→2) cap=∞ (依存1→3) cap=5 cap=3 最小カット=8(w2,w3側を切る)→ 答え=10-8=2

ヒント(段階的開示)

ヒント1(方向性)

「$a$を選ぶなら$b$も選ぶ」という依存関係を持つ最大化問題は、最小カットに帰着できる古典パターン(プロジェクト選択問題/最大重みクロージャ問題)である。

ヒント2(アプローチ)

超頂点$S,T$を用意し、$w_i>0$なら$S\to i$に容量$w_i$、$w_i<0$なら$i\to T$に容量$-w_i$の辺を張る。依存関係$(a,b)$には$a\to b$に容量$\infty$の辺を張る。最小カットを$C$とすると答えは$\left(\sum_{w_i>0}w_i\right)-C$。

ヒント3(誘導)

最小カットで$S$側に残る頂点集合が選択すべきプロジェクト。$\infty$辺がカットされないことがちょうど依存関係の充足を保証する。

# S=0, T=N+1
# w[i] > 0: add_edge(S, i, w[i])
# w[i] < 0: add_edge(i, T, -w[i])
# 依存 (a, b): add_edge(a, b, INF)
# answer = sum(w[i] for w[i] > 0) - max_flow(S, T)

模範解答 (Python)

import sys
from collections import deque


class MaxFlow:
    def __init__(self, n):
        self.n = n
        self.graph = [[] for _ in range(n)]

    def add_edge(self, u, v, cap):
        self.graph[u].append([v, cap, len(self.graph[v])])
        self.graph[v].append([u, 0, len(self.graph[u]) - 1])

    def bfs(self, s, t):
        self.level = [-1] * self.n
        self.level[s] = 0
        q = deque([s])
        while q:
            u = q.popleft()
            for e in self.graph[u]:
                v, cap, _ = e
                if cap > 0 and self.level[v] < 0:
                    self.level[v] = self.level[u] + 1
                    q.append(v)
        return self.level[t] >= 0

    def dfs(self, u, t, f):
        if u == t:
            return f
        while self.it[u] < len(self.graph[u]):
            e = self.graph[u][self.it[u]]
            v, cap, rev = e
            if cap > 0 and self.level[u] < self.level[v]:
                d = self.dfs(v, t, min(f, cap))
                if d > 0:
                    e[1] -= d
                    self.graph[v][rev][1] += d
                    return d
            self.it[u] += 1
        return 0

    def max_flow(self, s, t):
        flow = 0
        while self.bfs(s, t):
            self.it = [0] * self.n
            while True:
                f = self.dfs(s, t, float('inf'))
                if f == 0:
                    break
                flow += f
        return flow


def main():
    data = sys.stdin.buffer.read().split()
    idx = 0
    N = int(data[idx]); idx += 1
    M = int(data[idx]); idx += 1
    w = [int(data[idx + i]) for i in range(N)]
    idx += N

    S = 0
    T = N + 1
    mf = MaxFlow(N + 2)
    INF = float('inf')

    total_positive = 0
    for i in range(N):
        v = i + 1
        if w[i] > 0:
            mf.add_edge(S, v, w[i])
            total_positive += w[i]
        elif w[i] < 0:
            mf.add_edge(v, T, -w[i])

    for _ in range(M):
        a = int(data[idx]); idx += 1
        b = int(data[idx]); idx += 1
        mf.add_edge(a, b, INF)

    cut = mf.max_flow(S, T)
    print(total_positive - cut)


main()

Step-by-Step 解説

1問題をカット問題に翻訳する
「$S$側=選ぶ」「$T$側=選ばない」と決め、正利益の機会損失、負利益のコストをそれぞれ辺の容量として表現する。
2正負での辺の張り方
$w_i>0$は$S\to i$に容量$w_i$(選ばないと損失)。$w_i<0$は$i\to T$に容量$-w_i$(選ぶとコスト)。
3依存関係を無限容量辺で表現
$a\to b$に$\infty$の辺を張れば、$a$が$S$側かつ$b$が$T$側になるカットのコストが無限大になり選ばれない。
4最小カット=最大流
最大流最小カット定理よりDinic法で$O(V^2E)$で計算できる。$N\le300,M\le3\times10^4$なら十分高速。
5答えの復元
正利益の総和からカットを引いたものが最大利益。$S$から到達可能な頂点が実際の選択集合になる。

よくあるミス

ミス原因正しい書き方
依存関係の辺の向きを逆にする「$a$なら$b$」を$b\to a$と誤解$a\to b$に$\infty$容量。逆向きだと制約が守られない
$w_i=0$の扱いを誤解辺なしで放置すればよいと考え頂点自体を削ってしまう辺は不要だが依存関係の中継点として頂点は必要
total_positiveの計算漏れ正利益合計を答えの基準に使い忘れる答え=正利益合計-最小カットを必ず両方計算する
∞容量をfloat('inf')にしてエラーフロー計算でinf同士の演算が発生し得る十分大きい有限値(例:10^18)を使うか演算を避ける実装にする
Dinic法のitをBFS毎にリセットし忘れる前回のイテレータ位置を使い回す各BFS成功後に必ずit=[0]*nで初期化する

次のステップ

  • 発展: 「少なくとも1つを選べば良い」OR条件を含む場合、単純な最大重みクロージャでは表現できない。補助頂点を追加した拡張モデルを設計せよ。
  • 発展2: $N$が大きい($10^5$規模)場合、ISAPやPush-Relabel法との計算量比較を実測せよ。
  • 次回予告: 次回のテーマは今後のセッションで選定

自己評価

自分の回答

気づき・メモ