NOI C++ 课程体系 / 算法篇 / 第13讲
CSP-J 入门级 · 搜索算法 · m4-13

深度优先搜索 DFS 与图的深度优先遍历

一条路走到黑,不撞南墙不回头 —— 掌握 DFS 的核心思想,学会用递归框架解决迷宫、图遍历、N皇后等经典问题。

6 大模块 4 个经典案例 2 种图存储 5 大易错点 32 道练习题
本讲 · 第 37/56 讲深度优先搜索 DFS
模块 01

情景导航

从一个迷宫探险故事开始,感受"一条路走到黑"的搜索思想。

故事迷宫探险

小明进入了一个大型迷宫,他只有一个策略:沿一条路一直往前走,走到死胡同就退回上一个岔路口,换一条路继续走。

第1步:从入口往前走 → 到岔路口A,选左边的路
第2步:继续走 → 到岔路口B,选左边的路
第3步:走到死胡同!→ 退回岔路口B
第4步:从B走右边 → 又到死胡同!→ 退回B → B的路都走完了 → 退回A
第5步:从A走右边 → 找到了出口!🎉

一条路走到黑 → 走不通就回头 → 换条路继续!

这种"不撞南墙不回头"的策略,就是深度优先搜索(DFS)的核心思想。它总是优先往深处探索,直到无路可走才回头。

💡 DFS 的关键词

深入:优先往更深层次走,不急于探索旁边的路

回溯:走到死路就退回上一步,尝试其他选择

递归:DFS 天然适合用递归来实现 —— 每走一步就调用自身

DFS 是搜索算法的基石,后续的图论、回溯、动态规划都建立在 DFS 之上。

①生活类比:DFS 无处不在
🌀

走迷宫

沿一条路走到头,走不通就退回换路。

→ 经典 DFS
📂

文件夹查找

先深入子文件夹找完,再回到上级找其他。

→ 树的 DFS 遍历
🌳

家谱查询

先查完一个人的所有后代,再查他的兄弟。

→ 先序遍历
🧩

数独填数

一个格子试一个数,不行就退回来换数。

→ 回溯法 (DFS)
②编程定义

深度优先搜索(DFS, Depth-First Search):一种搜索策略,从起始点出发,尽可能深地沿一个方向探索,直到无法继续时才回退到最近的分叉点,转向其他方向继续探索。

核心特征:后进先出(LIFO)—— 最后探索的节点最先回溯。用递归或栈实现。

📌 本讲核心知识地图

板块核心内容对应大纲
DFS 框架递归模板 + visited 标记 + 回溯深度优先搜索
图的存储邻接矩阵 / 邻接表图的基本概念
图的 DFS 遍历从起点出发遍历所有可达节点图的深度优先遍历
迷宫寻路方向数组 + 回溯 + 边界判断DFS 应用
N 皇后回溯法经典问题DFS 应用
连通分量多次 DFS 统计连通块数图的连通性
③核心口诀
🔑 DFS 核心三步

① 标记:到达一个点,先标记已访问(visited)
② 深入:递归探索所有未访问的相邻点
③ 回溯:所有方向走完,返回上一层

🔑 DFS 模板

void dfs(当前状态) {
  if (到达目标) { 记录答案; return; }
  for (所有可能的下一步) {
    if (合法 && 未访问) {
      标记; dfs(新状态); 取消标记; // 回溯
    }
  }
}

模块 02

知识探究

从基本框架到经典案例,全面掌握 DFS 的设计与实现。

1DFS 基本框架 核心

DFS 的实现有两种方式:递归(最常用)和栈(手动模拟)。竞赛中几乎都用递归。

1
📍
确定状态

当前在哪个位置?已做了什么选择?

2
✅
终止条件

什么时候到达目标?什么时候停止递归?

3
🔄
枚举选择

当前状态有哪些合法的下一步?

4
↩️
递归回溯

做出选择 → 递归 → 撤销选择(回溯)

