剖析深度优先搜索(一)理解for循环与递归的关系

  深度优先搜索,是一种搜索方式,它往往树形图紧密联系,因而通过画图可以帮助我们很好的理解搜索过程,同时它也往往通过递归来实现,因而,对递归的理解以及写出恰当的递归对我们来说十分重要
  而对于递归的理解,我认为对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皇后问题,我也在后续帮大家分析。

  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
宽度优先搜索深度优先搜索都是常用的图遍历算法,它们的主要区别在于搜索的顺序和搜索方式。 宽度优先搜索(BFS)的搜索顺序是按照图中结点的层次逐层遍历,即从起点开始,先访问与起点相邻的结点,再访问与这些结点相邻的结点,以此类推,直到找到目标结点或者遍历完整个图。BFS 通常使用队列来实现。 深度优先搜索(DFS)的搜索顺序是从起点开始,沿着一条路径一直向下搜索,直到到达最深处,然后返回上一层,继续搜索下一条路径,直到找到目标结点或者遍历完整个图。DFS 通常使用归或者栈来实现。 下面是宽度优先搜索深度优先搜索的具体特点: 宽度优先搜索: - 搜索顺序按照层次逐层遍历,因此找到的路径一定是最短路径。 - 可以找到任意两个结点之间的最短路径。 - 在搜索过程中,需要存储当前层的所有结点,因此需要更多的内存空间。 - 容易陷入死循环,需要判断结点是否已经被访问过。 深度优先搜索: - 搜索顺序沿着一条路径一直向下搜索,因此找到的路径不一定是最短路径。 - 可以找到一条路径上的所有结点。 - 在搜索过程中,只需要存储当前路径上的结点,因此需要较少的内存空间。 - 可能会陷入无限归,需要判断结点是否已经被访问过。 因此,在实际应用中,我们需要根据具体问题来选择使用 BFS 还是 DFS。如果我们需要找到任意两个结点之间的最短路径或者需要遍历整个图,那么我们可以选择使用 BFS。如果我们只需要找到一条路径,或者需要在搜索过程中尽可能节省内存空间,那么我们可以选择使用 DFS。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值