深度优先搜索
事实上,深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次.
下面以一个例题来引入深度优先搜索算法。
给定一个整数n,将数字1~n排成一排,将会有很多种排列方法。现在,请你按照字典序将所有的排列方法输出。
输入格式:
一个整数n
输出格式:
按照字典序排列所有排列方案,每个方案占一行
数据范围:
1≤n≤7
输入样例:3
解题思路:
排列数字说白了就是求1~n的全排列,并输出。
那就要一个一个枚举搜索,这里采取dfs回溯的方法,可以简化枚举的时间复杂度
操作如图所示:
我们可以认为1~n这n个数字分别有两种状态:已经被选择和还未被选择。
我们此题想要实现的无非就是选择这1~n个数字,去填入一个盒子里面,问有哪几种选择的先后顺序的方法。
所以刚开始可以选1、2、3.
然后如果第一次选的1,下一次再从1~3遍历一边,发现可以选2或3这两个还没有被选过的,再然后如果第二次选了2,那么第三次再遍历一边发现就只能选3了;同理在第二次如果选的是3,那么第三次遍历一边发现只有2没有被选,就只能选2了。
由此概括,总结一个1~n(n无穷大)的通解
开始:遍历1~n
第一层:在1~n任选一个选出来,标记已被选择
第二层:在第一层基础上再遍历1~n,在没有被选择的再选择一个,标记已选择
第三层:在第二层…
…
第n层:在第n-1层…
结束:发现全部被选完了,开始输出~~
代码如下:
/*
DFS实现排列组合,假设n=3;
1 2 3
2 3 1 3 1 2
3 2 3 1 2 1
*/
#include<iostream>
using namespace std;
int a[10],n,book[10];//a[i]这个数组用来记录已经第i个被选进去的是哪个数字
//book[i]这个数组用来记录数字i是否被选择了
void dfs(int now,int num)//now表示现在选择的是哪个数字,num表示已经选择了多少个数字
{
if(num==n)//已经选择的数已经到n了,说明选完了,开始输出
{
for(int i=1;i<=n;i++)
{
cout<<a[i]<<" ";
}
cout<<endl;
return;//输出完了,结束void函数,直接return,不用执行后面的了
}
for(int i=1;i<=n;i++)//再从1~n遍历
{
if(book[i]==0)//i这个数字没有选过,那就选
{
book[i]=1;
a[++num]=i;//注意是++num而不是num++
//可以改写为num++;a[num]=i;
dfs(i,num);//继续回溯,因为前面num已经++了,这里就直接回溯num就行
book[i]=0;//和前面一样,恢复初值
a[num--]=0;//注意是num--,不能是--num!
}
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)//第一层这是,从1~n,都给他来一遍
{
book[i]=1;//标记i为已选择
a[1]=i;//这是第一个选的数字,所以a[1]=i
dfs(i,1);//开始进入递归函数,回溯dfs~~
book[i]=0;//42-43这两行是很重要的,但是同样也是不太好理解的关键点
a[1]=0;//这里把book和a又重新弄回0了,给恢复原值,为什么呢?最后细讲。
}
return 0;
}
/*
手动模拟上面注释的操作,假设输入的是n=3.执行代码,从1~3遍历,然后
执行dfs(1,1)--->这是第一次选择1的情况
在dfs(1,1)中又开始选择其他的~~~,好,我们现在假设第一次选1的全都执行完了,已经输出完第一次选1的了
那么该回去了,第一次要选择2了,但是注意,book[]和a[]可是有值的,并不是初始化的0噢,所以这就是为什么
我们在执行一次回溯后后面要恢复初值,是为了方便下一次回溯~
*/
为了方便理解,手动模拟n=3的情况如下:
输入n=3->dfs(1,1)->dfs(2,2)->dfs(3,3)->输出“1 2 3”->回溯dfs(2,2)->回溯dfs(1,1)->dfs(3,2)-> dfs(2,3)->输出“1 3 2”->回溯dfs(1,1)->dfs(2,1)->dfs(1,2)->dfs(3,3)->输出“2 1 3”->回溯dfs(1,2)->dfs(3,2)->dfs(1,3)->输出“2 3 1”->回溯dfs(1,1)->dfs(3,1)->dfs
(1,2)->dfs(2,3)->输出“3 1 2”->回溯dfs(1,2)->dfs(2,2)->dfs(1,3)->输出“3 2 1”->结束bingo!
趁热打铁,再来一个n皇后问题
题目描述:
n-皇后问题是指将n个皇后放在n*n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同—列或同一斜线上。
输入格式:
一个整数n
输出格式:
所有满足要求的布局方案,方案之间一个空格间隔开
数据范围:1≤n≤9
输入样例:4
输出样例:
代码如下:
#include<iostream>
using namespace std;
int n,l1[1000],l2[1000],row[1000];//l1---lean1->左上右下对角线(差为0) l2---lean2->左下右上对角线(和为定值) row->列
char map[11][11];
void dfs(int now)
{
if(now==n+1)//满足条件就输出√
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
cout<<map[i][j];
}
cout<<endl;
}
cout<<endl;
return ;
}
for(int i=1;i<=n;i++)//i代表列,now代表行
{
if(row[i]==0&&l1[now-i+100]==0&&l2[now+i]==0)//满足列、两条对角线上面都没有皇后
{
row[i]=1;
l1[now-i+100]=1;
l2[now+i]=1;
map[now][i]='Q';
dfs(now+1);//标记,回溯,恢复初值
row[i]=0;
l1[now-i+100]=0;
l2[now+i]=0;
map[now][i]='.';
}
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)//先全部给初始化成点,放皇后的再给弄成Q
for(int j=1;j<=n;j++)
map[i][j]='.';
dfs(1);//这里我们枚举的是行,一行一行往下搜索,所以刚开始dfs(1)
return 0;
}
宽度优先搜索
宽搜相比较于深搜,更倾向于“多面地毯式搜索”。
从网上偷了个图,举个栗子
假设我们从v0开始遍历这个图,按照深搜肯定是v0->v3->v5->v6->回溯v3->v1->v4->回溯v1->v2->v6。这样子从v0到v6,但是按照宽搜的话,路线则是:v0->v1&v2&v3->v5&v4&v6(v3->v5,v1->v4->v2->v6)只用了两下就全部遍历完了,当然这是在这个图布局挺好的情况下,宽搜比深搜效率高,要是另一种图,可能就是深搜比宽搜效率高一些咯,所以就题而论,根据题目情况来选择深搜或宽搜~
作为实战笔记,直接上例题训练。
题目描述:
给定一个n*m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。
最初,有一个人位于左上角(1, 1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角(n, m)处,至少需要移动多少次。
数据保证(1, 1)处和(n, m)处的数字为0,且一定至少存在一条通路。
输入格式:第一行包含两个整数n和m。
接下来n行,每行包含m个整数(0或1),表示完整的二维数组迷宫。
输出格式:输出一个整数,表示从左上角移动至右下角的最少移动次数。
数据范围:1≤n,m≤100
Ps:这里的迷宫问题,我们用了pair来进行双值传递,因为要把坐标压入队列,所以需要用pair类型,每次拓展,book+1
代码如下:
/*
向右走,x不变,y++;向左走,x不变,y--;向上走,y不变,x--;向下走,y不变,x++;
*/
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
typedef pair<int ,int >Pair;//定义数据类型,传递双值
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};//分别代表向上、向下、向左、向右走
int map[1001][1001],book[1001][1001];//map用来记录道路状况,book用来记录到达i,j处需要多少步
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>map[i][j],book[i][j]=-1;//初始化
queue <Pair> q;//定义数列q
q.push(Pair(1,1));//压入队列中
book[1][1]=0;//标记为已走过
while(!q.empty())//开始宽搜
{
Pair p=q.front();
q.pop();
for(int i=0;i<4;i++)//模拟四个方向
{
int x=p.first+dx[i],y=p.second+dy[i];
if(map[x][y]==0&&x>=1&&x<=n&&y>=1&&y<=m&&book[x][y]==-1)
{
q.push(Pair(x,y));//满足条件,压入队列,开始拓展
book[x][y]=book[p.first][p.second]+1;
}
}
}
cout<<book[n][m]<<endl;
return 0;
}
代码注释很清楚,不再过多解释。
再来一个变式题:洛谷P1135
Ps:都很清楚电梯是上下,不过我们貌似不太好模拟上下,那就把电梯横过来,变成左右,那这样是不是可以转换模型,建模成一个数轴呢?每一层楼都是一个点,区间是[1,B],问从A到B的最小操作数,这样子思路就迎刃而解咯,那剩下的就是解决变量的定义问题了,我们该如何去存储操作步数?这里用结构体来解决,往队列里面压入结构体类型的变量。
代码如下:
/*
拓展知识:队列里面的元素用结构体类型,就要
queue<node>q q.push({,})
*/
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
int N,A,B,a[10001],dir[]={-1,1},temp=0,book[10001];
struct node{
int id;//代表楼层数
int step;//代表操作数
};
queue<node>q;
node x;
int main()
{
cin>>N>>A>>B;
for(int i=1;i<=N;i++)
{
cin>>a[i];
}
q.push((node){A,0});//压入起点,刚开始没有操作,所以操作数为0
while(!q.empty())//开始宽搜
{
x=q.front();
if(x.id==B)//只要走到目的地了
{temp=1;break;}//标记一下说明是有可行方案的,跳出搜索
q.pop();
for(int i=0;i<2;i++)//模拟两种方向,分别是向左和向右
{
if(x.id+a[x.id]*dir[i]>=1&&x.id+a[x.id]*dir[i]<=B&&book[x.id+a[x.id]*dir[i]]!=1)
{
q.push((node){x.id+a[x.id]*dir[i],x.step+1});//满足条件的拓展
book[x.id+a[x.id]*dir[i]]=1;//标记这里已经走过,防止出现一上一下一上一下的死循环情况
}
}
}
if(temp==1)
cout<<x.step<<endl;//有方案
else
cout<<"-1"<<endl;//没有方案
return 0;
}
大功告成~~
搜索的学习需要理论的支撑和不断地去练习和打磨,我们要将搜索当成一个工具,解题优化的一个方法,而不是一个刻意去死记硬背的一个模板,大家一起加油哦~ qwq