C++DFS 通用模板
void dfs(int state) {
    // 1. 终止条件
    if (达到目标) {
        记录答案;
        return;
    }

// 2. 枚举所有可能的选择
    for (int i = 0; i < 选择数; i++) {
        int next_state = 做出选择(state, i);

// 3. 检查合法性
        if (合法(next_state) && !visited[next_state]) {
            visited[next_state] = true;   // 标记
            dfs(next_state);                // 深入
            visited[next_state] = false;  // 回溯(取消标记)
        }
    }
}
🔑 两种 DFS 的区别

遍历型 DFS:访问所有节点,不需要回溯(visited 不需要撤销)
→ 如:图的遍历、连通分量
搜索型 DFS:寻找满足条件的解,需要回溯(visited 要撤销)
→ 如:迷宫、N 皇后、全排列

2图的存储方式 基础

在用 DFS 遍历图之前,首先要学会如何在程序中存储图。

📊 邻接矩阵
  • 用二维数组 g[i][j] 表示 i→j 是否有边
  • g[i][j]=1 有边,0 无边
  • 优点:判断两点是否相连 O(1)
  • 缺点:空间 O(n²),遍历邻居 O(n)
  • 适合稠密图(边多)或 n ≤ 1000
int g[1005][1005];
// 加边 u→v
g[u][v] = 1;
g[v][u] = 1; // 无向图
📋 邻接表
  • 用 vector 数组 adj[i] 存 i 的所有邻居
  • 优点:空间 O(n+m),遍历邻居 O(度)
  • 缺点:判断相连需遍历
  • 适合稀疏图(边少),竞赛首选
vector<int> adj[100005];
// 加边 u→v
adj[u].push_back(v);
adj[v].push_back(u); // 无向图
比较项邻接矩阵邻接表
空间复杂度O(n²)O(n + m)
加边O(1)O(1)
查邻居O(n)O(度数)
判断相连O(1)O(度数)
适用场景n ≤ 1000, 稠密图n ≤ 10⁵, 稀疏图
案例经典案例1:图的深度优先遍历 必会

给定一个无向图,从节点 1 出发进行 DFS 遍历,输出访问顺序。

📊 图结构(6个节点,7条边)
1 2 3 4 5 6

🔍 DFS 遍历过程(从节点1出发)

邻接表(按编号从小到大):

adj[1] = {2, 4}
adj[2] = {1, 3, 5}
adj[3] = {2, 6}
adj[4] = {1, 5}
adj[5] = {2, 4, 6}
adj[6] = {3, 5}

执行过程:

① 访问 1 → visited={1} → 邻居{2,4},先走2

② 访问 2 → visited={1,2} → 邻居{1,3,5},1已访问,走3

③ 访问 3 → visited={1,2,3} → 邻居{2,6},2已访问,走6

④ 访问 6 → visited={1,2,3,6} → 邻居{3,5},3已访问,走5

⑤ 访问 5 → visited={1,2,3,6,5} → 邻居{2,4,6},2和6已访问,走4

⑥ 访问 4 → visited={1,2,3,6,5,4} → 邻居{1,5},都已访问,回溯

DFS 遍历顺序:1 → 2 → 3 → 6 → 5 → 4

C++graph_dfs.cpp
#include <iostream>
#include <vector>
using namespace std;

int n, m;
vector<int> adj[100005];
bool vis[100005];

void dfs(int u) {
    cout << u << " ";        // 访问当前节点
    vis[u] = true;            // 标记已访问
    for (int v : adj[u]) {    // 遍历所有邻居
        if (!vis[v]) {         // 未访问过
            dfs(v);             // 递归深入
        }
    }
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);   // 无向图
    }
    dfs(1);  // 从节点1开始
    cout << endl;
    return 0;
}
运行结果
输入:6 7
1 2  1 4  2 3  2 5  3 6  4 5  5 6
输出:1 2 3 6 5 4
案例经典案例2:迷宫寻路 高频

给定一个 n×m 的迷宫,0 表示空地,1 表示墙壁。从左上角 (1,1) 走到右下角 (n,m),问是否存在通路?

🔍 DFS 迷宫求解分析

状态:当前坐标 (x, y)

选择:四个方向(上、下、左、右)

终止:到达 (n, m)

约束:不越界、不走墙、不走已访问的点

