深度优先搜索,是一种搜索方式,它往往树形图紧密联系,因而通过画图可以帮助我们很好的理解搜索过程,同时它也往往通过递归来实现,因而,对递归的理解以及写出恰当的递归对我们来说十分重要
而对于递归的理解,我认为对For循环与递归的关系认识格外的重要,我们对递归本身就已经挺头疼的了,而此时的深度搜索往往是for循环嵌套递归,循环与递归的结合,如果我们可以理解for循环与递归结合后实现的效果,理解for循环与递归各自的发挥的作用,那么对于深度优先搜索的理解大有裨益,与深度优先搜索有关的剪枝与回溯也可以较好的理解,那 么我们就首先通过一道例题来理解它吧。
例1,数的划分
题目描述
将整数n分成k份,且每份不能为空,任意两个方案不相同(不考虑顺序),例如:n=7,k=3,下面三种分法被认为是相同的:1,1,5; 1,5,1; 5,1,1;
问有多少种不同的分法。
输入输出格式
输入
7 3
输出
4
这4种可能分别为(115 124 133 223 )
我们大多能直接想到的便是for循环,可是乍一想却不知道怎样下手,那么我们可以先降降难度,我们将题目变为将整数n分为3份,接下来我们自然会想到用三层循环,分别确定第一个数,第二个数,第三个数。
代码如下:
#include <iostream>
using namespace std;
int a[100];//用于临时存储得到的数字
int main() {
int n;
int d = 0;//总方案数
cin >> n;
for (int i = 1; i <= n; i++) {
a[1] = i;
for (int j = i; j <= n; j++) {
a[2] = j;
for (int k = j; k <= n; k++) {
a[3] = k;
if (i + j + k == n)
d++;
}
}
}
cout << d;
}
注意细节
除了第一层,其余每一层的起始不是1,而是上一层的变量值,它的目的是为了排除重复的组合,如果内层继续从1开始,会出现115 511 151这样的情况(可以与例2做对比)。
通过观察我们会发现,如果我们分割为4个数就需要4层for循环,50个数就50层for循环,k个数就k层for循环,我们不知道k具体是多少,那么就无法实现这个程序,即使知道,我们也不可能真的写50层for循环。而这时候我们发现它每一层的for循环,有一些结构是类似的,比如,变量的起始位置,结束位置,赋值……这时候我们便可以用递归进行一个统一,递归函数如下:
#include <iostream>
using namespace std;
int n;
int a[100];
int d = 0;
int s;
void dfs(int k) {
if (k == n + 1) {
int sum = 0;
for (int i = 1; i <= n; i++)
sum = sum + a[i];
if (sum == s)
d++;
return ;
}
for (int i = a[k - 1]; i <= s; i++) {
a[k] = i;
dfs(k + 1);
}
}
int main() {
a[0] = 1;
cin >> s >> n;
dfs(1);
cout << d;
}
我们就需要格外注意这两部分的关系:
会发现下图其实是对上图这个dfs()的深层展开,直到展开到最后k+1,判断输出即可。
那么实际上for循环实现的是第i个数选择什么,而递归负责深入到第i+1个数进行选择。
我们再来看一道题,通过这道题我们可以看到深层搜索中回溯这种操作的影子。
例2,1-n的全排列
题目描述
输出所有1-n的全排列
输入输出格式
输入
3
输出
123
132
213
231
312
321
同样,我们首先降一降难度,研究输出某一个特定数字,比如3的全排列,这时候我们需要三层循环,代码如下
#include <iostream>
using namespace std;
int mark[10];//表示i这个数是否用过
int main() {
for (int i = 1; i <= 3; i++) {
if (!mark[i]) {
mark[i] = 1;
for (int j = 1; j <= 3; j++) {
if (!mark[j]) {
mark[j] = 1;
for (int k = 1; k <= 3; k++) {
if (!mark[k]) {
mark[k] = 1;
cout << i << j << k;
cout << endl;
mark[k] = 0;
}
}
mark[j] = 0;
}
}
mark[i] = 0;
}
}
}
注意一些细节
1,每一层的for循环的起始位置都从1开始,和上一题不同,本质原因是组合与排列的不同,例1是组合,例2是排列,相关例题在之后我也会深入对比,本节主要研究for循环与递归的关系。
2,每一层for循环内都有一个mark 标记,表示i这个数是否被用,只有标记为0表示没有被用才可以继续使用。每一层结束前都需要将标记改为0。
那么与上一题同样,如果是1-n便需要n层for循环,我们也可以通过递归来统一,代码如下
#include <iostream>
using namespace std;
int n;
int a[100];
int mark[100];
void dfs(int step) {
if (step == n + 1) {
for (int i = 1; i <= n; i++)
cout << a[i];
cout << endl;
return;
}
for (int i = 1; i <= n; i++) {
if (!mark[i]) {
mark[i] = 1;
a[step] = i;
dfs(step + 1);
mark[i] = 0;
}
}
}
int main() {
cin >> n;
dfs(1);
}
对照两个程序段我们发现他们其实一模一样,多层for循环都通过dfs()给统一了,就连if语句,mark的归零也都一样。
dfs()下面的那一部分称为回溯,是比较难理解的,但我们如果对照两个程序,首先理解第一个程序3的for循环的嵌套,明白结束for循环后mark归零的意义,那么对于递归的理解,以及对递归后的回溯操作就比较好理解了。
深度优先搜索效率比较低,但有些问题能想到暴力搜索已经很不错了,为了提升效率,我们可以进行剪枝的操作,这一部分我在后续总结,例1实际上还可以使用动态规划,我也在动态规划的章节里给大家分析。
本节其实有两大类问题,排列问题,组合问题,还有一些排列问题与组合问题的变形,如拆分问题,数独问题,n皇后问题,我也在后续帮大家分析。