GESP2026年9月认证C++七级( 第三部分编程题(1、必经之路))精讲


一、先认识这道题:什么叫"必经点"?

题目给我们一张有向图:

  • 有 n 个结点;

  • 有 m 条有向边;

  • 每条边都有方向,比如:

    1 → 3

表示只能从1走到3,不能反过来从3走到1。

题目规定:

  • 入度为0的点:可以作为合法起点;

  • 出度为0的点:可以作为合法终点。

如果:

从所有可能的合法起点出发,到所有可能的合法终点,无论怎么走,都必须经过某个点 u,

那么这个点 u 就叫做:

必经点

题目要求我们把所有必经点找出来。


二、把它想象成"王国逃生游戏"

假设有一个王国:

复制代码
城堡A → 森林 → 山洞 → 城门

国王规定:

所有冒险者都必须从"入口"进入,最后从"出口"离开。

如果所有路线都必须经过一座桥:

复制代码
       ┌───────┐
       ↓       ↓
入口 → 森林 → 【桥】 → 城门
       ↑       ↑
       └───────┘

那么这座桥所在的位置就是:

必经点!

因为不管你选择哪条路线:

都绕不过它。


三、先看题目的样例

样例1:

复制代码
8 9
1 3
2 3
3 4
4 5
5 6
6 7
6 8
2 4
5 7

我们把它画得简单一些:

复制代码
        1 ──→ 3 ──→ 4 ──→ 5 ──→ 6 ──→ 7
              ↑     ↑      │    └───→ 8
              │     │      │    
        2─────┘─────│      └──→ 7

实际上,题目给出的合法起点是:

复制代码
1、2

合法终点是:

复制代码
7、8

题目把所有可能路径列出来以后发现:

4和5一定会经过。

所以答案是:

复制代码
2
4 5

也就是:

这就是样例1的输出。


四、这道题最关键的问题

小朋友看到题目后,很容易产生一个想法:

"我把所有路径都找出来,然后看看哪些点出现次数最多,不就可以了吗?"

不可以!

因为图里面可能有非常多条路径,甚至有环。

而且题目真正问的是:

这个点是不是所有合法起点 → 合法终点的路径都必须经过?

所以我们需要换一个思路。


五、换个角度:如果这个点"消失"了呢?

这是整道题最重要的思维!

假设我们怀疑:

复制代码
4

是必经点。

那么我们可以做一个非常聪明的实验:

把4号点暂时关闭,不允许经过它。

然后问:

还有没有办法从某个合法起点走到某个合法终点?


情况1:还能走出去

例如:

复制代码
起点 → 3 → 5 → 终点

完全没有经过4。

那么说明:

4并不是必经点。

因为我们找到了一个"不经过4"的路线。


情况2:完全走不到终点

如果把4关闭之后:

复制代码
起点 → 3 → ❌

所有道路都被堵死了。

那么说明:

所有原来的合法起点到合法终点的路线,都必须经过4。

所以:

4是必经点


六、于是问题就变得非常简单了

我们可以:

一个点一个点地进行"拆除实验"。

例如有8个点:

复制代码
尝试删除1 → 看还能不能从起点到终点
尝试删除2 → 看还能不能从起点到终点
尝试删除3 → 看还能不能从起点到终点
......
尝试删除8 → 看还能不能从起点到终点

如果:

删除这个点以后,再也无法从任何合法起点到达任何合法终点。

那么:

复制代码
这个点 = 必经点

是不是一下子就清楚了?


七、但是还有一个小问题

我们怎么判断:

"删除一个点之后,还有没有起点能够到达终点?"

这时候就轮到我们之前学过的超级英雄:

DFS!

当然,BFS也可以。

因为我们只是想知道:

能不能走到?

而不是求最短距离。

所以:

  • DFS可以;

  • BFS也可以。

这里为了让初学者更容易理解,我们使用 DFS。


八、DFS在这里干什么?

假设我们暂时关闭4号点:

复制代码
4号点不能进入!

那么DFS就从所有合法起点开始探索。

例如:

复制代码
合法起点:
1
2

我们分别从1、2出发。

如果DFS最终能够到达:

复制代码
7或者8

说明还有路线。

那么:

复制代码
4不是必经点

如果所有DFS都到不了7和8:

复制代码
4就是必经点

九、如何找到合法起点和合法终点?

这一步特别重要。

题目告诉我们:

入度为0的结点是合法起点。

什么叫入度?

就是:

有多少条箭头指向这个点。

例如:

复制代码
1 → 3
2 → 3

那么:

复制代码
1:入度0
2:入度0
3:入度2

所以:

复制代码
1、2

可以作为合法起点。


合法终点呢?

题目规定:

出度为0的点是合法终点。

例如:

复制代码
6 → 7
6 → 8

那么6有两条边出去:

复制代码
outdegree[6] = 2

而7和8没有边出去:

复制代码
outdegree[7] = 0
outdegree[8] = 0

所以:

复制代码
7、8

是合法终点。


十、程序需要准备什么?

我们可以准备几个数组。

1. 邻接表

复制代码
vector<int> graph[N];

表示:

每个点可以走到哪些点。

例如:

复制代码
1 → 3

graph[1] = {3}

如果:

复制代码
2 → 3
2 → 4

那么:

复制代码
graph[2] = {3, 4}

2. 入度数组

复制代码
int indegree[N];

记录:

每个点有多少条边指向它。


3. 出度数组

复制代码
int outdegree[N];

记录:

每个点有多少条边从它出去。


4. 访问数组

复制代码
bool vis[N];

DFS时防止重复走。


十一、读入每一条边

题目给出的每条边:

复制代码
u v

表示:

复制代码
u → v

所以:

复制代码
graph[u].push_back(v);
indegree[v]++;
outdegree[u]++;

这三句话非常重要。

可以理解成:

在u的通讯录里记下v。

同时:

v的入度+1。

还有:

u的出度+1。


十二、最核心的DFS

我们写:

复制代码
void dfs(int u, int ban)

这里的 ban 是什么意思?

就是:

这次实验中,我们禁止经过哪个点。

例如:

复制代码
dfs(1, 4);

意思就是:

从1开始搜索,但是4号点封锁了。


DFS第一件事

如果:

复制代码
u == ban

直接不能走:

复制代码
if (u == ban)
    return;

第二件事

标记已经访问:

复制代码
vis[u] = true;

第三件事

看看是不是到达合法终点。

复制代码
if (outdegree[u] == 0)
    found = true;

如果到了合法终点:

说明存在一条没有经过 ban 的合法路线。

于是:

复制代码
found = true;

第四件事

继续向邻居走:

复制代码
for (int v : graph[u]) {
    if (!vis[v] && v != ban) {
        dfs(v, ban);
    }
}

这样DFS就完成了。


十三、把"删除一个点"的过程完整走一遍

假设我们现在测试:

复制代码
ban = 4

也就是:

关闭4号点。

首先:

复制代码
found = false;

然后从所有合法起点开始:

复制代码
1
2

进行DFS。

如果最终找到:

复制代码
7

或者:

复制代码
8

那么:

复制代码
found = true;

说明:

即使没有4,仍然存在起点→终点的路线。

所以:

复制代码
4不是必经点

反过来,如果:

复制代码
found == false

说明:

关闭4以后,所有合法起点都无法到达任何合法终点。

那么:

复制代码
4就是必经点。

十四、这里有一个非常容易犯的错误

假设我们测试:

复制代码
ban = 1

而1本身就是合法起点。

这时候:

不能从1开始DFS!

因为题目要求"不经过1"。

所以启动DFS的时候必须:

复制代码
if (s == ban)
    continue;

也就是说:

如果当前合法起点就是被关闭的点,就直接跳过。


十五、如果被关闭的是终点呢?