方向数组:dx[]={0,0,1,-1}, dy[]={1,-1,0,0}(右、左、下、上)

C++maze_dfs.cpp
#include <iostream>
using namespace std;

int n, m;
int grid[105][105];
bool vis[105][105];
int dx[] = {0, 0, 1, -1};  // 右、左、下、上
int dy[] = {1, -1, 0, 0};
bool found = false;

void dfs(int x, int y) {
    if (x == n && y == m) {   // 到达终点
        found = true;
        return;
    }
    vis[x][y] = true;
    for (int d = 0; d < 4; d++) {
        int nx = x + dx[d];
        int ny = y + dy[d];
        if (nx>=1 && nx<=n && ny>=1 && ny<=m  // 不越界
            && grid[nx][ny]==0                  // 不是墙
            && !vis[nx][ny]) {                   // 未访问
            dfs(nx, ny);
            if (found) return;  // 找到了就提前返回
        }
    }
}

int main() {
    cin >> n >> m;
    for (int i=1; i<=n; i++)
        for (int j=1; j<=m; j++)
            cin >> grid[i][j];
    dfs(1, 1);
    cout << (found ? "YES" : "NO") << endl;
    return 0;
}
运行结果
输入:
4 5
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
0 1 0 0 0
输出:YES

💡 迷宫 DFS 的关键技巧

方向数组:用 dx/dy 统一管理四个方向,避免写 4 个 if-else

提前返回:找到目标后立即 return,避免无意义的搜索

注意:迷宫 DFS 的 visited 不需要撤销(因为只要找一条路,走过的点不需要再走)

如果要找最短路径,应该用 BFS 而不是 DFS!

案例经典案例3:N 皇后问题 经典

在 n×n 的棋盘上放置 n 个皇后,使得任意两个皇后不能在同一行、同一列、同一对角线上。求方案数。

🔍 回溯法分析

状态:当前处理第 row 行(每行放一个皇后)

选择:第 row 行放在哪一列(1~n)

约束:该列、主对角线、副对角线上没有其他皇后

终止:n 行都放完 → 方案数 +1

优化:用 col[j]、dg[i+j]、udg[i-j+n] 三个数组 O(1) 判断冲突

C++nqueens.cpp
#include <iostream>
using namespace std;

int n, cnt = 0;
bool col[20], dg[40], udg[40];  // 列、主对角线、副对角线

void dfs(int row) {
    if (row == n + 1) {    // n行都放完了
        cnt++;
        return;
    }
    for (int j = 1; j <= n; j++) {
        if (!col[j] && !dg[row + j] && !udg[row - j + n]) {
            col[j] = dg[row + j] = udg[row - j + n] = true;  // 标记
            dfs(row + 1);                                      // 递归下一行
            col[j] = dg[row + j] = udg[row - j + n] = false; // 回溯!
        }
    }
}

int main() {
    cin >> n;
    dfs(1);
    cout << cnt << endl;
    return 0;
}
运行结果
输入:8
输出:92

💡 N 皇后的对角线技巧

• 主对角线(↘方向):同行列之差相同 → 用 row - j + n 作下标(+n 防负数)

• 副对角线(↗方向):同行列之和相同 → 用 row + j 作下标

• 这是回溯型 DFS的典型例子:标记 → 递归 → 撤销标记

N 皇后方案数:n=1→1, n=4→2, n=8→92, n=10→724, n=13→73712

案例经典案例4:连通分量计数 高频

给定一个无向图,问图中有多少个连通分量(互相可达的节点组成的最大集合)。

🔍 算法思路

从节点 1 开始,依次对每个未访问的节点执行 DFS。每次 DFS 能访问到的所有节点构成一个连通分量。

连通分量数 = 执行 DFS 的次数

C++connected_components.cpp
#include <iostream>
#include <vector>
using namespace std;

int n, m;
vector<int> adj[100005];
bool vis[100005];

