前置芝士:递归与队列的基本概念。
例题 on Luogu:P1706 全排列问题 与 P1443 马的遍历
搜索算法简介
搜索算法是信息学竞赛中最基础也是最重要的算法之一。当我们需要在一组可能的解中找到符合特定条件的解时,搜索算法往往是我们首先想到的工具。无论是迷宫寻路、八皇后问题,还是复杂的图论问题,搜索算法都扮演着不可或缺的角色。本文将介绍两种最基本的搜索算法:深度优先搜索(DFS)和广度优先搜索(BFS)。
深度优先搜索(DFS)
DFS 基本思想
深度优先搜索,顾名思义,就是优先向“深处”搜索。它的基本策略是:从起点出发,沿着一条路径一直走到底,如果无法继续前进,则退回到上一个分岔口,选择另一条路径继续探索,直到找到目标或遍历完所有可能。
这种“不撞南墙不回头”的策略,可以用一个生动的比喻来理解:想象你在一个巨大的迷宫中寻找出口,你的方法是每遇到一个岔路口,就随意选择一条路走进去,一直走到死胡同或者出口才回头,然后尝试另一个岔路口。
DFS 的实现方式
DFS 通常有两种实现方式:递归和栈。其中递归实现最为直观和常用,因为递归本身就是在系统栈中完成的“深入”与“回溯”。
DFS 的基本框架如下:
void dfs(当前状态) {
if (当前状态满足结束条件) {
记录或输出结果;
return;
}
for (遍历所有可能的下一步选择) {
if (当前选择合法) {
标记当前选择(避免重复访问);
dfs(下一个状态);
取消标记(回溯);
}
}
}
经典例题:全排列问题
来看一个经典的 DFS 应用——生成 1 到 n 的所有全排列。题目要求按字典序输出所有排列。
我们可以这样思考:第一个位置可以放 1~n 中的任意一个数,第二个位置可以放除了第一个数之外的任意数,以此类推。这正是 DFS 的典型场景。
#include<bits/stdc++.h>
using namespace std;
const int N = 15;
int n;
int ans[N]; // 存储当前排列
bool vis[N]; // 标记数字是否被使用过
void dfs(int step) {
// step 表示当前正在确定第几个位置的数
if (step > n) { // 已经填满了所有位置
for (int i = 1; i <= n; i++) {
cout << setw(5) << ans[i]; // 按格式输出
}
cout << endl;
return;
}
for (int i = 1; i <= n; i++) {
if (!vis[i]) { // 如果数字 i 还没有被使用
vis[i] = true; // 标记已使用
ans[step] = i; // 将 i 放入当前位置
dfs(step + 1); // 递归确定下一个位置
vis[i] = false; // 回溯,取消标记
// ans[step] 不需要清空,因为下次会被覆盖
}
}
}
int main() {
cin >> n;
dfs(1); // 从第一个位置开始
return 0;
}
DFS 的时间复杂度
对于全排列问题,时间复杂度为 O(n!),因为需要生成 n 个数的所有排列。一般来说,DFS 的时间复杂度与解空间的大小直接相关。
广度优先搜索(BFS)
BFS 基本思想
广度优先搜索与 DFS 的策略正好相反,它是“层层推进”的。从起点出发,先访问所有距离为 1 的点,然后通过它们访问所有距离为 2 的点,依此类推,直到找到目标或遍历完所有点。
还是用迷宫比喻:你每遇到一个岔路口,不会贸然深入,而是把所有岔路口都记录下来,然后一层一层地向前推进。这样做的结果是:第一次找到目标时,所经过的路径一定是最短的。
BFS 的实现方式
BFS 通常使用队列实现。队列的先进先出特性正好符合 BFS 的层次遍历需求。
BFS 的基本框架如下:
void bfs(起点) {
queue<状态> q;
q.push(起点);
标记起点已访问;
while (!q.empty()) {
当前状态 = q.front();
q.pop();
if (当前状态是目标状态) {
处理结果;
return;
}
for (遍历所有可能的下一步) {
if (下一个状态合法且未访问) {
标记已访问;
记录距离(或步数);
q.push(下一个状态);
}
}
}
}
经典例题:马的遍历
来看一个 BFS 的经典应用——马的遍历问题。在一个 n×m 的棋盘上,有一个马(按"日"字移动),需要计算它到达棋盘上每个位置的最少步数。
这是一个典型的最短路问题,而 BFS 天然适合求解无权图的最短路。
#include<bits/stdc++.h>
using namespace std;
const int N = 405;
int n, m, sx, sy;
int dis[N][N]; // 存储起点到每个点的最短距离,-1表示不可达
int dx[] = {1, 2, 2, 1, -1, -2, -2, -1}; // 马的8种移动方式
int dy[] = {2, 1, -1, -2, -2, -1, 1, 2};
struct Point {
int x, y;
};
void bfs() {
memset(dis, -1, sizeof(dis)); // 初始化为-1
queue<Point> q;
q.push({sx, sy});
dis[sx][sy] = 0; // 起点距离为0
text
while (!q.empty()) {
Point now = q.front();
q.pop();
for (int i = 0; i < 8; i++) {
int nx = now.x + dx[i];
int ny = now.y + dy[i];
// 检查是否在棋盘范围内且未访问过
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && dis[nx][ny] == -1) {
dis[nx][ny] = dis[now.x][now.y] + 1;
q.push({nx, ny});
}
}
}
}
int main() {
cin >> n >> m >> sx >> sy;
bfs();
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cout << left << setw(5) << dis[i][j];
}
cout << endl;
}
return 0;
}
BFS 的时间复杂度
BFS 的时间复杂度为 O(V + E),其中 V 是顶点数,E 是边数。在棋盘问题中,每个点最多有 8 条边,所以总时间复杂度约为 O(8×n×m)。
两种算法的对比
核心区别
| 特性 | 深度优先搜索 (DFS) | 广度优先搜索 (BFS) |
|---|---|---|
| 搜索策略 | 一条路走到黑,然后回溯 | 层层推进,逐层扩展 |
| 实现方式 | 递归或栈 | 队列 |
| 空间复杂度 | 与搜索深度相关 O(h) | 与分支因子相关 O(b^d) |
| 是否保证最短路径 | 不保证 | 保证(无权图) |
| 适用场景 | 解的存在性、路径计数、排列组合 | 最短路径、最少步数、层序遍历 |
如何选择
选择 DFS 的情况:
- 需要遍历所有可能解(如全排列、子集生成)
- 问题与树的高度相关,且空间有限
- 不需要最短路径,只需要判断是否存在解
选择 BFS 的情况:
- 需要求最短路径或最少步数
- 图的规模不大,层次不会太深
- 解在较浅的层次,BFS 可以更快找到
进阶技巧
DFS 剪枝
对于 DFS,当搜索空间过大时,常常需要剪枝来优化。常见的剪枝策略包括:
- 可行性剪枝:当前路径已经不可能到达目标时,提前返回
- 最优性剪枝:当前路径已经比已知最优解差时,提前返回
- 重复性剪枝:通过记忆化避免重复搜索相同状态
BFS 双向搜索
当知道起点和终点时,可以使用双向 BFS 大幅提升效率。即从起点和终点同时进行 BFS,当两个方向的搜索相遇时,就找到了最短路径。这样可以将搜索复杂度从 O(b^d) 降低到 O(b^(d/2))。
代码总结
这里给出两种算法的通用模板,方便大家在实际题目中快速套用:
DFS 通用模板:
void dfs(参数) {
if (满足结束条件) {
记录结果;
return;
}
text
for (选择 : 所有可能的选择) {
if (选择合法) {
做选择;
标记;
dfs(新参数);
取消标记; // 回溯
}
}
}
BFS 通用模板:
void bfs(起点) {
queue<状态> q;
q.push(起点);
dist[起点] = 0; // 记录距离
text
while (!q.empty()) {
auto now = q.front(); q.pop();
for (所有邻居) {
if (邻居未访问) {
dist[邻居] = dist[now] + 1;
pre[邻居] = now; // 记录路径(可选)
q.push(邻居);
}
}
}
}
总结
深度优先搜索和广度优先搜索是信息学竞赛中最基础也最重要的两种搜索算法。DFS 擅长探索所有可能性,适合解决排列组合类问题;BFS 擅长寻找最短路径,适合解决最少步数类问题。掌握这两种算法,不仅能够解决大量基础题目,更是学习更高级算法(如记忆化搜索、启发式搜索)的基础。
恭喜你初步学会了两种搜索算法,快去推荐题目中试试吧!
7301

被折叠的 条评论
为什么被折叠?