例如:

复制代码
ban = 7

7是合法终点。

也不能走到7。

因此DFS过程中:

复制代码
if (v == ban)
    continue;

不能进入被关闭的点。

而且:

复制代码
if (u == ban)
    return;

也可以保证这一点。


十六、完整程序

下面给初学C++的小学生一个C++11版本、尽量清晰的完整程序:

复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int N = 1005;

vector<int> graph[N];

int indegree[N];
int outdegree[N];

bool vis[N];

bool found;

// 从 u 出发搜索
// ban 表示禁止经过的点
void dfs(int u, int ban)
{
    // 不能进入被关闭的点
    if (u == ban)
        return;

    vis[u] = true;

    // 如果到达合法终点
    if (outdegree[u] == 0)
    {
        found = true;
        return;
    }

    // 继续向其他点走
    for (int v : graph[u])
    {
        if (!vis[v] && v != ban)
        {
            dfs(v, ban);

            // 已经找到终点,不需要继续搜索
            if (found)
                return;
        }
    }
}

int main()
{
    int n, m;
    cin >> n >> m;

    // 读入所有边
    for (int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;

        graph[u].push_back(v);

        indegree[v]++;
        outdegree[u]++;
    }

    vector<int> answer;

    // 一个一个测试每个点
    for (int ban = 1; ban <= n; ban++)
    {
        // 清空访问标记
        for (int i = 1; i <= n; i++)
        {
            vis[i] = false;
        }

        found = false;

        // 从所有合法起点出发
        for (int s = 1; s <= n; s++)
        {
            // 入度为0,才是合法起点
            if (indegree[s] == 0 && s != ban)
            {
                dfs(s, ban);

                if (found)
                    break;
            }
        }

        // 如果一个合法起点都无法到达合法终点
        // 说明 ban 是必经点
        if (!found)
        {
            answer.push_back(ban);
        }
    }

    // 输出答案数量
    cout << answer.size() << endl;

    // 输出所有必经点
    if (!answer.empty())
    {
        for (int x : answer)
        {
            cout << x << " ";
        }

        cout << endl;
    }

    return 0;
}

十七、把程序拆成"四个小任务"

对于小学生,我不建议让他们一下子背完整程序。

可以把它拆成四个任务。

任务1:建立地图

复制代码
vector<int> graph[N];

把所有道路记录下来。


任务2:找入口和出口

复制代码
indegree[s] == 0

就是合法起点。

复制代码
outdegree[u] == 0

就是合法终点。


任务3:关闭一个点

复制代码
ban

把它理解成:

"这个城市今天封路了,谁也不能进去。"

然后DFS。


任务4:判断是不是必经点

如果关闭它以后:

复制代码
还有起点 → 终点

那么:

复制代码
不是必经点

如果:

复制代码
没有任何起点 → 终点

那么:

复制代码
是必经点

十八、用样例1手动模拟

样例:

复制代码
8 9

1 3
2 3
3 4
4 5
5 6
6 7
6 8
2 4
5 7

合法起点:

复制代码
1、2

合法终点:

复制代码
7、8

我们来测试4。


第一步:关闭4

相当于:

复制代码
4号城市关闭!

那么原来的:

复制代码
1 → 3 → 4

走不通。

2也不能通过4。

于是:

复制代码
1、2
 ↓
 3
 ↓
❌ 4

到不了5、6、7、8。

所以:

复制代码
关闭4以后,没有任何起点能够到达终点。

因此:

4是必经点


第二步:关闭5

同理:

复制代码
1 → 3 → 4 → ❌5
2 → 3 → 4 → ❌5

虽然6、7、8还存在,但是到不了它们。

所以:

5也是必经点


第三步:关闭3

这时候:

复制代码
1 → ❌3

但是:

复制代码
2 → 4 → 5 → 6 → 7

仍然可以走到终点。

所以:

3不是必经点

这就是为什么样例最终只有:

复制代码
4 5