void dfs(int u) {
    vis[u] = true;
    for (int v : adj[u])
        if (!vis[v]) dfs(v);
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

int components = 0;
    for (int i = 1; i <= n; i++) {
        if (!vis[i]) {       // 未访问 → 新的连通分量
            dfs(i);            // 标记该分量所有节点
            components++;       // 分量数 +1
        }
    }
    cout << components << endl;
    return 0;
}
运行结果
输入:6 3
1 2  2 3  4 5
输出:3
(三个连通分量:{1,2,3}、{4,5}、{6})
模块 03

实践应用

通过分析题和编程题,巩固 DFS 的设计与调试能力。

题1DFS 序列分析

题目:以下无向图,从节点 1 出发进行 DFS(邻居按编号从小到大访问),写出 DFS 遍历序列。

边:(1,2), (1,3), (2,4), (2,5), (3,5), (4,6), (5,6)

提示:画出图结构,模拟 DFS 的执行过程。
查看参考答案

邻接表(排序后):

adj[1]={2,3}, adj[2]={1,4,5}, adj[3]={1,5}, adj[4]={2,6}, adj[5]={2,3,6}, adj[6]={4,5}

逐步模拟:

① dfs(1):访问 1,邻居 {2,3},先走 2

② dfs(2):访问 2,邻居 {1,4,5},1已访问,走 4

③ dfs(4):访问 4,邻居 {2,6},2已访问,走 6

④ dfs(6):访问 6,邻居 {4,5},4已访问,走 5

⑤ dfs(5):访问 5,邻居 {2,3,6},2和6已访问,走 3

⑥ dfs(3):访问 3,邻居 {1,5},都已访问,回溯

DFS 序列:1 → 2 → 4 → 6 → 5 → 3

题2迷宫路径计数

题目:在 n×m 的网格中,从 (1,1) 走到 (n,m),每次只能向右或向下走。网格中有障碍物(标记为1)。求共有多少条不同路径?

查看参考答案

分析:这是搜索型 DFS + 回溯。每到一个格子,尝试向右和向下两个方向。

注意:由于只能向右/向下,不需要 visited 数组(不会走回头路)。

#include <iostream>
using namespace std;
int n, m, cnt = 0;
int grid[15][15];

void dfs(int x, int y) {
    if (x == n && y == m) { cnt++; return; }
    if (x + 1 <= n && grid[x+1][y] == 0) dfs(x+1, y); // 向下
    if (y + 1 <= m && grid[x][y+1] == 0) dfs(x, y+1); // 向右
}

int main() {
    cin >> n >> m;
    for (int i=1;i<=n;i++)
        for (int j=1;j<=m;j++) cin >> grid[i][j];
    if (grid[1][1] == 0) dfs(1, 1);
    cout << cnt << endl;
    return 0;
}

复杂度:最坏 O(2^(n+m)),n,m ≤ 15 时可行。更大的数据需用 DP。

题3图的染色问题

题目:给定一个无向图,用 3 种颜色(红、绿、蓝)给每个节点染色,要求相邻节点颜色不同。判断是否存在合法染色方案?若存在,输出一组方案。

查看参考答案

分析:这是经典的图着色问题,用 DFS 回溯求解。

对每个节点,尝试 3 种颜色,检查是否与相邻节点冲突。

#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<int> adj[105];
int color[105]; // 0=未染色, 1/2/3

bool dfs(int u) {
    if (u == n + 1) return true; // 所有节点都染完了
    for (int c = 1; c <= 3; c++) {
        bool ok = true;
        for (int v : adj[u])
            if (color[v] == c) { ok = false; break; }
        if (ok) {
            color[u] = c;
            if (dfs(u + 1)) return true;
            color[u] = 0; // 回溯
        }
    }
    return false;
}

int main() {
    cin >> n >> m;
    for(int i=0;i<m;i++){int u,v;cin>>u>>v;adj[u].push_back(v);adj[v].push_back(u);}
    if(dfs(1)){
        cout<<"YES"<<endl;
        for(int i=1;i<=n;i++) cout<<color[i]<<" ";
    } else cout<<"NO";
    cout<<endl;
}

关键:回溯时必须将 color[u] 恢复为 0。图着色是 NP-完全问题,3色着色在一般图上无多项式算法。

模块 04

总结提升

