深度优先搜索

深度优先搜索,简称dfs。我们可以将它跟递归联合在一起。


dfs与递归

先回顾一下递归。

我们使用递归完成斐波那契数列的计算:


int fib(int n){
	if(n == 1 || n == 2){
		return 1;	
	}
	return fib(n - 1) + fib(n - 2);
}


以上递归实现斐波那契实际上就是按照深度优先的方式进行搜索。也就是 “一条路走到黑” 。注意:这里的搜索指的是一种穷举方式,把可行的方案都列举出来,不断尝试,直到找到问题的解。


python深度优先算法实验 深度优先算法递归_搜索


以上即为Fib(5)的计算过程,我们发现实际上对应着一棵树,这棵树被称为搜索树

深度优先搜索与递归的区别:

  1. 深度优先搜索是一种算法,更注重思想。
  2. 递归是一种基于编程语言的实现方式。
  3. 深度优先搜索可以使用递归实现!当然也就存在非递归的的方式实现搜索。

模板

参考了一个大神,模板原文链接如下:,这里总结一下。


深度优先搜索适合解决必须走到 最深处(例如对于树,须走到它的叶子节点) 才能得到一个解的问题


每次递归开始的时候要判断是否达到收敛条件,若达到了则得到一个可行解,若没达到,则对当前状态进行扩展(扩展的时候通常会根据实际情况过滤掉一些非法的状态,这个过程叫 剪枝,适当的剪枝有时能极大地提高搜索的速度),如果要求输出具体解,则此时应该保存该状态,当扩展结束后,需要释放这个保存的状态。下面是一个深度优先搜索的编程模板:



/**
* dfs 模板.
* @param[in] input 输入数据指针
* @param[inout] cur or gap 标记当前位置或距离目标的距离
* @param[out] path 当前路径,也是中间结果
* @param[out] result 存放最终结果
* @return 路径长度,如果是求路径本身,则不需要返回长度
*/
void dfs(type *input, type *path, int cur or gap, type *result) {
  if (数据非法) return 0; // 终止条件
  if (cur == input.size( or gap == 0)) { // 收敛条件
    将 path 放入 result
  }
  if (可以剪枝) return;
  for(...) { // 执行所有可能的扩展动作
     执行动作,修改 path
     dfs(input, step + 1 or gap--, result);
     恢复 path
  }
}



模板应用(经典例题)

N皇后问题

这个题刚接触一定难倒不少人,但真的希望大家别怕,别去逃避,完完全全啃完这篇文章,这题算是秒杀了。

在一个 N × N 的棋盘上放置 N 个皇后,每行刚好放置一个并使其不相互攻击(同一行、同一列、同一斜线上的皇后都是自动攻击),求一共有多少种合法的方法放置 N 个皇后

输入格式:输入一个整数N,代表N个皇后。

输出格式:输出一个整数,表示有多少种放置皇后方法。


python深度优先算法实验 深度优先算法递归_搜索_02


我们采用逐行放置,当行数为N+1行时,即为终止条件

那我们如何判断某个位置是否能放置皇后呢?因为我们是逐行放置的,因此在行内是不会产生冲突的,于是我们采用三个数组来记录位置是否可用,分别对应了棋盘的  列、对角线1、对角线2 ,若可用,则标记为0,若不可用,则标记为1。最初的情况是棋盘上所有位置都是可用的,但在我们搜索的过程中,我们每次放置一个皇后的时候就需要占用一个位置,如果我们不对该位置进行标记(标记为已占有),则在下一步放置的过程中就有可能出现违规的情况(同一列、同一斜线)。解释这么多,就是想说明我们在迭代前,一定要修改 列、对角线1、对角线2 的标志!

需要注意的是,列我们可以通过坐标表示,那对角线如何表示呢?


python深度优先算法实验 深度优先算法递归_算法_03


于是,对于对角线1(从右上到左下),我们可以表示为 (行 + 列);同理,对于对角线2(从左上到右下)可以表示为(行 - 列 + n)。

于是我们可以写出代码:


//8皇后
#include<bits/stdc++.h>
using namespace std;
int n, ans;
bool vy[1005], vd1[1005], vd2[1005];
bool check(int x, int i){
	return !vy[i] && !vd1[x + i] && !vd2[x - i + n];
}
void dfs(int x){
	if(x == n){
		//从0行开始的,所以当 x = n 时候就结束 
		ans++;
		return; 
	}
	for(int i = 0; i < n; i++){
		//每一行内,对列情况讨论(每一个小 path) 
		if(check(x, i)){
			//如果可以放置
			vy[i] = true; 
			vd1[x + i] = true;
			vd2[x - i + n] = true;
			dfs(x + 1); //进行下一行搜素
			//搜索完成后需要释放掉 
			vd1[x + i] = false;
			vd2[x - i + n] = false;
			vy[i] = false; 
		}
	}
}
int main(){
	scanf("%d", &n);
	dfs(0);
	printf("%d", ans);
	return 0;
}

黑白N皇后问题

通过对N皇后的理解,我们对深度优先搜索的讨论以及有一定理解了,接下来看看两个皇后同时摆放的情况。

给定一个n × n 的棋盘,棋盘中有一些位置不能放皇后。现在要向棋盘中放入 n 个黑皇后和 n 个白皇后,使任意的两个黑皇后都不在同一行、同一列或同一斜线(正负斜线)上,任意两个白皇后都不在同一行、同一列或同一斜线(正负斜线)上。问共有多少种放法?n 小于等于8。

输入格式:输入第一行为一个整数 n,表示棋盘大小。接下来 n 行,每行 n 个 0 或 1 的整数,如果一个整数为1,表示对应的位置可以放皇后。如果一个整数为0,表示对应的位置不可以放皇后。

输出格式:输出一个整数,表示总共有多少种方法。

python深度优先算法实验 深度优先算法递归_搜索_04

黑白N皇后问题实际上跟N皇后很相似,唯一不同的就是多加入了一个皇后。因此我们仅需按顺序解决即可。我们先统一解决黑皇后,然后再解决白皇后。又因为存在有些位置这不能放置的情况,因此在前面八皇后的三个用于判断是否能放置的数组需要进行修改。修改情况如下:

  • 0:表示黑、白皇后都能放置
  • 1:表示黑皇后不能放置
  • 2:表示白皇后不能放置
  • 3:表示黑、白皇后都不能放置

并且,我们对dfs函数需要做出调整。多加入一个参数,用于标记对黑皇后操作,还是对白皇后操作。

//黑白
#include<bits/stdc++.h>
using namespace std;
int n, ans; 
int mp[10][10]; //保存棋盘 
int vy[10], vd1[20], vd2[20]; //判断 列、正负对角线是否能放置。 
bool check(int row, int x, int p){
	return mp[row][x] && vy[x] != 3 && vy[x] != p && vd1[row + x] != 3 && vd1[row + x] != p && vd2[row - x + n] != 3 && vd2[row - x + n] != p;
}
void dfs(int row, int p){
	if(row == n && p == 2){
		//中止条件
		ans++;
		return;
	}
	if(row == n){
		//收敛条件,此时黑皇后摆放完成,开始摆放白皇后
		dfs(0, p + 1); 
	}
	for(int i = 0; i < n; i++){
		//对一个行中的每一列讨论
		if(check(row, i, p)){
			//如果p皇后能在这里放置
			mp[row][i] = 0;
			vy[i] += p; 
			vd1[row + i] += p;
			vd2[row - i + n] += p;
			dfs(row + 1, p); //进行下一行遍历,注意:这里的p并没有变化
			//结束后要释放标记位
			vy[i] -= p;
			vd1[row + i] -= p;
			vd2[row - i + n] -= p;
			mp[row][i] = 1;
		} 
	}
}
int main(){
	scanf("%d", &n);
	for(int i = 0; i < n; i++){
		for(int j = 0; j < n; j++){
			scanf("%d", &mp[i][j]);
		}
	}
	dfs(0, 1); //从第0行,黑皇后开始 
	printf("%d", ans); 
	return 0;
}

木棍拼三角形问题

小A 手上有一些小木棍,它们长短不一,小A 想用这些木棍拼出一个等边三角形.并且每根木棍都要用到。例如,小A 手上有长度为 1 , 2 ,3 ,3 的 4 根木棍.他可以让长度为 1 , 2 的木棍组成一条边.另外 2 跟分别组成 2 条边,拼成一个边长为 3 的等边三角形。蒜头君希望你提前告诉他能不能拼出来,免得白费功夫。
输入格式:首先输入一个整数n。( 3 <=n <=20 ) ,表示木数量,接下来输入 n 根木棍的长度p( 1 <=p<= 10000 )。
输出格式:如果小A 能拼出等边三角形,输出” yes " ,否则输出“no”。

