深度优先搜索可以解决指数型、组合型、排列型等问题,很多小伙伴拿到DFS的问题时不知道该如何处理,这篇文章主要是通过每个类型最具有代表性、最简单的题目来帮助同学们对DFS的写法有更深的理解。
一. DFS实现指数型枚举(求序列的所有子集)
1. 题目描述
从 1 ∼ n 1∼n 1∼n 这 n n n 个整数中随机选取任意多个,输出所有可能的选择方案。
输入格式
输入一个整数
n
n
n。
输出格式
每行输出一种方案。
同一行内的数必须升序排列,相邻两个数用恰好 1 1 1 个空格隔开。
对于没有选任何数的方案,输出空行(空集)。
本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。
数据范围
1
≤
n
≤
15
1≤n≤15
1≤n≤15
输入样例:
3
输出样例:
3
2
2 3
1
1 3
1 2
1 2 3
2. 分析思路
所有的深搜问题都可以对应到一棵递归搜索树,我们必须要考虑一个枚举的顺序,使得能够不漏掉任何一个方案,一种比较好的搜索顺序如下图。
我们可以发现,这种搜索树就是第
1
1
1 层考虑第一个数,第
2
2
2 层考虑第二个数,以此类推。。。
这个问题当然很简单,看起来只是一个选和不选的问题,体现出来是一个二叉树。但对于一个更复杂的问题,我们也可能把它转化成一个多叉树,每一叉代表一种递归的分支。
如果我们将这一棵递归搜索树转化成代码时,我们需要注意这几个参数:
- 当前看到第几个数(对应于树中是层数),记作 u u u
- 我们要记录前面的数是否被选,需要一个 b o o l bool bool 数组
3. 代码模板
#include <iostream>
#include <cstring>
using namespace std;
const int N = 20;
bool st[N];
int n;
void dfs(int u)
{
// 到达叶子结点
if (u > n)
{
for (int i = 1; i <= n; i ++ )
if (st[i]) printf("%d ", i);
puts("");
return;
}
// 在每个结点处依次枚举每个分支
// "选" 分支
st[u] = true;
dfs(u + 1);
st[u] = false; // 属于外部排序,需要回溯
// "不选" 分支
dfs(u + 1);
}
int main()
{
scanf("%d", &n);
dfs(1);
return 0;
}
二. DFS实现组合型枚举(求序列 C n m C_n^m Cnm的所有方案)
1. 题目描述
从 1 ∼ n 1∼n 1∼n 这 n n n 个整数中随机选出 m m m 个,输出所有可能的选择方案。
输入格式
两个整数
n
,
m
n,m
n,m ,在同一行用空格隔开。
输出格式
按照从小到大的顺序输出所有方案,每行
1
1
1 个。
首先,同一行内的数升序排列,相邻两个数用一个空格隔开。
其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面(例如 1 3 5 7
排在 1 3 6 8
前面)。
数据范围
n
>
0
,
n>0 ,
n>0,
0
≤
m
≤
n
,
0≤m≤n ,
0≤m≤n,
n
+
(
n
−
m
)
≤
25
n+(n−m)≤25
n+(n−m)≤25
输入样例:
5 3
输出样例:
1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5
2. 算法思路
对于组合类型的枚举,我们知道 [1 3 2 4]
和 [1 2 4 3]
是同一种情况,为了防止重复枚举,我们一般采取一种类似于从左到右或者从小到大的搜索顺序去进行深度优先搜索。注意:一定要按照某种顺序。比如上面说的从左到右,序列可能是无序的,但是按照这种顺序最后也可以保证防止重复枚举;要是先排序,然后按照从小到大去枚举,既可以防止重复枚举,也可以保证搜出来的叶子结点有序。
例如:
对于序列
[
1
,
2
,
3
,
4
]
[1, 2, 3, 4]
[1,2,3,4],如果我们要从中选
2
2
2 个数,即求
C
4
2
C_4^2
C42 的具体方案:
- 如果第一个数选 1 1 1:则第二个数可以选 [ 2 ] [ 3 ] [ 4 ] [2] [3] [4] [2][3][4]
- 如果第一个数选 2 2 2:则第二个数可以选 [ 3 ] [ 4 ] [3] [4] [3][4]
- 如果第一个数选 3 3 3:则第二个数可以选 [ 4 ] [4] [4]
合在一起一共有 C 4 2 = 6 C_4^2=6 C42=6 种选法。
对应的搜索树如下:
让我们思考一下需要哪些变量去将上述的树转换成代码:
- 维护每一种方案的变量 p a t h path path,既对应着叶子结点,也对应着根到叶子结点的路径
- 层数 u u u,对应着当前选择的第 u u u 个数,也对应着路径中第 u u u 个数
- 维护一个 s t a r t start start 变量,记录下一层要从第几个数开始搜
3. 代码模板
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> path;
int n, m;
// 本质是 C(n, m),path 记录长度为 m 的路径
void dfs(int u, int start)
{
// 说明当前已经找到 m 个数了
if (u >= m)
{
for (int i = 0; i < m; i++)
printf("%d ", path[i]);
puts("");
return;
}
for (int i = start; i <= n; i++)
{
path.push_back(i);
dfs(u + 1, i + 1); // 当前这一层是 i , 下一层就要从 i + 1 开始搜
path.pop_back();
}
}
int main()
{
scanf("%d%d", &n, &m);
dfs(0, 1);
return 0;
}
三. DFS实现排列型枚举(对序列进行全排列)
1. 题目描述
把 1 ∼ n 1∼n 1∼n 这 n n n 个整数排成一行后随机打乱顺序,输出所有可能的次序。
输入格式
一个整数
n
n
n。
输出格式
按照从小到大的顺序输出所有方案,每行
1
1
1 个。
首先,同一行相邻两个数用一个空格隔开。
其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面。
数据范围
1
≤
n
≤
9
1≤n≤9
1≤n≤9
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
2. 算法思路
相信通过对前两个例子的学习,这个模型就非常简单了,直接上搜索树:
然后我们考虑需要哪些参数:
- 当前搜索到全排列的第 u u u 个位置
- 整个全排列要存放在数组 p a t h path path 中
- 同时,还要用 s t st st 数组记录哪些值已经被用过
3. 代码模板
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 15;
bool st[N];
vector<int> path;
int n;
void dfs(int u)
{
// 枚举到 n 的位置则结束
if (u >= n)
{
for (int i = 0; i < path.size(); i++)
printf("%d ", path[i]);
puts("");
return;
}
for (int i = 1; i <= n; i++)
{
if (!st[i])
{
st[i] = true;
path.push_back(i);
dfs(u + 1);
path.pop_back();
st[i] = false;
}
}
}
int main()
{
scanf("%d", &n);
dfs(0);
return 0;
}