1核心要点总结
知识板块必须记住的核心
DFS 思想一条路走到黑,走不通就回头(回溯)
DFS 框架标记 → 递归深入 → 回溯(取消标记)
图存储邻接矩阵(O(n²)) / 邻接表(O(n+m)),竞赛选邻接表
图的 DFSvis 数组标记,遍历所有邻居递归深入
迷宫 DFS方向数组 dx/dy + 边界判断 + 障碍物判断
N 皇后col/dg/udg 三数组 O(1) 判断冲突
连通分量对每个未访问节点执行 DFS,执行次数 = 分量数
时间复杂度图 DFS: O(n+m);搜索型 DFS: 取决于搜索空间
2拓展:DFS 剪枝优化

💡 剪枝:让 DFS 快起来的秘密武器

可行性剪枝:当前状态已经不可能满足条件,直接 return

例:N 皇后中,如果剩余行数不够放剩余皇后,提前终止

最优性剪枝:当前代价已经超过已知最优解,直接 return

例:迷宫求最短路径,当前步数已超过已知最短,提前终止

记忆化:记录已搜索过的状态,避免重复计算

例:用 visited 或 dp 数组缓存已搜索的结果

竞赛技巧:搜索型 DFS 如果超时,第一反应应该是"能不能剪枝?"

3思考与启发

💡 学完本讲,你应该掌握的6项能力

1. 能写出DFS 递归模板并正确处理标记和回溯。

2. 能用邻接表存储图,并实现图的 DFS 遍历。

3. 能用方向数组处理迷宫/网格类 DFS 问题。

4. 能区分遍历型 DFS和搜索型 DFS的差异(是否需要回溯)。

5. 能用 DFS 求连通分量数。

6. 能识别 DFS 超时的原因,并用剪枝优化。

模块 05

学习检测

5 个初学者最常犯的错误。

易错1

忘记标记 visited

❌ 错误
DFS 中没有标记已访问,导致无限递归 → 栈溢出 (Stack Overflow)。
✅ 正确
进入 DFS 的第一件事就是标记 vis[u]=true,防止重复访问。
口诀:进门先打卡(标记),出门看情况(回溯)!
易错2

遍历型和搜索型 DFS 混淆

❌ 错误
图的遍历中把 vis[u]=false 写在 return 前(撤销标记),导致重复遍历。
✅ 正确
遍历型不需要回溯(vis 保持 true);搜索型(迷宫/N皇后)需要回溯(vis 撤销)。
口诀:遍历不回头,搜索要回溯!
易错3

迷宫 DFS 用最短路径

❌ 错误
用 DFS 找迷宫最短路径,结果找到的不是最短的。
✅ 正确
DFS 找到的是某一条路径,不一定最短。找最短路径应该用 BFS!
口诀:DFS 找路径,BFS 找最短!
易错4

方向数组下标搞错

❌ 错误
dx/dy 不匹配,"向下"写成了 dy 加1 而不是 dx 加1。
✅ 正确
dx 对应行(x),dy 对应列(y)。统一约定:dx[]={0,0,1,-1}, dy[]={1,-1,0,0}。
口诀:dx 管行,dy 管列,配对不能乱!
易错5

DFS 超时不剪枝

❌ 错误
DFS 暴力搜索所有状态,n 稍大就 TLE,不知道优化。
✅ 正确
搜索前分析:可行性剪枝(不可能就 return)+ 最优性剪枝(超过已知最优就 return)。
口诀:搜索先想剪枝,暴力也要聪明地暴力!
速查DFS 速查表
问题类型DFS 要点复杂度
图的遍历vis标记,不回溯O(n+m)
连通分量多次DFS,次数=分量数O(n+m)
迷宫寻路方向数组+回溯(或不回溯)O(4^nm) 最坏
N皇后col/dg/udg 三数组,回溯O(n!)
全排列选过的不能选,回溯O(n!)
图着色检查邻居颜色冲突,回溯O(k^n)
📌知识点笔记卡

🧩 DFS 核心

一条路走到黑

走不通就回头

用递归实现

📊 图的存储

邻接矩阵: O(n²)

邻接表: O(n+m)

竞赛选邻接表

