【从零到蓝桥杯省一】算法详解之深度优先搜索

简介: 大家好,我是泡泡,一名算法爱好者,在学习算法的这条路上有很多的坎坷与‘大山’,想必各位都深有体会,我认为学习算法的几个很卡的点,首当其冲就是深度优先搜索和广度优先搜索,虽然理解了是什么意思但是完全敲不出来代码,练基础的题也不会写,导致了自己很迷惑,之前学的排序一类的并没有这种情况,为什么呢?实际上是因为你没有在了解算法的同时记住这个模板,y总有句话讲的很好,你并不是在构造算法,你是在学习使用模板,是啊,这是人类历史上几十年甚至上百年各个科学家智慧的结晶,对于初学肯定很难,所以就有了今天这篇文章。

前言


大家好,我是泡泡,一名算法爱好者,在学习算法的这条路上有很多的坎坷与‘大山’,想必各位都深有体会,我认为学习算法的几个很卡的点,首当其冲就是深度优先搜索和广度优先搜索,虽然理解了是什么意思但是完全敲不出来代码,练基础的题也不会写,导致了自己很迷惑,之前学的排序一类的并没有这种情况,为什么呢?实际上是因为你没有在了解算法的同时记住这个模板,y总有句话讲的很好,你并不是在构造算法,你是在学习使用模板,是啊,这是人类历史上几十年甚至上百年各个科学家智慧的结晶,对于初学肯定很难,所以就有了今天这篇文章。


今天这篇文章,就是为了帮助还卡在深度优先搜索,实在不会写代码的同学们,看了这篇文章可能会给你不一样的体验,这里效仿了闫式dp分析法做了一个自己的简单的dfs四步法,牢记四步,迷宫问题,排列问题完全不怕不会做!


目录


算法简述


练习1:海战


题目描述


输入格式


输出格式


练习2:排列


题目描述


输入格式


输出格式


总结


算法简述


深度优先搜索:


它是图的遍历方法的一种,用栈来实现,深度优先搜索和树的先序遍历比较类似。


它的思想用人话来说就是不撞南墙不回头,一直走一个点直到不能走再考虑其他方向,直到所有的点都被遍历过。显然,深度优先搜索是一个递归的过程。


可能光说不能感受到他的魅力,我们上图来了解一下。


46.png


因为博主画画太丑了,所以借用了浙大数据结构的图,这里是一个图,所有点都没访问过


47.png


然后我们从第一个点开始,灯泡点亮了,如果在代码里我们就需要一个数组来记录走过的路。


48.png


然后我们往右边走,发现没走过,然后走到这


49.png


到这里之后发现只能往左走,那就继续往走左


50.png


此时这里我们发现有两条路可以走,用犹豫嘛?不用,我们最终都遍历的,所以干就完了,选择上面,走!


51.png


52.png


走完之后,我们发现无路可走了,于是我们原路返回,返回刚才做选择的位置。


53.png


然后选择没有走过的地方继续走


54.png


结束


相信大家看图可以很明白地看到深度优先搜索是怎么运作的,但是实际应用的情况代码应该如何去写呢,我这里先拿出来一个自己写的简易模版来给大家看一下理解一下,然后我们引入几个例子让大家更深入地去理解深度优先搜索。


按照我们四步法来,第一步,存图,方向,标记数组


大家要读题来决定,存图数组,方向数组,标记数组开成什么样子,这里以正常二维数组和上下左右来写


const int N = 10001;
int dx[] = {0,1,-1,0};
int dy[] = {1,0,0,-1};
int s[N][N];
bool vis[N][N];


第一层是数据范围,第二层第三层是方向,如果你 此时在1 1 那么+0 +1就变成1 2 那么就是右边一个,+1 +0就是2 1变往下走一个。四个方向都这么来就好,如果是八个方向需要多一些


int dx[] = {1,0,0,-1,1,-1,1,-1}
int dy[] = {0,1,-1,0,1,-1,-1,1}


第一步解决了,我们开始第二步


第二步:找到起始点,题目要求,开始dfs


比如要求我们找连通块,我们可以循环整个图,两个for进去,然后如果图的位置等于哪个连通块的字符或者数字,就进入搜索,代码如下


