DFS(深度优先搜索)入门介绍

目录

基本概念

实现方法

算法思想

例一 全排列问题

例二 n皇后问题

例三 01背包问题


基本概念

深度优先搜索算法(Depth First Search,简称DFS):一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过或者在搜寻时结点不满足条件,搜索将回溯到发现节点v的那条边的起始节点。整个进程反复进行直到所有节点都被访问为止。

设想我们处在一个迷宫的入口,从起点开始行进,当遇到岔道口的时候我们总是会选择其中一条继续向前行进(比如总是选择最右边的岔道),当遇到死胡同时,回退到最近的岔路口选择另一条岔路。当碰到岔道口时,我们以“深度”作为关键词,不碰南墙不回头。这就是“深度优先搜索”。

从迷宫的例子还可以看出,深度优先搜索会走遍所有路径,每次碰到死胡同就代表一条完整路径的形成。也就是说,深度优先搜索是一种枚举所有完整路径以遍历所有情况的搜索方法

DFS(深度优先搜索)入门介绍_第1张图片

实现方法

由于我们不断的要探索-回溯,相当于入栈-出栈的操作,所以DFS对应的数据结构是栈。当然,由于用栈实现把不轻松,所以我们往往会选择递归来实现(使用递归时,系统会使用系统栈来存放递归中每一层的状态,因此递归实现DFS的本质还是栈)。

算法思想

回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。 回溯相当于递归中的“剪枝”,意思是对于已经知道错误的路径没有必要再探索下去。比如在一个有序数列1 2 3 4 5中寻找和为5的所有集合,当搜索进行到3的时候显然就没有必要继续往下走了,因此结束这段搜索。这是对搜索过程的一种优化。

来看例题:

例一 全排列问题

活动 - AcWing

给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。

现在,请你按照字典序将所有的排列方法输出。

输入格式

共一行,包含一个整数 n。

输出格式

按字典序输出所有排列方案,每个方案占一行。

数据范围

1≤n≤7

输入样例:

3

输出样例:

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
#include 
using namespace std;
const int N = 10;
int n;
int path[N]; //path为当前排列 
bool st[N];  //st来记录整数x是否已经在当前path中 
void DFS(int u){
	if(u == n){ //搜索边界,已经处理完排列的1~n位 
		for(int i = 0; i < n; i ++)
			printf("%d ", path[i]);
		puts("");
		return;
	}
	for(int i = 1; i <= n; i ++){ //枚举1~n,试图将x填入path[u] 
		if(!st[i]){ //st[i] == 0代表i不在当前排列中 
			path[u] = i; //于是将i加入当前排列 
			st[i] = true; //记录:i已经在当前排列中 
			DFS(u + 1); //继续深入搜索,处理path[u+1] 
			st[i] = false; //搜索完后还原现场 
		}
	}
}
int main(){
	scanf("%d", &n);
	DFS(0);
	
	return 0;
}

说个题外话,如果你熟悉STL,可以尝试如下解法:

#include 
#include 
using namespace std;
int main(){
	int a[10];
	int n;
	cin >> n;
	for(int i = 1; i <= 10; i ++)
		a[i] = i;
	do{
		for(int i = 1; i <= n; i ++)
			cout << a[i] << " ";
		puts("");
	}while(next_permutation(a + 1, a + n + 1));
	
	return 0;
} 

第一种解法即是我们的深搜算法,从中可以得出DFS的基本模板:

void DFS(int step){
	if(满足边界条件){
		相应操作 
	}
	for(尝试每种可能){
		if(满足条件){
			标记
			继续下一步DFS(step + 1)
			恢复初始状态(回溯的时候需要) 
		}
	} 
}

例二 n皇后问题

843. n-皇后问题 - AcWing题库

n−皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。

DFS(深度优先搜索)入门介绍_第2张图片

现在给定整数 n,请你输出所有的满足条件的棋子摆法。

输入格式

共一行,包含整数 n。

输出格式

每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。

其中 . 表示某一个位置的方格状态为空,Q 表示某一个位置的方格上摆着皇后。

每个方案输出完成后,输出一个空行。

注意:行末不能有多余空格。

输出方案的顺序任意,只要不重复且没有遗漏即可。

数据范围

1≤n≤9

输入样例:

4

输出样例:

.Q..
...Q
Q...
..Q.

..Q.
Q...
...Q
.Q..

 对于这个问题,如果采用组合数的方式来暴力枚举(从n^2个位置中选择n个位置),当n = 8时就是54 502 232 次枚举,n较大时显然是无法承受的。

但换个思路,我们可以按行展开搜索,因为每行只允许有一个皇后,对于每行都找出一个合法的位置,直到组成一个答案