🌀 图DFS遍历

标记→递归→不回溯

访问所有可达节点

O(n+m)

🧱 迷宫DFS

方向数组dx/dy

边界+障碍判断

找路径不求最短

👑 N皇后

col/dg/udg 三数组

标记→递归→回溯

回溯经典问题

✂️ 剪枝

可行性剪枝

最优性剪枝

让DFS更快

课后练习

分层练习作业

A基础 / B提高 / C挑战 + 洛谷原题,难度递增。

🟢 A组 · 基础(4题)

A1选择题

深度优先搜索(DFS)使用的数据结构是()

A. 队列(FIFO)  B. 栈(LIFO)或递归   C. 优先队列   D. 哈希表

查看参考答案

答案:B

DFS 的核心是"后进先出"——最后探索的节点最先回溯。这正好是栈(LIFO)的特性。递归本质上也是用系统调用栈实现的。

考点:DFS 依赖“后进先出”,递归调用本身就是在用系统栈模拟这一过程。

易错:把 DFS 说成用队列、BFS 说成用栈,正好把两者记反。

A2选择题

对 n 个节点 m 条边的无向图进行 DFS,时间复杂度为()

A. O(n)   B. O(m)   C. O(n+m)   D. O(n×m)

查看参考答案

答案:C. O(n+m)

每个节点被访问一次 O(n),每条边被检查两次(无向图)O(m),总计 O(n+m)。

考点:图遍历的复杂度由“顶点数 + 边数”共同决定,两者是相加关系。

易错:把无向图的每条边算成被访问两次后写成 O(n+2m) 之类的式子,或误答 O(n×m)。

A3选择题

DFS 中 visited 数组的作用是()

A. 记录最短路径   B. 防止重复访问导致死循环   C. 记录节点权值   D. 排序用

查看参考答案

答案:B

visited 数组标记已访问的节点,防止 DFS 重复访问同一个节点导致无限递归(栈溢出)。

考点:visited 标记保证每个节点只被访问一次,是避免重复访问与无限递归的关键。

易错:忘记在进入节点时就把起点标记为已访问,起点会被邻居再次访问,形成循环。

A4编程题

给定 n 个节点 m 条边的无向图,从节点 1 开始 DFS,输出遍历序列。

查看参考答案
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> adj[1005];
bool vis[1005];
void dfs(int u) {
    cout << u << " ";
    vis[u] = true;
    for (int v : adj[u])
        if (!vis[v]) dfs(v);
}
int main() {
    int n, m; cin >> n >> m;
    for(int i=0;i<m;i++){
        int u,v; cin>>u>>v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    for(int i=1;i<=n;i++) sort(adj[i].begin(),adj[i].end());
    dfs(1);
    cout << endl;
}

注意:题目要求邻居按编号从小到大访问,所以需要对邻接表排序。

思路:用邻接表存图,dfs(u) 先打印 u 并标记 visited,再依次递归访问所有未访问的邻居。

🟡 B组 · 提高(4题)

B1编程题

给定 n×m 迷宫(0空地1墙),求从(1,1)到(n,m)是否存在通路。若存在输出 YES,否则输出 NO。

查看参考答案
#include <iostream>
using namespace std;
int n,m,g[105][105];
bool vis[105][105],found=false;
int dx[]={0,0,1,-1},dy[]={1,-1,0,0};
void dfs(int x,int y){
    if(x==n&&y==m){found=true;return;}
    vis[x][y]=true;
    for(int d=0;d<4;d++){
        int nx=x+dx[d],ny=y+dy[d];
        if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!g[nx][ny]&&!vis[nx][ny])
            dfs(nx,ny);
        if(found)return;
    }
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)cin>>g[i][j];
    dfs(1,1);
    cout<<(found?"YES":"NO")<<endl;
}

思路:从 (1,1) 出发尝试四个方向,走到终点就置 found=1 返回;墙壁和已访问的格子都直接跳过。

易错:递归里忘记先判断越界就访问数组,导致非法下标;或把终点判断放在标记之后,遇到终点仍继续搜索。

B2编程题