十九、为什么这个方法能通过数据范围?

题目的数据范围是:

n≤1000 m≤2000

我们最多测试:

复制代码
1000个点

每测试一个点,需要做一次图搜索。

DFS一次大约:

O(n+m)

所以总复杂度大约:

O(n(n+m))

也就是大约:

3,000,000

级别的操作。

对于这个数据范围来说是完全可以接受的。

所以:

这道题不需要一上来就使用非常复杂的图论算法。

用我们已经学习过的:

邻接表 + DFS + 枚举

就可以解决。


二十、这道题真正考查的是什么?

表面上看,这是一道:

图论题。

但实际上它考查了好几个知识点的组合:

① 有向图

知道:

复制代码
u → v

表示有方向的边。


② 入度

复制代码
indegree[v]++;

用来寻找合法起点。


③ 出度

复制代码
outdegree[u]++;

用来寻找合法终点。


④ 邻接表

复制代码
graph[u].push_back(v);

保存每个点能够走向哪里。


⑤ DFS

从起点不断探索。


⑥ 枚举

一个点一个点地进行"关闭实验"。


⑦ 反向思维

这是最重要的!

题目问:

哪些点所有路线都必须经过?

我们没有直接找"所有路线"。

而是反过来问:

如果不允许经过这个点,还有没有一条路?

如果有:

复制代码
不是必经点

如果没有:

复制代码
就是必经点

这就是算法竞赛里非常重要的:

"正着不好做,就反过来想!"


二十一、给小学生的"魔法口诀"

这道题,大家记住这段口诀:

先数入度找入口,
再数出度找出口;
一个点一个点封,
DFS看看能否通;
还能通,不必经;
完全不通,必经点!

再浓缩成一句:

"封一个,搜一次;还能到,不必经;到不了,就是它!"


二十二、这道题和前面学过的知识串起来

这道题特别适合用来帮助学生建立"知识网络":

复制代码
                    《必经之路》
                         │
             ┌───────────┼───────────┐
             ↓           ↓           ↓
           有向图       邻接表        DFS
             │           │           │
        ┌────┴────┐      │      ┌────┴────┐
        ↓         ↓      ↓      ↓         ↓
       入度       出度   存边    递归      visited
        │         │
        ↓         ↓
      合法起点   合法终点
             \     /
              \   /
               ↓
            必经点
               ↑
               │
          枚举"封锁"
               │
               ↓
        还能不能走到终点?

这也是非常重要的一种学习方式:

不要把"图、DFS、入度、出度、邻接表"当成一个个孤零零的知识点,而要学会把它们组合起来解决问题。

这道《必经之路》的核心思想其实只有一句话:
"判断一个点是不是必经点,就把它暂时删掉,然后看看是否还存在一条合法起点到合法终点的路。"


相关推荐
ebiobiz1 小时前
Zig 嵌入式底层库选择与 build.zig 组织指南
c语言·c++·嵌入式硬件·mcu
漂流瓶jz2 小时前
UVA-11491 奖品的价值 题解答案代码 算法竞赛入门经典第二版
c++·算法·图论·题解·aoapc·算法竞赛入门经典·uva
weixin_307779136 小时前
C++代码实现MATLAB中的dlarray函数功能
开发语言·c++·算法·matlab
Frank_refuel10 小时前
C++11之一场名为“搬家”的 C++ 之旅
开发语言·c++
hetao173383712 小时前
2026-10-03~04 hetao1733837 的刷题记录
c++·算法
longlongzihan12 小时前
从LeetCode 70. 爬楼梯看动态规划:如何精准推导状态转移方程?
c++·算法·leetcode·动态规划
潼心1412o15 小时前
C++初阶(长期更新)第6讲:类和对象(下)
开发语言·c++·算法
-森屿安年-16 小时前
按身高排序
c++·算法
(Charon)17 小时前
【C++面试】vector底层原理:扩容、拷贝移动与迭代器失效
c++·面试