题解:AcWing 379 捉迷藏
【题目来源】
AcWing:379. 捉迷藏 - AcWing题库
【题目描述】
Vani 和 cl2 在一片树林里捉迷藏。
这片树林里有 \(N\) 座房子,\(M\) 条有向道路,组成了一张有向无环图。
树林里的树非常茂密,足以遮挡视线,但是沿着道路望去,却是视野开阔。
如果从房子 \(A\) 沿着路走下去能够到达 \(B\),那么在 \(A\) 和 \(B\) 里的人是能够相互望见的。
现在 cl2 要在这 \(N\) 座房子里选择 \(K\) 座作为藏身点,同时 Vani 也专挑 cl2 作为藏身点的房子进去寻找,为了避免被 Vani 看见,cl2 要求这 \(K\) 个藏身点的任意两个之间都没有路径相连。
为了让 Vani 更难找到自己,cl2 想知道最多能选出多少个藏身点。
【输入】
输入数据的第一行是两个整数 \(N\) 和 \(M\)。
接下来 \(M\) 行,每行两个整数 \(x,y\),表示一条从 \(x\) 到 \(y\) 的有向道路。
【输出】
输出一个整数,表示最多能选取的藏身点个数。
【输入样例】
7 5
1 2
3 2
2 4
4 5
4 6
【输出样例】
3
【核心思想】
-
问题分析:给定一个包含 \(N\) 个节点 \(M\) 条边的有向无环图(DAG),需要选出最多的节点作为藏身点,使得任意两个藏身点之间都没有路径相连(即互相不可见)。这等价于求最小路径覆盖问题:用最少的不相交路径覆盖所有节点,每条路径上只能选一个节点作为藏身点。
-
算法选择:
- Floyd-Warshall 传递闭包:预处理图中任意两点间的可达性,将原图转化为传递闭包矩阵
- 二分图最大匹配(匈牙利算法):将 DAG 的最小路径覆盖问题转化为二分图最大匹配问题
- Dilworth 定理:最小路径覆盖数 = 总节点数 - 最大匹配数
-
关键步骤:
- 建图:读入 \(N\) 个节点和 \(M\) 条有向边,构建邻接矩阵 \(d[i][j]\) 表示 \(i\) 能否直接到达 \(j\)
- Floyd-Warshall 求传递闭包:
- 三重循环遍历 \(k, i, j\)
- 更新 \(d[i][j] = d[i][j] \lor (d[i][k] \land d[k][j])\)
- 最终 \(d[i][j]\) 表示 \(i\) 是否可以通过任意路径到达 \(j\)
- 构建二分图:
- 将每个节点拆分为左部和右部两个副本
- 若 \(d[i][j]\) 为真(\(i\) 可达 \(j\)),则在左部 \(i\) 和右部 \(j\) 之间连边
- 匈牙利算法求最大匹配:
- 遍历每个左部节点 \(i\),尝试寻找增广路
- 使用 \(st[]\) 数组标记右部节点是否在本轮被访问过
- 若找到匹配则匹配数 \(res\) 加 \(1\)
- 计算答案:最小路径覆盖数 = \(N - res\),即最多可选的藏身点数量
-
时间/空间复杂度:
- 时间复杂度:\(O(N^3 + N \cdot M_{match})\),Floyd-Warshall 为 \(O(N^3)\),匈牙利算法为 \(O(N \cdot E)\),其中 \(E\) 为二分图边数
- 空间复杂度:\(O(N^2)\),传递闭包矩阵存储
-
最小路径覆盖与二分图匹配的核心思想:
- 最小路径覆盖:用最少的不相交路径覆盖 DAG 中所有节点,路径之间不能有公共节点
- Dilworth 定理:在 DAG 中,最小路径覆盖数等于总节点数减去对应二分图的最大匹配数
- 二分图构建技巧:将每个节点拆分为左右两份,原图的可达关系转化为二分图的边
- 匈牙利算法:通过寻找增广路不断扩大匹配,每次增广使匹配数加 \(1\)
- 传递闭包:Floyd-Warshall 算法预处理可达性,将间接可达转化为直接边
- 适用于 DAG 上的路径覆盖、偏序集链划分、任务调度类问题
【算法标签】
二分图
【代码详解】
#include <bits/stdc++.h>
using namespace std;
// ================= 常量与全局变量 =================
const int N = 205; // 最大节点数
const int M = 30005; // 最大边数(未使用,保留原样)
int n; // 节点数量(左右部节点数相同)
int m; // 边的数量
bool d[N][N]; // 传递闭包矩阵:d[i][j] 表示 i 能否到达 j(有向边或间接可达)
bool st[N]; // 标记数组:记录右部顶点在当前轮匈牙利算法中是否被访问过
int match[N]; // 匹配数组:match[j] 记录右部节点 j 当前匹配的左部节点编号
// ================= 匈牙利算法:尝试为左部节点 x 寻找增广路 =================
// 返回值:是否成功为节点 x 找到匹配
bool find(int x)
{
// 遍历节点 x 的所有可能右部匹配对象
for (int i = 1; i <= n; i++)
{
// 如果 x 可以到达 i,且 i 在本轮搜索中还未被考虑
if (d[x][i] && !st[i])
{
st[i] = true; // 标记节点 i 已被本轮考虑
// 情况1:节点 i 还未被匹配
// 情况2:节点 i 已被匹配,但可以尝试为它的原匹配对象 match[i] 寻找新的匹配
if (match[i] == 0 || find(match[i]))
{
match[i] = x; // 将节点 i 匹配给节点 x
return true; // 成功找到匹配
}
}
}
// 尝试了所有邻接点都失败,返回 false
return false;
}
// ================= 主函数 =================
int main()
{
// 读取节点数和边数
cin >> n >> m;
// 读入 m 条有向边
while (m--)
{
int a, b;
cin >> a >> b;
d[a][b] = true; // 标记 a 可以直接到达 b
}
// Floyd-Warshall 算法求传递闭包
// d[i][j] 最终表示 i 是否能通过任意路径到达 j
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
d[i][j] |= d[i][k] & d[k][j];
// ========= 匈牙利算法求最大匹配 =========
int res = 0; // 记录最大匹配数
// 为每个左部节点尝试寻找匹配
for (int i = 1; i <= n; i++)
{
memset(st, false, sizeof(st)); // 每轮搜索前重置标记数组
if (find(i)) // 如果成功找到匹配
res++; // 匹配数加 1
}
// 输出结果:最小路径覆盖数 = 总节点数 - 最大匹配数
cout << n - res << endl;
return 0;
}
【运行结果】
7 5
1 2
3 2
2 4
4 5
4 6
3
浙公网安备 33010602011771号