for(int i=1;i<=n;i++)
{
    for(int j=1;j<=m;j++)
    {
        if(s[i][j]=='*')
        {
            dfs(i,j);
        }
    }
}


然后就是我们的第三步,dfs函数的写法


dfs函数怎么写,首先标记当前位置已经走过,然后写一个循环,有几个方向循环几层,循环里写判断是否越界走过,是否满足要求,没有就继续递归下去,代码如下


bool pd(int x,int y)
{
  if(x<1||x>n||y<1||y>m)
  {
  return false;
  }
  if(vis[x][y])
  {
  return false;
  }
  return true;
}
void dfs(int x, int y)
{
    vis[x][y] = 1;
    for(int i=0;i<4;i++)
  {
  int xx = x+dx[i];
  int yy = y+dy[i];
        if(pd(xx,yy)&&s[xx][yy]=='.')
        {
          dfs(x+xx[i],y+yy[i]);
  }
    }
}


第四步就是保存答案输出,这就很简单了,我们就不放代码了。


综上所述,我们就学会了一个联通块dfs的代码模板,遇到联通块问题我们就可以用这四步,妥妥的解决,大家先看一下记一下这四步,然后我们做两道题来巩固一下!


练习1:海战


题目描述


在峰会期间,武装部队得处于高度戒备。警察将监视每一条大街,军队将保卫建筑物,领空将布满了F-2003飞机。此外,巡洋船只和舰队将被派去保护海岸线。不幸的是因为种种原因,国防海军部仅有很少的几位军官能指挥大型海战。因此,他们考虑培养一些新的海军指挥官,他们选择了“海战”游戏来帮助学习。


在这个著名的游戏中,在一个方形的盘上放置了固定数量和形状的船只,每只船却不能碰到其它的船。在这个题中,我们仅考虑船是方形的,所有的船只都是由图形组成的方形。编写程序求出该棋盘上放置的船只的总数。


输入格式


输入文件头一行由用空格隔开的两个整数R和C组成,1<=R,C<=1000,这两个数分别表示游戏棋盘的行数和列数。接下来的R行每行包含C个字符,每个字符可以为“#”,也可为“.”,“#”表示船只的一部分,“.”表示水。


输出格式