给定 n×m 的棋盘,在其中放置 k 个棋子,要求任意两个棋子不在相邻的格子中(上下左右)。求方案数。

查看参考答案

思路:按格子编号 1~n*m,DFS 枚举每个格子放或不放。用回溯法搜索所有合法方案。

#include <iostream>
using namespace std;
int n,m,k,cnt=0;
int board[10][10];
int dx[]={0,0,1,-1},dy[]={1,-1,0,0};

bool canPlace(int r,int c){
    for(int d=0;d<4;d++){
        int nr=r+dx[d],nc=c+dy[d];
        if(nr>=1&&nr<=n&&nc>=1&&nc<=m&&board[nr][nc]) return false;
    }
    return true;
}

void dfs(int pos,int placed){
    if(placed==k){cnt++;return;}
    if(pos>n*m)return;
    if(n*m-pos+1<k-placed)return; // 剪枝
    int r=(pos-1)/m+1, c=(pos-1)%m+1;
    if(canPlace(r,c)){
        board[r][c]=1; dfs(pos+1,placed+1); board[r][c]=0; // 放
    }
    dfs(pos+1,placed); // 不放
}

int main(){
    cin>>n>>m>>k; dfs(1,0); cout<<cnt<<endl;
}

易错:回溯时忘记把 board 上的标记清零,后续分支会把已经是空位的格子误判为有棋子相邻,从而漏掉大量合法方案。

B3编程题

给定 n 个节点 m 条边的无向图,求连通分量数,并输出每个连通分量包含的节点。

查看参考答案
#include <iostream>
#include <vector>
using namespace std;
vector<int> adj[1005];
bool vis[1005];
vector<int> comp;
void dfs(int u){
    vis[u]=true; comp.push_back(u);
    for(int v:adj[u]) if(!vis[v]) dfs(v);
}
int main(){
    int n,m;cin>>n>>m;
    for(int i=0;i<m;i++){int u,v;cin>>u>>v;adj[u].push_back(v);adj[v].push_back(u);}
    int cnt=0;
    for(int i=1;i<=n;i++){
        if(!vis[i]){
            comp.clear(); dfs(i); cnt++;
            cout<<"分量"<<cnt<<": ";
            for(int x:comp) cout<<x<<" ";
            cout<<endl;
        }
    }
    cout<<"共"<<cnt<<"个连通分量"<<endl;
}

思路:外层遍历所有节点,遇到未访问的就启动一次 DFS 并收集该连通分量的节点,分量计数加一。

易错:只从 1 号节点 DFS 一次,其余连通分量全部漏统计;或收集列表忘记在开始新分量前清空。

B4分析题

分析 N=4 时 N 皇后问题的一个合法解,并验证其正确性。

查看参考答案

N=4 的一个合法解:皇后分别放在第 2、4、1、3 列

即:第1行第2列,第2行第4列,第3行第1列,第4行第3列

棋盘:

. Q . .
. . . Q
Q . . .
. . Q .

验证:

• 不同行:✓(每行恰好一个)

• 不同列:{2,4,1,3} 全部不同 ✓

• 主对角线(row-col):{-1,-2,2,1} 全部不同 ✓

• 副对角线(row+col):{3,6,4,7} 全部不同 ✓

N=4 共有 2 个解(另一个是 {3,1,4,2})

考点:N 皇后合法解需同时满足:不同行、不同列、行号列号之差互不相同、行号列号之和互不相同。

易错:只检查了不同行、不同列,忘记检查两条对角线约束,会把非法摆法误判为合法解。

🔴 C组 · 挑战(4题)

C1初赛真题

关于 DFS 和 BFS,下列说法正确的是()

A. DFS 找到的路径一定是最短的
B. BFS 使用栈实现
C. DFS 的时间复杂度总是 O(n²)
D. DFS 可以用递归实现,BFS 用队列实现

查看参考答案

答案:D

A错(DFS找到的不一定最短,BFS才能找最短)。B错(BFS用队列FIFO)。C错(图的DFS是O(n+m))。D正确。

考点:DFS 不保证最短路(要穷举才能确定),BFS 按层次扩展,无权图上第一次到达即最短。

