题解: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

【核心思想】

  1. 问题分析:给定一个包含 \(N\) 个节点 \(M\) 条边的有向无环图(DAG),需要选出最多的节点作为藏身点,使得任意两个藏身点之间都没有路径相连(即互相不可见)。这等价于求最小路径覆盖问题:用最少的不相交路径覆盖所有节点,每条路径上只能选一个节点作为藏身点。

  2. 算法选择

    • Floyd-Warshall 传递闭包:预处理图中任意两点间的可达性,将原图转化为传递闭包矩阵
    • 二分图最大匹配(匈牙利算法):将 DAG 的最小路径覆盖问题转化为二分图最大匹配问题
    • Dilworth 定理:最小路径覆盖数 = 总节点数 - 最大匹配数
  3. 关键步骤

    • 建图:读入 \(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\),即最多可选的藏身点数量
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N^3 + N \cdot M_{match})\),Floyd-Warshall 为 \(O(N^3)\),匈牙利算法为 \(O(N \cdot E)\),其中 \(E\) 为二分图边数
    • 空间复杂度:\(O(N^2)\),传递闭包矩阵存储
  5. 最小路径覆盖与二分图匹配的核心思想

    • 最小路径覆盖:用最少的不相交路径覆盖 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
posted @ 2026-06-17 17:23  团爸讲算法  阅读(7)  评论(0)    收藏  举报