为每一个段落输出一行解。如果船的位置放得正确(即棋盘上只存在相互之间不能接触的方形,如果两个“#”号上下相邻或左右相邻却分属两艘不同的船只,则称这两艘船相互接触了)。就输出一段话“There are S ships.”,S表示船只的数量。否则输出“Bad placement.”。


6 8

.....#.#

##.....#

##.....#

.......#

#......#

#..#...#


以上就是一个最简单的判断连通块的问题,我们直接套用四步法,第一步,存图,方向,标记。


因为题目要求是四个方向,二位数组,所以我们按上面的开就好。


代码:


#include<bits/stdc++.h>
using namespace std;
const int N = 1001;
int dx[] = {1,0,0,-1};
int dy[] = {0,1,-1,0};
char s[N][N];
bool vis[N][N];
int main()
{
  int r,c;
  cin>>r>>c;
  for(int i=1;i<=r;i++)
  {
    for(int j=1;j<=c;j++)
    {
      cin>>s[i][j];
    }
  }
  return 0;
}


然后是我们的第二步,找到起点,题目要求,开始dfs


题目要求 不能有相邻的船,题目这个意思就是一个2*2的格子里如果有三个船体就是相邻的,所以是错误的,要写一个函数来单独判断,起点就是遇到的任何一个没走过的船体。


bool pd(int a,int b)
{
  int sum = 0;
  if(s[a][b]=='#')
  {
  sum++;
  }
  if(s[a+1][b]=='#')
  {
  sum++;
  }
  if(s[a][b+1]=='#')
  {
  sum++;
  }
  if(s[a+1][b+1]=='#')
  {
  sum++;
  }
  if(sum==3)
  {
  return false;
  }
  return true;
}


这是判断函数 下,右下右四个方向,判断是否有炸船。


然后就是我们的找起点代码


for(int i=1;i<=r;i++)
{
  for(int j=1;j<=c;j++)
  {
  if(s[i][j]=='#'&&vis[i][j]==0)
  {
    dfs(i,j);
  }
  }
}


找到船体#就开搜。


然后就是我们的第三步,写dfs。


dfs怎么写呢,记得我们上面说的,越界,走没走过,循环。


bool yuejie(int x,int y)
{
  if(x<1||x>r||y<1||y>c)
  {
  return false;
  }
  if(vis[x][y])
  {
  return false;
  }
  return true;
}
void dfs(int x,int y)
{
  vis[x][y] = 1;
  for(int i=0;i<4;i++)
  {
  int xx = x+dx[i];
  int yy = y+dy[i];
  if(yuejie(xx,yy)&&s[xx][yy]=='#')
  {
    dfs(xx,yy);
  }
  }
}


第四步就是我们的答案,我们首先判断是否炸船,然后再定义一个变量,每次进入联通块就+1就好了,如果炸船直接输出结束,不用判断了就。


完整代码如下


#include<bits/stdc++.h>
using namespace std;
const int N = 1001;
int dx[] = {1,0,0,-1};
int dy[] = {0,1,-1,0};
char s[N][N];
bool vis[N][N];
int r,c;
int num;
bool yuejie(int x,int y)
{
  if(x<1||x>r||y<1||y>c)
  {
    return false;
  }
  if(vis[x][y])
  {
    return false;
  }
  return true;
}
void dfs(int x,int y)
{
  vis[x][y] = 1;
  for(int i=0;i<4;i++)
  {
    int xx = x+dx[i];
    int yy = y+dy[i];
    if(yuejie(xx,yy)&&s[xx][yy]=='#')
    {
      dfs(xx,yy);
    }
  }
}
bool pd(int a,int b)
{
  int sum = 0;
  if(s[a][b]=='#')
  {
    sum++;
  }
  if(s[a+1][b]=='#')
  {
    sum++;
  }
  if(s[a][b+1]=='#')
  {
    sum++;
  }
  if(s[a+1][b+1]=='#')
  {
    sum++;
  }
  if(sum==3)
  {
    return false;
  }
  return true;
}
int main()
{
  cin>>r>>c;
  for(int i=1;i<=r;i++)
  {
    for(int j=1;j<=c;j++)
    {
      cin>>s[i][j];
    }
  }
  for(int i=1;i<=r;i++)
  {
    for(int j=1;j<=c;j++)
    {
      if(pd(i,j)==false)
      {
        cout<<"Bad placement.";
        return 0;
      }
    }
  }
  for(int i=1;i<=r;i++)
  {
    for(int j=1;j<=c;j++)
    {
      if(s[i][j]=='#'&&vis[i][j]==0)
      {
        num++;
        dfs(i,j);
      }
    }
  }
  printf("There are %d ships.",num);
  return 0;
}


直接AC!其实这个代码可以不用vis,让代码量更小,大家可以思考自己写一下。


ps:修改原图数值


如果你跟下来的话你应该理解了联通块dfs如果解决了吧,我准备了几道题,大家一定要联系一下,记住我的四步法然后自己去解决,你解决一个dfs你自己就会了!


洛谷:P1451 求细胞数量 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)


P1596 [USACO10OCT]Lake Counting S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)


题都不难,大家自己敲一遍!


然后就是我们的全排列了。


全排列也是四步法,这里拿最经典的一个全排列问题来讲解


练习2:排列


题目描述


按照字典序输出自然数 1 到 n 所有不重复的排列,即 n 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。


输入格式


一个整数 nn。


输出格式


由 1∼n 组成的所有不重复的数字序列,每行一个序列。


每个数字保留 5 个场宽。


看到这个题,很多人都笑了,这题也是很简单,这里四步法给大家讲一下


第一步,我们没有了方向数组,就是判断是否用过和存值数组。


int n,pd[100],a[100];


第二步,找到起始点,题目要求,开始dfs


起始点自然是0,然后直接搜。


int main()
{
    cin>>n;
    dfs(0);
    return 0;
}


第三步,写dfs函数


题目要求是n,所以我们首先要判断有没有达到题目要求,达到就输出返回,不达到就接着往下搜,因为题目要求了n,所以循环以=n结束,每次判断该数有没有用过,没有用过我们就保存起来,然后写一个简单的记忆化搜索,因为用完之后返回,如果不清理现场那么就很乱了。


