DFS,深度优先搜索.在我的理解里,是优先将一条路径搜索完,再一步步退回去将所有的可能性都遍历.可以举一个二叉树的遍历的例子:
如图是一颗二叉树
(画的有点丑qwq)
按照DFS,我们从1出发,它有两种选择,可以是2和3,我们先选择2,同理,到下一层先选择4,这时候第一条路已经走完为:1->2->4,这个时候已经到底了,这个时候就可以返回到上一层,选择除了4的另外的可能性,这时候路径就是1->2->5,这就是第二条路径.再返回上一层会发现从2出发已经没有下一种可能了,那就尝试返回上一层,到达1,此时2已经被搜索过了,我们就搜索选择除了2以外的所有可能,则选择3,同理,选择6了再返回3选择7,就得出1->3->6,1->3->7两条路,这个时候一直返回,发现已经将根节点1以下所有可能遍历过了,没有路可走,就直接return结束程序.
这就是dfs的基本思想,不断的搜索,返回,知道找到自己的目标或者将所有路都遍历完.当然,用代码实现这个程序会出现两个难点:1.如何做到遍历到没有下一条路后进行返回.2.怎么做到不会重复遍历已经走过的路.解决这两个问题方法的分别是--递归与数组标记.递归我之前写过博文介绍过,将大问题分解为小问题解决.而标记就是另外用一个数组或者其他东西表示的元素的状态,通过对元素状态的判断决定是否进行搜索,最简单的就是保存0,1,用0表示未被访问,1表示已经被访问.下面我们根据例题进行讲解:
问题:
输入一个小于6的数,输出从1到n的所有排列.
输入:
3
输出:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
该问题就是一个典型的dfs问题,我先将代码展示再详细讲解:
#include<iostream>
#include<cstring>
using namespace std;
int n;
int vis[10],num[10];
void dfs(int step){
if(step>n){
for(int i=1;i<=n;i++){
printf("%d ",num[i]);
}
printf("\n");
return ;
}//退出条件
for(int i=1;i<=n;i++){
if(vis[i]==0){
vis[i]=1;
num[step]=i;
dfs(step+1);
vis[i]=0;//难点:回溯
}
}
return;
}
int main(){
while(scanf("%d",&n)==1){
memset(vis,0,sizeof(vis));
dfs(1);
}
}
dfs的核心思想,就是递归,用递归在求出每一条路后返回上一层,在进行下一种可能的遍历.基本的dfs都是先写出口条件,在进行所有可能的遍历,若是未被标记,就先进行标记,数据元素储存等操作,再递归到下一层.当递归完,再回溯,如上文代码,回溯就是将你本次访问过的元素状态转换为未被访问,因为你在下一层的时候可能需要访问这个元素,如果还是表示他被访问了,那么就不会再次访问,也就会漏掉一些可能性,所以需要回溯.
那么将上文dfs函数代码从头到尾讲解一下;
首先就是退出条件,为什么是当step>n,要搞清楚step是什么,step是目前这个排列已经拥有了多少数字,而n表示一共要多少数字组成一种排列,当step>n时,说明已经排列完了,那么我们就可以输出这个排列了,这个时候就会用到num数组,我用它来储存这些排列.这里return直接退出step+1这一层,返回上一层继续递归.
再就是for循环中的代码,这个循环本身就是配合if以及vis函数来将所有可以选取进行排列的数字元素列举出了进行选择.for加上if这一段用汉语翻译过来就是如果从1到n这些数字元素没有被标记的话就进行以下操作.首先将它标记,防止在下一层递归的时候重复使用这个元素,再就是num[step]=i将选取的数字i储存在num数组中,在递归到下一层.接下来就是不断重复搜索递归,返回,当再次返回到这一层,它就会运行回溯操作vis[i]=0,这一步就表明我们放弃选择i作为这一层的数据元素,取消标记,选择另外的数据元素,但是在之后的递归时可以选择这个i.
这就是dfs的基本思想,dfs和bfs一样,基本题目都可以按照模板来进行解题,但是要注意的是每个题的标记,回溯等地方不尽相同,要按照题目本身而定,不能一味生搬硬套.
下面就再看一个经典的题目--八皇后,基本思想一致,但是标记方法回溯等不尽相同:
#include<iostream>//八皇后
using namespace std;
int place[14]={0}; //记录每种情况
int flag[14]={0};//记录每行是否选择
int sx[50]={0};//上对角线记录
int xx[50]={0};//下对角线 记录
int sum=0;//种类记录
void queen(int n,int q);//递归
void print(int n);
int main(){
int n,q=0;
cin>>n;
queen(n,q);//q记录列数 ,n为棋盘大小
cout<<sum;
}
void queen(int n,int q){
int qwq;
for(qwq=0;qwq<n;qwq++){// 遍历每一行的列数
if(flag[qwq]==0&&sx[q+qwq]==0&&xx[q-qwq+14]==0){//判断是否可以选择
place[q]=qwq;// 记录行数
flag[qwq]=1;// 标记不可选的位置
sx[q+qwq]=1;// 标记不可选的位置
xx[q-qwq+14]=1;// 标记不可选的位置
if(q<n-1)//没有选满的情况下,进行下一步的选择
queen(n,q+1);
else
print(n);//选满了就输出,
flag[qwq]=0;
sx[q+qwq]=0;
xx[q-qwq+14]=0; //清零,进行下一个可选的qwq行数
}
}//如果选完,递归,回到q-1的上一层,到下一个可选点
}
void print(int n){
sum+=1;
if(sum<=3){
for(int i=0;i<n;i++)cout<<place[i]+1<<" ";
cout<<endl;
}
}
行和列的标记很好理解,但是这个对角线的标记有点难想到,可以一个个标记,但是很麻烦,上文就是找规律:从左上往右下斜的这些对角线他们横纵坐标之差绝对值相等,如下图:(i,j分别为横纵坐标)
而从右上往左下斜的对角线横纵坐标之和相同如下图:
就可以用这些差的绝对值,和来进行标记,当然,因为采用了多个数组进行标记,所以回溯时要注意都回溯,回溯本质就是返回上一层,一定要记得.
八皇后的其余步骤其实和上面的那个题大致相同,理解后基本的dfs都可以解出来.qwq