目录
基本概念
实现方法
算法思想
例一 全排列问题
例二 n皇后问题
例三 01背包问题
深度优先搜索算法(Depth First Search,简称DFS):一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过或者在搜寻时结点不满足条件,搜索将回溯到发现节点v的那条边的起始节点。整个进程反复进行直到所有节点都被访问为止。
设想我们处在一个迷宫的入口,从起点开始行进,当遇到岔道口的时候我们总是会选择其中一条继续向前行进(比如总是选择最右边的岔道),当遇到死胡同时,回退到最近的岔路口选择另一条岔路。当碰到岔道口时,我们以“深度”作为关键词,不碰南墙不回头。这就是“深度优先搜索”。
从迷宫的例子还可以看出,深度优先搜索会走遍所有路径,每次碰到死胡同就代表一条完整路径的形成。也就是说,深度优先搜索是一种枚举所有完整路径以遍历所有情况的搜索方法。
由于我们不断的要探索-回溯,相当于入栈-出栈的操作,所以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)
恢复初始状态(回溯的时候需要)
}
}
}
843. n-皇后问题 - AcWing题库
n−皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
现在给定整数 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背包问题。下面我们将通过一道背包问题来展示“剪枝”的思想。
有 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;
}