dfs——深度优先搜索
前言:
介绍: 深度优先搜索,又称dfs,是一种图的搜索方法。算法的基本思想是:从图的某一个节点出发,沿着某一条路径一直往下搜索,直至该条路径的所有节点均被访问,则向上回溯,寻找没有被访问的节点。这个算法的思想有点类似于我们之前学过的暴力求解法,即枚举问题的所有可能的情况,对每一种情况进行讨论,然后找到符合问题的解。
算法优点: 思维量小,不需要费脑筋。也不挑题型,几乎所有的题型均可用dfs(但是有些题并不适用dfs)。申明一下,可用和适用是两回事。
算法缺点: 时间复杂度较高,只适用于问题规模较小的问题。对于输入规模较大的问题,dfs算法将不再适用。(这里的不适用,并不是不能求解,而是求解问题所需要的时间太久)。
为什么要学? 深度优先搜索虽然是图的一种搜索方式,但是它的算法思想常常被我们用来解决生活中一些其他的问题。例如,一些问题的解没有规律,需要讨论问题的每一种情况来求解,如果使用其他算法,问题的解的准确率得不到保证。这时候我们就可以采用dfs。
入门案例——数字排列和组合
问题描述: 现有五张卡牌,上面分别有数字1、2、3、4、5。让你从从中选出三张卡牌,组成一个三位数,且每个三位数的大小不同,问你能组成多少个三位数?
输出示例:
1 2 3
1 2 4
1 2 5
1 3 2
1 3 4
1 3 5
1 4 2
1 4 3
1 4 5
1 5 2
1 5 3
1 5 4
2 1 3
2 1 4
2 1 5
2 3 1
2 3 4
2 3 5
2 4 1
2 4 3
2 4 5
2 5 1
2 5 3
2 5 4
3 1 2
3 1 4
3 1 5
3 2 1
3 2 4
3 2 5
3 4 1
3 4 2
3 4 5
3 5 1
3 5 2
3 5 4
4 1 2
4 1 3
4 1 5
4 2 1
4 2 3
4 2 5
4 3 1
4 3 2
4 3 5
4 5 1
4 5 2
4 5 3
5 1 2
5 1 3
5 1 4
5 2 1
5 2 3
5 2 4
5 3 1
5 3 2
5 3 4
5 4 1
5 4 2
5 4 3
代码示例:
#include<bits/stdc++.h>
using namespace std;
int visit[6];//标识数组,若数字 i 已经被选择了,则arr[i]=1,反之,arr[i]=0
int arr[4];// 用存放每次找到三位数的结果 ,arr[1]、arr[2]、arr[3]表示三个盒子
void dfs(int num)
int main(){
dfs(1);// 先找一号盒子中的数字
return 0;
}
void dfs(int num){
if(num>3){
// 打印结果
for(int i=1;i<=3;i++){
cout<<setw(3)<<arr[i];
}
cout<<endl;
return ;
}
for(int i=1;i<=5;i++){
// 先判断数字 i 是否被已经被选择了
if(!visit[i]){
// 如果没被选择,将数字 i 标识为已选择,即visit[i]=1
visit[i]=1;
// 把数字 i 存进结果数组arr中
arr[num]=i;
// 寻找下一个盒子中的数字
dfs(num+1);
// 回溯
visit[i]=0;
}
}
}
思路分析:
需要从五张卡牌中选出三张,我们不妨假设有三个盒子来存放这三张卡牌。先找一张卡牌放入①号盒子,然后再找②盒子中的卡牌,再找③号盒子中的卡牌。如图所示,以①号盒子中放的卡牌号码是 1 为例:
假设①号盒子为1,②号盒子为2,当找到③号盒子的中一个解时,我们就可以回溯到②号盒子,重新来找满足③号盒子的解。同理,当找完所有满足③号盒子的解时,我们可以回溯到①号盒子,来找另一个满足②号盒子的解,以此类推,找出满足条件的每一种情况。
提升案例——n皇后问题
问题描述: 在n*n的方格棋盘中,放置n个皇后,使得它们不能相互攻击(即任意2个皇后不能在同一行、同一列,并且不能在一条对角线上),对于给定的n,请问有多少种放置方法?
输入格式: 一个正整数n,棋盘的大小,皇后的个数
输出格式: 一个正整数,放置方法的数量
输入示例:
5
输出示例:
10
代码示例:
#include<bits/stdc++.h>
using namespace std;
int n;// 棋盘的规模,也是皇后的个数
int visit[11][11];// 标识数组,如果棋盘的第i行,第j列放置了皇后,则visit[i][j]=1,反之visit[i][j]=0;
void dfs(int line,int num);
int sum=0; // 放置的方法的数量
bool checkout(int x,int y);// 检验函数,判断该位置是否能放置皇后
int main(){
cin>>n;
dfs(1,0);
cout<<sum<<endl;
return 0;
}
void dfs(int line,int num){ // line为行数,num为已经放置皇后的个数
if(line>n){
// 因为需要放置n个皇后,所以当放置了n个皇后后,就找到了一种放置方法
if(num==n){
sum+=1;
}
num=0;
return ;
}
// 从第一行开始,一行放置一个皇后
for(int i=1;i<=n;i++){
if(checkout(line,i)){
visit[line][i]=1;
num+=1;
dfs(line+1,num);
num-=1;
visit[line][i]=0;
}
}
}
bool checkout(int x,int y){
bool flag=true;
for(int i=1;i<=n;i++){
// 如果该位置的同一行或者同一列有放置皇后,则该位置不能放置皇后,将flag赋值为false
if(visit[x][i]==1||visit[i][y]==1){
flag=false;
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
// 如果该位置的对角线上有放置皇后,则该位置不能再放置皇后
// 利用斜率来判断是否在对角线上,在对角线上斜率必然相同
if(abs(i-x)==abs(j-y)&&visit[i][j]==1){
flag=false;
}
}
}
return flag;
}
思路分析:
每一行必须放置一个皇后,我们可以从第一行开始,先放置第一行的皇后,然后以此类推放置每一行的皇后。在放置每一行的皇后时,从第一列开始判断,如果满足条件就放置皇后,将该位置的标记为已访问,即visit [i] [j] =1。当准备放置皇后且行数为n时,并且放置皇后后,如果已经放置了的皇后的个数为n,则放置皇后的方法数加1。
基本思想就是判断每一行的每一列,如果该位置满足条件,就放置皇后。
不同行、不同列,只要满足不在同一行、同一列就行。不在对角线上,可以通过判断其斜率是否相同,若斜率相同,则必然处于对角线上。
下面是4行4列的表格:0为皇后放置的位置
1 | 2 | 3 | 4 | |
---|---|---|---|---|
1 | 0 | |||
2 | 0 | |||
3 | 0 | |||
4 | 0 |
综合案例——迷宫问题
问题描述: 给定一个 n * m 大小的迷宫,其中迷宫位置上为 1 表示可以通行,为 0 表示不能通行。迷宫的入口坐标为(0,0),给定出口坐标,只能横向移动和纵向移动,不能斜着移动。求入口坐标到出口坐标的最少步骤。(其中 n和m 小于等于10)
输入格式: 正整数n和m,n为迷宫的行数,m为迷宫的列数。输入n行数据,每行有m个数字(0或者1)。最后一行输入迷宫出口坐标a,b。
输出格式: 一个正整数,表示最少的步骤数
输入示例:
5 5
1 1 0 0 1
1 1 1 1 1
1 1 0 0 1
1 1 0 1 1
1 1 1 1 0
3 3
输出示例:
8
代码示例:
#include<bits/stdc++.h>
using namespace std;
int n,m;// n为迷宫的行数,m为迷宫的列数
int a,b;// a,b为迷宫出口的坐标
int arr[10][10]; // arr 来存放迷宫的大小
int visit[10][10];// 标识数组,如果该点被访问过,则visit[i][j]=1
int sum=100000;// sum为到出口的步骤数,因为需要找最小的步骤数,所以初始值给大一点
// 方向向量dx、dy,分别表示在x轴和y轴上的偏移量
// 例如向右移动,则x+1,y不变,用方向向量表示为x+dx[0]、y+dy[0]
int dx[4]={1,0,-1,0};
int dy[4]={0,1,0,-1};
void dfs(int x,int y,int num);// x,y为当前位置的坐标,num为从入口到当前位置的步骤数
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
cin>>arr[i][j];
}
}
cin>>a>>b;
// 出口位置必然是可以通行的,arr[a][b]=1
// 这里为什么需要再次赋值,这是因为我出题不严谨所导致的 ,这个影响不大
arr[a][b]=1;
dfs(0,0,0);
cout<<sum<<endl;
}
void dfs(int x,int y,int num){
if(x==a&&y==b){
if(num<sum){
sum=num;
}
num=0;
return ;
}
// 迷宫问题一般需要确定一个方向,根据一个方向依次向前尝试
// 这里我规定的是顺时针方向
for(int i=0;i<4;i++){
int l=x+dx[i];
int c=y+dy[i];
// 判断一下,数组下标是否越界,如果越界则跳过
if((l<0||l>n)&&(c<0||c>m)){
continue;
}
if(arr[l][c]==1){
if(!visit[l][c]){
visit[l][c]=1;
num+=1;
dfs(l,c,num);
// 这里注意一下,回溯的方法
// 因为之前找到了这个位置, 步骤数num加了1,所以回溯的时候步骤数num-1
num-=1;
visit[l][c]=0;
}
}
}
}
思路分析:
迷宫问题首先需要确定一个遍历方向,因为题目规定只能横向或纵向移动,所以只有上、下、左、右四个方向可以选择。根据确定好的方向依次进行遍历判断。本题中,我是根据顺时针方向进行判断,先向右,再向下,再向左,最后再向上移动。到达一个位置,先判断其右方向是否可以通行,如果可以通行,则移动到该位置上,然后依次判断其他方向。如果碰到了各个方向不能通行的位置,则向后回溯,寻找其他可以通行的路径。
输入示例迷宫如图所示:
入口 | 1 | 0 | 0 | 1 |
---|---|---|---|---|
1 | 1 | 1 | 1 | 1 |
1 | 1 | 0 | 0 | 1 |
1 | 1 | 0 | 出口 | 1 |
1 | 1 | 1 | 1 | 0 |
结束语:
dfs ——万能解题法,它可以解决我们遇到的绝大多数问题,但是它的时间复杂度较高,所以我们应用时,要注意dfs的剪枝,提高算法效率。上面的案例中没有涉及到剪枝的操作,不同题型的剪枝方法有所不同,具体题型具体分析。剪枝可以减少多余情况的遍历,从而提高算法的效率。