ACM算法笔记(十)深度优先搜索与宽度优先搜索

深度优先搜索

事实上,深度优先搜索属于图算法的一种,英文缩写为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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值