把「最少士兵走遍所有点且每点只走一次」建模为 DAG 最小路径点覆盖,拆点建二分图跑匈牙利算法,答案 = N - 最大匹配。
OJ: roj
题目 ID: 3221
难度:提高
标签:图论二分图匹配匈牙利算法DAG最小路径点覆盖
创建: 2026-10-02 02:46
更新: 2026-10-10 11:30
形式化题目
给定一张
- 路径之间两两没有公共点(每个交叉口只被一个士兵走过一次);
- 所有路径的点集并起来覆盖全部
个点。
求所需路径条数的最小值,这个量称为 DAG 的最小路径点覆盖。
注意路径的连续性指的是边上连续:士兵只能沿一条真实存在的道路
正解
思路
第一步:朴素做法为什么不可行
把每个点指派为“某条路径的起点”或“接在某个前驱后面”。最直接的枚举是:每个点从
瓶颈很清楚:链的起点集合和终点集合事先不知道,于是只能连链的划分一起枚举。
第二步:只看“接边”,不看链
换个角度数数。设一共用了
这个恒等式说明:只要数出接边数,路径条数就被确定了:
同时,一组接边要真的能拼成不相交的路径,必须满足:
- 每个点至多有一个后继(不能同时接两个点在自己后面);
- 每个点至多有一个前驱(不能被两个点同时接在后面)。
第三步:把“接边”翻译成二分图匹配
把每个点
- 左副本
:负责 的出边; - 右副本
:负责 的入边。
对原图的每条有向边
- “每个点至多一个后继”= 每个左副本最多出现在一条被选中的边里;
- “每个点至多一个前驱”= 每个右副本最多出现在一条被选中的边里。
这正是匹配的定义。所以“能接出的最多边数”就是这张二分图的最大匹配
还剩最后一个担心:匹配边可能连成一个环。比如
两个方向合起来:任意一组合法路径拆出接边就得到一个同大小的匹配,
而任意一个匹配又能拼回一组合法路径,所以最大接边数恰好等于最大匹配
第四步:样例 1 对照
样例 1 是
flowchart LR
N1((1)) --> N3((3))
N2((2)) --> N3
N3 --> N4((4))
图中
拆点建图后,
| 二分边(候选接边) | 含义 | 是否进入最大匹配 |
|---|---|---|
| 是 | ||
| 是 | ||
| 否, |
被选中的两条边恰好各占用一个不同的左副本(
把匹配边当作链内箭头,就还原出覆盖全部 4 个点的路径。
下表每行是一条路径:中间列出路径上的点,右列是该路径贡献的接边,
“点数
| 路径 | 点数 | 链内接边 |
|---|---|---|
| 3 | ||
| 1 | 无 |
接边总数
算法流程
- 读入
与 条有向边;对每条边 ,在 adj[u]里加入。 - 用匈牙利算法对
依次寻找增广路,得到最大匹配 。 - 输出
。
实现上按编号顺序为每个左副本增广一次即可:处理完前
代码
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
vector<int> adj[125]; // 左副本 u 喜欢的右副本
int match_r[125];
bool seen[125];
bool find_augment(int u) {
for (size_t i = 0; i < adj[u].size(); i++) {
int v = adj[u][i];
if (seen[v]) continue;
seen[v] = true;
if (match_r[v] == 0 || find_augment(match_r[v])) {
match_r[v] = u;
return true;
}
}
return false;
}
int max_matching(int n) {
for (int i = 0; i <= n; i++) match_r[i] = 0;
int matched = 0;
for (int u = 1; u <= n; u++) {
for (int i = 0; i <= n; i++) seen[i] = false;
if (find_augment(u)) matched++;
}
return matched;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) adj[i].clear();
for (int i = 0; i < m; i++) {
int x, y;
cin >> x >> y;
adj[x].push_back(y);
}
// 最小路径点覆盖 = 顶点数 - 最大匹配数
cout << n - max_matching(n) << "\n";
}
return 0;
}#!/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-02 02:46
# update_at: 2026-10-02 02:46
import sys
FREE = 0 # 右副本未匹配的哨兵;顶点编号从 1 开始,0 永远不会和真实顶点撞上
def find_augment(u: int, adj: list[list[int]], match_r: list[int], seen: list[bool]) -> bool:
"""从右副本一侧给左副本 u 找增广路:能腾出一个位置就返回 True。"""
for v in adj[u]:
if seen[v]:
continue
seen[v] = True # 本轮已试过 v,不再重复访问
if match_r[v] == FREE or find_augment(match_r[v], adj, match_r, seen):
match_r[v] = u # v 改配 u;原来占 v 的点已另找出路
return True
return False
def max_matching(n: int, adj: list[list[int]]) -> int:
"""匈牙利算法:依次为每个左副本找增广路,返回二分图最大匹配数。"""
match_r = [FREE] * (n + 1)
matched = 0
for u in range(1, n + 1):
if find_augment(u, adj, match_r, [False] * (n + 1)):
matched += 1
return matched
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
T = next(data)
for _ in range(T):
n, m = next(data), next(data)
# 拆点建图:每条有向边 u → v 对应一条二分边 u_L — v_R(左管出边,右管入边)
adj: list[list[int]] = [[] for _ in range(n + 1)]
for _ in range(m):
adj[next(data)].append(next(data))
# 最小路径点覆盖 = 顶点数 - 最大匹配数
out.append(str(n - max_matching(n, adj)))
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
设点数为
- 最大匹配:匈牙利算法,每个左副本增广一次,每次 DFS 最多扫过全部
条边, 时间 。本题 ,官方数据 ,实测最慢 0.019s。 - 空间:邻接表
,匹配数组与访问标记各 ,实测峰值约 14.7MB, 远低于 128MB 限制。
与标准 C++ 正解(拆点 + 匈牙利)同阶,没有降级为暴力。
总结
- 每个交叉口恰好走一次、士兵沿街道连续前进,等价于用点不相交的有向路径覆盖全部点, 即 DAG 最小路径点覆盖;
- 关键恒等式:
条路径的接边总数恒为 。于是最小化路径条数 ⇔ 最大化接边数, 问题从“枚举链的划分”变成“选尽量多的接边”; - 把点
拆成 ,边 变成二分边 , “每点至多一个前驱、至多一个后继”就是匹配的度数约束,所以最大接边数 = 最大匹配 ; - 原图无环保证匹配不会连成环,链分解一定成立;
- 答案
,用匈牙利算法在 内求出; - 与
roj/3206的区别:那题把“存在一条路径”也算连通,必须先做传递闭包; 本题士兵必须走直接边,直接用原图建二分图即可。
图示解析
整道题只有一条主线,三次等价改写:
题面:最少几个士兵,走遍所有点且每点只走一次
│ 士兵沿有向边连续前进,路径两两不相交
▼
DAG 最小路径点覆盖:用最少的点不相交有向路径覆盖全部点
│ k 条路径的接边总数恒为 N-k
│ ⇒ 最小化 k 等价于 最大化接边数 ν
▼
拆点二分图:边 u→v 变成 u_L→v_R
│ "每点至多一个前驱/后继" = 匹配的度数约束
▼
最大匹配 ν(匈牙利算法)⇒ 答案 = N - ν读图要点:每一步都是等价改写而不是近似,所以最终输出的