python深度优先算法实验 深度优先算法递归_c算法_05



由于是等边三角形,因此边长可以一下就判断出来(sum / 3)。接下来,我们逐条边进行拼凑。完成一条边后再进行下一条边的寻找。终止条件是完成三条边的拼凑,因此我们在递归中需要一个参数来记录完成的边数,同时还需要一个参数来记录我们目前的边长和(用来匹配边长),还需要一个参数用来进行索引的推移。

//木棍
#include<bits/stdc++.h>
using namespace std;
int t[10010];
bool vs[10010];
bool f;
int n;
int sum = 0;
void dfs(int cnt, int tot, int index){
	if(f) return;
	if(cnt == 3){
		//完成拼凑 
		f = true;
		return;
	}
	if(tot == sum / 3){
		//完成了一条边,进行下一条 
		dfs(cnt + 1, 0, 0); 
	}
	for(int i = 0; i < n; i++){
		if(!vs[i]){
			//说明可用
			vs[i] = true; 
			dfs(cnt, tot + t[i], i + 1);
			vs[i] = false;
		}
	}
}
int main(){
	scanf("%d", &n);
	for(int i = 0; i < n; i++){
		scanf("%d", &t[i]);
		sum += t[i];
	}
	if(sum % 3 != 0){
		//直接排除
		printf("no\n"); 
		return 0;
	}else{
		dfs(0, 0, 0);
	}
	if(f) printf("yes\n");
	else printf("no\n");
	return 0;
}

数独

是不是已经很有感觉了!其实深度优先搜索的题目并不难,关键找对递归的过程以及终止条件讨论好就行了。常用bool数组来作为访问过的标志。最后练一题。

标准数独是由一个给与了提示数字的 9 × 9 网格组成,我们只需将其空格填上数字,使得每一行,每一列以及每一个 3 × 3 宫都没有重复的数字出现。

输入格式:一个 9 × 9 的数独,数字之间用空格隔开,* 表示需要填写的数字。

输出格式:输出一个 9 × 9 的数独,把输入中的 * 换成需要填写的数字即可。

python深度优先算法实验 深度优先算法递归_python深度优先算法实验_06



//数独
#include<bits/stdc++.h>
char s[10][10];
bool f; //是否找到
//vx[x][y]——x:第 x 行;y:1-9,有哪些被占用
//vy[x][y]——x:第 x 列;y:1-9,有哪些被占用
//vx[x][y]——x:第 x 个 3*3 的格子;y:1-9,有哪些被占用
bool vx[10][10], vy[10][10], vv[10][10]; //行、列、3*3 能填几 
void dfs(int x, int y){
	if(f) return;
	if(x == 9){
		f = true;
		for(int i = 0; i < 9; i++){
			for(int j = 0; j < 9; j++){
				if(j != 8){
					cout << s[i][j] << " ";
				}else{
					cout << s[i][j] << endl;
				}
			}
		}
		return;
	}
	//换行 
	if(y == 9){
		dfs(x + 1, 0);
		return;
	}
	//本来已经有占位的 
	if(s[x][y] != '*'){
		dfs(x, y + 1);
		return;
	}
	//填写的时候 
	for(int i = 1; i <= 9; i++){
		//对填写内容加上限制 
		if(!vx[x][i] || !vy[y][i] || !vv[i / 3 * 3 + j / 3][i]){
			s[x][y] = '0' + i;
			vx[x][i] = true;
			vy[y][i] = true;
			vv[i / 3 * 3 + j / 3][i] = true;
			dfs(x, y + 1);
			vx[x][i] = false;
			vy[y][i] = false;
			vv[i / 3 * 3 + j / 3][i] = false;
			s[x][y] = '*';
		}
	}
}
int main(){
	//input
	for(int i = 0; i < 9; i++){
		for(int j = 0; j < 9; j++){
			cin >> s[i][j];
		}
	}
	for(int i = 0; i < 9; i++){
		for(int j = 0; j < 9; j++){
			if(s[i][j] != '*'){
				vx[i][s[i][j] - '0'] = true;
				vy[j][s[i][j] - '0'] = true;
				vv[i / 3 * 3 + j / 3][s[i][j] - '0'] = true;
			}
		}
	}
	dfs(0, 0);
	return 0;
}