代码如下:

#include 
using namespace std;
const int N = 21;
int n;
char g[N][N]; //模拟出一个棋盘 
bool col[N], dg[N], udg[N];
//用于判断该列、对角线、反对角线有无皇后存在 
void DFS(int u){ //处理第u行皇后的放法 
	if(u == n){ //到达搜索边界,返回 
		for(int i = 0; i < n; i ++)
			puts(g[i]);
		puts("");
		return;
	} 
	for(int i = 0; i < n; i ++){ //遍历第u行每个格子 
		if(!col[i] && !dg[u + i] && !udg[n - u + i]){ //该位置合法 
			g[u][i] = 'Q'; //将该位置标记 
			col[i] = dg[u + i] = udg[n - u + i] = true; //将该位置对应列、对角线标记 
			DFS(u + 1); //搜索下一行 
			col[i] = dg[u + i] = udg[n- u + i] = false; //恢复初始状态 
			g[u][i] = '.'; //恢复初始状态 
		}
	}
}
int main(){
	cin >> n;
	for(int i = 0; i < n; i ++) //初始化 
		for(int j = 0; j < n; j ++)
			g[i][j] = '.';
	DFS(0);
	
	return 0;
}

当然,也可以按每个格子“放”与“不放”皇后来进行搜索。请读者自行思考,这里直接给出代码:

#include 
using namespace std;
const int N = 20;
int n;
char g[N][N];
bool row[N], col[N], dg[N], udg[N];
void DFS(int x, int y, int s){
	if (y == n) y = 0, x ++;
	if (x == n){
		if(s == n){
			for(int i = 0; i < n; i ++)
				puts(g[i]);
			puts("");
		}
		return;
	}
	//不放皇后 
	DFS(x, y+1, s);
	//放皇后 
	if(!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n]){
		g[x][y] = 'Q';
		row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;
		DFS(x, y + 1, s + 1);
		row[x] = col[y] = dg[x + y] = udg[x - y + n] = false;
		g[x][y] = '.';
	}
}
int main(){
	cin>>n;
	for(int i = 0; i < n; i ++){
		for(int j = 0; j < n; j ++){
			g[i][j] = '.';
		}
	}
	DFS(0, 0, 0);
	
	return 0;
}

对于上面这种“每个格子选或不选两种选择”,有点像01背包问题。下面我们将通过一道背包问题来展示“剪枝”的思想。

例三 01背包问题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

 (注意:本题只用作算法展示,在数据量过大的情况下请使用动态规划求解)

解一

看到问题我们很容易想到深搜的写法,本题的岔路就是——每件物品装与不装。

#include 
using namespace std;
const int maxn = 1010;
int N, V, v[maxn], w[maxn];
int maxValue = 0; //所求最大价值 
void DFS(int u, int sumV, int sumW){ //sumV是当前装入物品体积,sumC是当前物品价值 
	if(u == N){ //到达递归边界 
		if(sumV <= V && sumW > maxValue)
			maxValue = sumW;
		return;
	}
	DFS(u + 1, sumV, sumW); //不装 
	DFS(u + 1, sumV + v[u], sumW + w[u]); //装 
}
int main(){
	cin >> N >> V;
	for(int i = 0; i < N; i ++)
		cin >> v[i] >> w[i];
	DFS(0, 0, 0);
	cout << maxValue;
	return 0;
}

这样的代码交上去会很轻松的TLE(超过时间限制),因为没有考虑到在没有搜索完N的情况下还有一种可以返回的可能——背包里的物品已经到达背包最大容量。在这种情况下,后面的物品我们就无需再考虑,就像放弃一些已经知道是枯萎的树枝分岔一样,称为剪枝。

附上剪枝后的代码:

#include 
using namespace std;
const int maxn = 1010;
int N, V, v[maxn], w[maxn];
int maxValue = 0;
void DFS(int u, int sumV, int sumW){
	if(u == N){
		if(sumV <= V && sumW > maxValue)
			maxValue = sumW;
		return;
	}
	DFS(u + 1, sumV, sumW);
	if(sumV + v[u] <= V){ //剪枝操作 
		if(sumW + w[u] > maxValue)
			maxValue = sumW + w[u];
		DFS(u + 1, sumV + v[u], sumW + w[u]);
	}
}
int main(){
	cin >> N >> V;
	for(int i = 0; i < N; i ++)
		cin >> v[i] >> w[i];
	DFS(0, 0, 0);
	cout << maxValue;
	return 0;
}

你可能感兴趣的:(DFS(深度优先搜索),深度优先,算法,leetcode,c++)