易错:认为 DFS 也能直接求出最短路径,忽略无权图上 DFS 找到的那条路径可能绕得很远。

C2初赛真题

8 皇后问题的解的个数是()

A. 8   B. 64   C. 92   D. 128

查看参考答案

答案:C. 92

8皇后问题共有 92 个不同的解。这是 DFS 回溯法的经典问题。

考点:8 皇后是 DFS 回溯的经典问题,其不同解的个数为 92。

易错:把 8 皇后的 92 个解与 4 皇后的 2 个解记混,或把“解数”当成摆放方式总数 8!。

C3综合编程

全排列问题:输出 1~n 的所有全排列(n≤8),按字典序输出。

查看参考答案

分析:DFS 回溯,每层选择一个未使用的数字填入当前位置。

#include <iostream>
using namespace std;
int n, a[10];
bool used[10];
void dfs(int pos) {
    if (pos == n + 1) {
        for(int i=1;i<=n;i++) cout<<a[i]<<" ";
        cout<<endl;
        return;
    }
    for (int i=1; i<=n; i++) {
        if (!used[i]) {
            used[i] = true;
            a[pos] = i;
            dfs(pos + 1);
            used[i] = false; // 回溯
        }
    }
}
int main() { cin >> n; dfs(1); }

n=3 时输出:1 2 3 / 1 3 2 / 2 1 3 / 2 3 1 / 3 1 2 / 3 2 1(共 3!=6 个)

易错:回溯时忘记写 used[i]=false,程序只能输出第一个排列;或递归边界写成 pos==n 而数组从 1 开始编号,造成越界。

C4综合编程

给定一个 n×m 的01矩阵,1表示岛屿,0表示水域。相连的1构成一个岛屿(上下左右相连)。求岛屿数量。

查看参考答案

分析:这是连通分量问题的二维版本。遍历每个格子,遇到1就执行DFS标记整个岛屿。

#include <iostream>
using namespace std;
int n, m, grid[105][105];
int dx[]={0,0,1,-1}, dy[]={1,-1,0,0};

void dfs(int x, int y) {
    grid[x][y] = 0; // 标记为已访问(直接改为0)
    for (int d = 0; d < 4; d++) {
        int nx = x + dx[d], ny = y + dy[d];
        if (nx>=1&&nx<=n&&ny>=1&&ny<=m&&grid[nx][ny]==1)
            dfs(nx, ny);
    }
}

int main() {
    cin >> n >> m;
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) cin >> grid[i][j];
    int cnt = 0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(grid[i][j]==1) { dfs(i,j); cnt++; }
    cout << cnt << endl;
}

技巧:直接将访问过的1改为0,省去了 visited 数组。

易错:DFS 里先把 grid[x][y] 改成 0 却没有先做边界判断,会访问到矩阵外;或把方向数组写全 8 个,斜对角也被当成同一岛屿。

🏆 洛谷原题精选(20道)

以下题目均与 DFS / 图遍历 / 搜索回溯相关,按难度从低到高排列。

1P1036选数(组合枚举 DFS)入门
2P1019单词接龙(DFS + 字符串)入门
3P1605迷宫(DFS 经典)入门
4P1219八皇后(N皇后经典)入门
5P1101单词方阵(DFS 方向搜索)入门
6P1162填涂颜色(DFS 标记边界)入门
7P1040加分二叉树(DFS + 记忆化)普及
8P1025数的划分(DFS + 剪枝)普及
9P1135奇怪的电梯(DFS/BFS)普及
10P1434滑雪(DFS + 记忆化)普及
11P1433吃奶酪(DFS + 剪枝)普及
12P114101迷宫(DFS 连通块)普及
13P1126机器人搬重物(DFS/BFS)普及
14P1123取数游戏(DFS + 剪枝)普及
15P2036PERKET(DFS 子集枚举)普及
16P1706全排列问题(DFS 回溯)普及
17P1074靶形数独(DFS + 剪枝)提高
18P1514引水入城(DFS + 区间覆盖)提高
19P1441砝码称重(DFS + DP)提高
20P1120小木棍(DFS + 强剪枝)提高