拆点成容量 1 的点弧后用最大流求最小点割大小,再按编号升序逐点重跑最大流判定可行性,得到字典序最小的坏机集合。
OJ: roj
题目 ID: 2090
难度:提高+/省选-
标签:网络流最小割
创建: 2026-10-01 08:23
更新: 2026-10-07 13:50
形式化题目
给定无向图
坏的对象是电脑(点):一台电脑坏了,与它相连的所有连接随之失效;连接(边)本身不会坏。因此本题求的是点意义下的
样例网络 1 与 3。
正解
思路
朴素想法与两个瓶颈。 枚举点集子集逐一验证断开与否是
- 点 vs 边:怎么让"坏一台电脑"变成"切断一条容量 1 的弧"?
- 字典序:一次最大流只给出"最小"的割,怎么拿到字典序最小的那一组?
瓶颈一:拆点。 把每台电脑
graph LR
v1in["1 入"] =="容量 INF"==> v1out["1 出"]
v1out =="容量 INF"==> v3in["3 入"]
v3in -- "容量 1:坏电脑 3 就切这里" --> v3out["3 出"]
v3out =="容量 INF"==> v2in["2 入"]
v2in =="容量 INF"==> v2out["2 出"]
style v3in fill:#f96,stroke:#333
style v3out fill:#f96,stroke:#333
应观察到:整张网络里唯一容量有限的弧是
瓶颈二:字典序。 跑完一次最大流,取"残量网络中源点可达、出点不可达"的点,得到的只是一组最小割——但它离源最近,不保证字典序最小。反例是一条链:
1 —— 4 —— 3 —— 2 c1 = 1, c2 = 2唯一增广路 6 7 9,"一次最大流 + 可达割"却给出 9 17 19。
正解:升序逐点判定。 字典序最小即升序列表逐位最小,于是先求出最小点割大小 cut),还差的点数 flow == rest:成立则收下
| 试探点 |
已选集 cut) |
重算 flow |
判定 | ||
|---|---|---|---|---|---|
| 3 | 0 | 断开 | 0 | ||
| 4 | 断开 | 0 |
表中 flow = 0 = rest,收下 3,输出 1 与 3。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 08:30
# update_at: 2026-10-01 09:46
import sys
from collections import deque
INF = 1 << 30 # 连接与起止电脑的容量:线路不会坏,c1/c2 不许坏
def build_net(n: int, adj: list[list[int]], broken: set[int], safe: set[int]) -> list[list[list[int]]]:
"""拆点建流网络:broken 里的电脑容量 0(当作已坏),safe 里容量 INF(不许坏),其余容量 1。"""
net: list[list[list[int]]] = [[] for _ in range(2 * n + 2)] # 点 v 的入点 2v、出点 2v+1
def add_arc(u: int, v: int, cap: int) -> None:
"""加一对正反弧:反向弧初始容量 0,供增广路回退流量。"""
net[u].append([v, cap, len(net[v])])
net[v].append([u, 0, len(net[u]) - 1])
for v in range(1, n + 1):
# 点内部弧 v入->v出:容量 1 表示"拆掉这台电脑"的代价
cap = 0 if v in broken else INF if v in safe else 1
add_arc(2 * v, 2 * v + 1, cap)
for u in range(1, n + 1):
for v in adj[u]:
if u < v: # 每条无向连接拆成两个方向,容量 INF:坏的是电脑不是线
add_arc(2 * u + 1, 2 * v, INF)
add_arc(2 * v + 1, 2 * u, INF)
return net
def max_flow(net: list[list[list[int]]], s: int, t: int) -> int:
"""Dinic 求 s 到 t 的最大流;按最大流最小割定理,它等于拆点网络的最小点割容量。"""
flow = 0
while True:
level = [-1] * len(net) # 分层图:只保留能层层推向 t 的弧
level[s] = 0
queue = deque([s])
while queue:
u = queue.popleft()
for v, cap, _ in net[u]:
if cap and level[v] < 0:
level[v] = level[u] + 1
queue.append(v)
if level[t] < 0:
return flow
cur = [0] * len(net)
def dfs(u: int) -> int:
"""在分层图上从 u 尽量往 t 推流,返回本条增广路推出的量。"""
if u == t:
return 1
while cur[u] < len(net[u]):
v, cap, rev = net[u][cur[u]]
if cap and level[v] == level[u] + 1:
pushed = dfs(v)
if pushed:
net[u][cur[u]][1] -= pushed
net[v][rev][1] += pushed
return pushed
cur[u] += 1
return 0
while (pushed := dfs(s)): # 一层内榨干所有增广路再重新分层
flow += pushed
def lex_min_cut(n: int, adj: list[list[int]], c1: int, c2: int) -> list[int]:
"""字典序最小的最小点割:先求最小割大小 k,再按编号从小到大逐点判定能否入选。"""
safe: set[int] = {c1, c2} # 起止电脑永远不许坏
k = max_flow(build_net(n, adj, set(), safe), 2 * c1, 2 * c2)
cut: list[int] = []
for v in range(1, n + 1):
if v in safe:
continue # 起止点与已排除点不再试探
# 试探 v 入选:已选点当作已坏,已排除点设为不可坏;
# 若剩余网络的最小割恰好 = 还差的点数 k-|cut|-1,则存在"含 v"的最小割
rest = k - len(cut) - 1
flow = max_flow(build_net(n, adj, set(cut) | {v}, safe), 2 * c1, 2 * c2)
if flow == rest: # 必有 flow >= rest,取等即说明含 v 的最小割存在
cut.append(v) # 收下 v,让答案这一位尽可能小
else:
safe.add(v) # 含 v 达不到最小规模:任何最小割都不含 v
return cut
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m, c1, c2 = next(data), next(data), next(data), next(data)
adj: list[list[int]] = [[] for _ in range(n + 1)]
for _ in range(m):
a, b = next(data), next(data)
adj[a].append(b) # 连接双向
adj[b].append(a)
cut = lex_min_cut(n, adj, c1, c2)
print(len(cut))
print(' '.join(map(str, cut)))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 12:40
* update_at: 2026-10-06 12:40
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 105; // 电脑数上限
const int MAXV = 2 * MAXN + 5; // 拆点后的点数上限
const int MAXM = 605; // 连接数上限
const int MAXE = 4 * MAXM + 2 * MAXN + 10; // 每次建网的弧数上界(含反向弧)
const ll INF = 1e9;
ll n, m, c1, c2;
ll ea[MAXM], eb[MAXM]; // 每条无向连接的两端电脑
ll cut_id[MAXN], cut_cnt; // 最小点割:已选中的坏电脑编号与个数
// 拆点网络:点 v 的入点是 2v,出点是 2v+1;链式前向星存弧
int head[MAXV], nxt[MAXE], to[MAXE];
ll cap[MAXE];
int e_cnt = 1; // 弧计数从 1 开始、成对预增:弧对是 (2,3),(4,5)...,i^1 恰好是 i 的反向弧
ll level[MAXV], cur[MAXV]; // Dinic 的分层图与当前弧
ll s, t; // 源 = c1 的入点,汇 = c2 的入点
bool safe[MAXN]; // safe[v]=1:v 不许坏(起止点或已排除点)
bool broken[MAXN]; // broken[v]=1:v 当作已坏(点弧容量 0)
// 加一对正反弧:反向弧初始容量 0,供增广路回退流量
void add_arc(int u, int v, ll c) {
++e_cnt; // 正弧取偶数编号,i^1 才是反向弧
to[e_cnt] = v; cap[e_cnt] = c; nxt[e_cnt] = head[u]; head[u] = e_cnt;
e_cnt++;
to[e_cnt] = u; cap[e_cnt] = 0; nxt[e_cnt] = head[v]; head[v] = e_cnt;
}
// 建网:坏点容量 0,不许坏点容量 INF,其余点容量 1;连接是双向 INF 线路
void build_net() {
memset(head, 0, sizeof(head));
e_cnt = 1; // 弧对从 (2,3) 开始,保证 i^1 配对成立
for (ll v = 1; v <= n; v++) {
ll c = 1; // 点内部弧 v入->v出:坏掉 v 的代价
if (broken[v]) c = 0;
else if (safe[v]) c = INF; // c1、c2 的点弧是 INF,永远不会被切
add_arc(2 * v, 2 * v + 1, c);
}
for (ll i = 1; i <= m; i++) {
// 坏的是电脑不是线:每条连接拆成两个方向,容量 INF
add_arc(2 * ea[i] + 1, 2 * eb[i], INF);
add_arc(2 * eb[i] + 1, 2 * ea[i], INF);
}
}
// BFS 分层:残量网络里从 s 出发能否层层推进到 t
bool bfs() {
memset(level, -1, sizeof(level));
queue<ll> q;
level[s] = 0;
q.push(s);
while (!q.empty()) {
ll u = q.front(); q.pop();
for (int i = head[u]; i; i = nxt[i]) {
ll v = to[i];
if (cap[i] > 0 && level[v] == -1) {
level[v] = level[u] + 1;
q.push(v);
}
}
}
return level[t] != -1;
}
// 在分层图上从 u 尽量往 t 推流,返回这条路径推出的流量
ll dfs(ll u, ll pushed) {
if (u == t) return pushed;
for (ll &i = cur[u]; i; i = nxt[i]) {
ll v = to[i];
if (cap[i] > 0 && level[v] == level[u] + 1) {
ll got = dfs(v, min(pushed, cap[i]));
if (got > 0) {
cap[i] -= got;
cap[i ^ 1] += got;
return got;
}
}
}
return 0;
}
// Dinic 最大流:按最大流最小割定理,它等于拆点网络的最小点割容量
ll dinic() {
ll flow = 0;
while (bfs()) {
for (ll v = 0; v < 2 * n + 2; v++) cur[v] = head[v];
while (ll got = dfs(s, INF)) flow += got;
}
return flow;
}
int main() {
cin >> n >> m >> c1 >> c2;
for (ll i = 1; i <= m; i++) cin >> ea[i] >> eb[i];
s = 2 * c1;
t = 2 * c2;
safe[c1] = safe[c2] = true; // 起止电脑永远不许坏
build_net();
ll k = dinic(); // 先求最小点割的大小 k
// 按编号从小到大逐点判定,保证答案字典序最小
for (ll v = 1; v <= n; v++) {
if (safe[v]) continue; // 起止点与已排除点不再试探
ll rest = k - cut_cnt - 1; // 若选 v,最小割里还差的点数
broken[v] = true; // 把 v 当作已坏重跑最大流
build_net();
// 恒有 flow >= rest;取等 <=> 存在含 v 的最小割
if (dinic() == rest) {
cut_cnt++;
cut_id[cut_cnt] = v; // 收下 v,让答案这一位尽可能小
} else {
broken[v] = false;
safe[v] = true; // 任何最小割都不含 v,标记为不许坏
}
}
cout << cut_cnt << "\n";
for (ll i = 1; i <= cut_cnt; i++) {
cout << cut_id[i] << (i < cut_cnt ? " " : "");
}
cout << "\n";
return 0;
}复杂度
- 时间复杂度:建网
;单次 Dinic 最坏 ( , ,实际因点弧容量仅 、流量 远小于最坏界);共 次最大流(求 一次 + 每点判定一次),合计 量级。这是字典序要求本身的代价(逐位可行性判定),不是枚举点集的暴力。 下实测 11 组数据单点最长 s,对 ms 很宽裕。 - 空间复杂度:
,每次建网约 条弧,峰值约 MB,远低于 MB 限制。
总结
坏的是点不是边,先拆点:点弧容量 flow == rest 判定"含该点的最小割是否存在”,成立就收下。不变量保证最终已选集恰为一组最小点割且逐位最小。实测数据仓 11 组全部一致(PASS=11),最长单点