那么到目前为止,我们练习了这么多题目了,是不是挺有感觉的了?下一步,我们来看看如何通过剪枝减少递归次数。


如何剪枝?

剪枝,顾名思义,就是通过一些判断,砍掉搜索树上不必要的子树。我们来看看几种不同的剪枝

可行性剪枝

有时候,我们会发现某个节点对应的子树的状态都不是我们想要的结果,那么我们其实没必要对每个分支都进行搜索,砍掉这个子树。

最优性剪枝

对于求最优解的一类问题,通常可以用最优性剪枝,比如在求迷宫最短路的时候,如果发现当前的步数已经超过了当前最优解,那么从当前状态开始的搜索都是多余的,因为这样搜索下去也找不到最优解了。通过剪枝,可以省去大量冗余的计算。

此外,对于可行性的问题,如前面例题中木棍拼三角形。一旦我们找到了一组可行解,就不再继续进行,这也算最优性剪枝。

重复性剪枝

对于某些特定的搜索方式,一些方案可能会被搜索很多次,这样是没必要的。

如这个问题:

给定 

python深度优先算法实验 深度优先算法递归_c算法_07

 个整数,要求选出 

python深度优先算法实验 深度优先算法递归_c算法_08

 个数,使得选出来的 

python深度优先算法实验 深度优先算法递归_c算法_08

 个数的和为 

python深度优先算法实验 深度优先算法递归_c++_10

那么在这题中,如果搜索方法是每次从剩下的数里面选一个数,一共搜到第 

python深度优先算法实验 深度优先算法递归_c算法_08

 层,那么 1,2, 3这三个选取方法能被搜索到 6 次,这是没必要的。因此我们只关注选出来的数的和,而根本不关注选出来的数的顺序。因此这里可以使用重复性剪枝。

 我们规定选出来的数的位置是递增的,在搜索的时候,用一个参数来记录上一次选区的数的位置,那么此次我们从这个数之后开始选取,这样最后选出来的方案就不会重复了。


来看看例题

引爆炸弹

python深度优先算法实验 深度优先算法递归_搜索_12

我们把能够相互引爆的炸弹放到同一个集合中,那么最终划分出集合的个数就是我们要求的答案。显然,引爆一个集合中任意一个炸弹造成的效果都是一样的,我们可以遍历 

python深度优先算法实验 深度优先算法递归_c算法_07

 × 

python深度优先算法实验 深度优先算法递归_python深度优先算法实验_14

 的方格中所有炸弹,如果当前炸弹没被引爆,那就引爆它,及标记它所在集合中的所有炸弹。

这里有一个剪枝,每行每列最多只搜索一遍,于是可以标记一下搜过的行、列,避免重复性搜索,这样每个格子都最多被检查两遍。

//炸弹
#include<bits/stdc++.h>
using namespace std;
char s[1005][1005];
bool vx[1005], vy[1005]; //表示第n行,和第n列 是否被访问过,因为访问过的话肯定都爆炸了。 
int n, m, cnt;
void dfs(int x, int y){ //相当于进行一次引爆操作。 
	s[x][y] = '0';
	if(!vx[x]){
		vx[x] = true;
		for(int i = 0; i < m; i++){
			if(s[x][i] == '1'){
				dfs(x, i);
			}
		} 
	}
	if(!vy[y]){
		vy[y] = true;
		for(int i = 0; i < m; i++){
			if(s[i][y] == '1'){
				dfs(i, y);
			}
		} 
	}
} 
int main(){
	scanf("%d %d", &n, &m);
	for(int i = 0; i < n; i++){
		scanf("%s", &s[i]);
	}
	for(int i = 0; i < n; i++){
		for(int j = 0; j < n; j++){
			if(s[i][j] == '1'){
				cnt++;
				dfs(i, j);//引爆 
			}
		}
	}
	printf("%d", cnt);
	return 0;
}

那么到这里,其实DFS覆盖的内容就差不多了,剩下的细节还需要大家多多练习,刷题才是硬道理!