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

2026-05-12 赤色 Master / Phase 8+ ★★★★★★★★★ 最大流・最小カット

問題

$N$ 個のプロジェクトと $M$ 個の依存関係 (i,j)(i を選ぶなら j も選ぶ)がある。選ぶ集合がクロージャを満たすとき利益合計を最大化せよ。

制約

$1 \le N \le 500$
$0 \le M \le 5000$
$-10^6 \le w_i \le 10^6$
$i \ne j$

入出力例

入力例 1

4 3
5 -3 2 -2
1 2
1 3
3 4

出力例 1

4

ヒント (段階的開示)

ヒント1: 方向性
最大重みクロージャ問題 = 最小カット(最大流)への帰着。
ヒント2: アプローチ
正の重み: $S \to v$ 容量 $w_v$。負の重み: $v \to T$ 容量 $|w_v|$。依存辺 $(i,j)$: $i \to j$ 容量 $\infty$。最大利益 = 正の和 − 最大流。
ヒント3: Dinic
Dinic 法で $O(V^2 E)$、頂点数 $N+2$、辺数 $M+N$。

模範解答 (Python)

import sys
from collections import deque
input = sys.stdin.readline

class Dinic:
    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:
            v = q.popleft()
            for to, cap, _ in self.graph[v]:
                if cap > 0 and self.level[to] < 0:
                    self.level[to] = self.level[v] + 1
                    q.append(to)
        return self.level[t] >= 0
    def _dfs(self, v, t, f):
        if v == t: return f
        while self._iter[v] < len(self.graph[v]):
            to, cap, rev = self.graph[v][self._iter[v]]
            if cap > 0 and self.level[v] < self.level[to]:
                d = self._dfs(to, t, min(f, cap))
                if d > 0:
                    self.graph[v][self._iter[v]][1] -= d
                    self.graph[to][rev][1] += d
                    return d
            self._iter[v] += 1
        return 0
    def max_flow(self, s, t):
        flow = 0
        INF = float('inf')
        while self._bfs(s, t):
            self._iter = [0] * self.n
            while True:
                f = self._dfs(s, t, INF)
                if f == 0: break
                flow += f
        return flow

def solve():
    N, M = map(int, input().split())
    W = list(map(int, input().split()))
    S, T = N, N + 1
    dn = Dinic(N + 2)
    INF = 10**18
    pos_sum = 0
    for i in range(N):
        if W[i] > 0:
            dn.add_edge(S, i, W[i])
            pos_sum += W[i]
        elif W[i] < 0:
            dn.add_edge(i, T, -W[i])
    for _ in range(M):
        u, v = map(int, input().split())
        dn.add_edge(u-1, v-1, INF)
    print(pos_sum - dn.max_flow(S, T))

solve()

Step-by-Step 解説

1クロージャの理解
$i$ を選ぶなら $j$ も選ぶ。閉集合制約。
2最大流への帰着
$S$-$T$ カットを「選ぶ/選ばない」境界として解釈。依存辺 $\infty$ で切断不能。
3Dinic 法
BFS でレベルグラフ、DFS でブロッキングフロー。
4選択頂点の特定
最大流後の BFS でソース側到達可能頂点が選ばれたプロジェクト。

よくあるミス

ミス原因正しい書き方
INF が小さい依存辺が切れる$\infty = 10^{18}$
辺方向の誤り逆向きに張る依存 $(i,j)$ は $i \to j$
符号誤り負の重みの方向add_edge(v, T, -W[v])
$W=0$ を含めるpos_sum の集計if W[i] > 0: で限定

次のステップ

  • QPBO / Generalized Project Selection

自己評価

自分の回答

気づき・メモ