比如用123456了,如果你不归0,那么下一次循环判断你数组里还是123456,会导致一些数据错误问题。


void dfs(int k)
{
    if(k==n)
    {
        for(int i=1;i<=n;i++)
        {
            printf("%5d",a[i]);
        }
        printf("\n");
        return ;
    }
    for(int i=1;i<=n;i++)
    {
        if(!pd[i])
        {
            pd[i]=1;
            a[k+1]=i;
            dfs(k+1);
            pd[i]=0;
        }
    }
}


第四步,就是我们的 过啦!哈哈哈哈


完整代码如下


#include<bits/stdc++.h>
using namespace std;
int n,pd[100],a[100];
void dfs(int k)
{
    if(k==n)
    {
        for(int i=1;i<=n;i++)
        {
            printf("%5d",a[i]);
        }
        printf("\n");
        return ;
    }
    for(int i=1;i<=n;i++)
    {
        if(!pd[i])
        {
            pd[i]=1;
            a[k+1]=i;
            dfs(k+1);
            pd[i]=0;
        }
    }
}
int main()
{
    cin>>n;
    dfs(0);
    return 0;
}


这里依然给出两个练习题,大家课后自己练一下,会一个就都会了!


洛谷:P1036 [NOIP2002 普及组] 选数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)


P1157 组合的输出 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)


总结


今天我们学习了dfs的联通块判断和全排列判断,这些是常用的,其他变种的需要我们的灵活多变了,联通块的四步法:1.存图方向判断2.起点 dfs3.dfs函数怎么写4.答案输出 全排列则是:1.存答案 判断2.起点 dfs3.dfs函数怎么写4.答案输出。这两个方法其实是差不多的,就是全排列没有方向,多的是对这个数字是否使用的判断,大家牢记于心,方可攻破dfs!  

目录
相关文章
|
1月前
|
算法 测试技术 定位技术
数据结构与算法——DFS(深度优先搜索)
数据结构与算法——DFS(深度优先搜索)
|
1月前
|
算法 测试技术 C++
【动态规划算法】蓝桥杯填充问题(C/C++)
【动态规划算法】蓝桥杯填充问题(C/C++)
|
1月前
|
存储 机器学习/深度学习 算法
蓝桥杯练习题(三):Python组之算法训练提高综合五十题
蓝桥杯Python编程练习题的集合,涵盖了从基础到提高的多个算法题目及其解答。
60 3
蓝桥杯练习题(三):Python组之算法训练提高综合五十题
|
5月前
|
存储 机器学习/深度学习 算法
第十五届蓝桥杯pb组国赛E题[马与象] (15分)BFS算法 详解
第十五届蓝桥杯pb组国赛E题[马与象] (15分)BFS算法 详解
61 3
|
1月前
|
算法 C++
【算法解题思想】动态规划+深度优先搜索(C/C++)
【算法解题思想】动态规划+深度优先搜索(C/C++)
|
5月前
|
Python
求解带有限重的三维装箱问题——启发式深度优先搜索算法
求解带有限重的三维装箱问题——启发式深度优先搜索算法
100 4
|
5月前
|
算法 Java
Java数据结构与算法:图算法之深度优先搜索(DFS)
Java数据结构与算法:图算法之深度优先搜索(DFS)
|
5月前
|
人工智能 算法 搜索推荐
蓝桥杯宝藏排序题目算法(冒泡、选择、插入)
以下是内容的摘要: 本文介绍了三种排序算法:冒泡排序、选择排序和插入排序。冒泡排序通过不断交换相邻的逆序元素逐步排序,最坏情况下需要 O(n^2) 次比较。选择排序在每轮中找到剩余部分的最小元素并放到已排序序列的末尾,同样具有 O(n^2) 时间复杂度。插入排序则是将每个元素插入到已排序序列的正确位置,时间复杂度也是 O(n^2),但空间复杂度为 O(1)。
|
5月前
|
人工智能 算法 物联网
求解三维装箱问题的启发式深度优先搜索算法(python)
求解三维装箱问题的启发式深度优先搜索算法(python)
87 0
|
5月前
|
算法 Python
利用深度优先搜索算法解决老鼠吃奶酪问题(python)
利用深度优先搜索算法解决老鼠吃奶酪问题(python)
36 0