《啊哈算法》之DFS深度优先搜索

✌好听的歌一起分享! 

▶ 花海 (Cover Version) (163.com)

目录

模板

例子

1,关于遍历

2,关于边界

正文 

1,概念

2,解救小哈

例子源码和题目 

 1,小学奥数

 2,全排列

 3,组队

递归中的return

dfs中隐式return


模板

关于dfs,先来个模板,它分为遍历边界两部分

模板不是万能的,它只是一种思路

void dfs(int step)
{

    for(遍历每一种可能) {

            ......

            book[i] = 1;(标记)

            dfs(step + 1);(递归)

            book[i] = 0;(取消标记)

    }

    if(判断边界) {

            ......

            return;(返回上一步)

    }

}

一个if判断边界,判断后,还要return到最近调用的地方

一个for用来遍历,遍历后,还要标记,递归,取消标记 

例子

1,关于遍历

对比几个例子,从对比中加深对dfs模板的理解

for(int i = 0; i < 20; ++i)//一个for遍历20个队员
    if(book[i] == 0)//如果他没被访问
    {
        book[i] = 1;//标记
        dfs(index+1, sum+a[i][index]);//递归
        book[i] = 0;//取消标记
    }
for(int i = 1; i <= n; ++i)
    if(book[i] == 0) { //数字i没放入
        a[place] = i; //把i放入第place个盒子
        book[i] = 1; //标记
        dfs(place + 1); //递归下一个盒子
        book[i] = 0; //取消标记
    }
for(int i = 1; i <= 9; ++i) 
    if(book[i] == 0) {//数字i未被选中
        a[place] = i; //扑克i放入第place号盒子
        book[i] = 1; //标记
        dfs(place + 1); //递归
        book[i] = 0; //取消标记
    }

2,关于边界

if(index == 6)//一个if判断边界
    {
        max_score = max(max_score, sum);
        return; //返回上一步
        //return不能去掉
    }
if(place == n + 1) {//到达最后一个盒子的下一个
    ans++; //总共的可能
    for(int i = 1; i <= n; ++i)
        cout<<a[i];
    cout<<endl;
    return; //返回上一个盒子(最近一次调用dfs的地方)
    //return可去
    }
if(place == 10) { //来到不存在的第10个盒子,已结束
    if(a[1]*100+a[2]*10+a[3] + a[4]*100+a[5]*10+a[6] == a[7]*100+a[8]*10+a[9]) {
        ans++;
        printf("%d%d%d+%d%d%d=%d%d%d\n",a[1],a[2],a[3],
                a[4],a[5],a[6],a[7],a[8],a[9]);
    }
    return; //返回上一个盒子
    //这里return可去掉,输出不变
}

正文 

1,概念

理解递归的前提,是理解递归😀

这些简单的例子,核心代码不超过20行,却饱含深度优先搜索(Depth First Search, DFS)的基本模型

理解深度优先搜索的关键在于解决“当下该如何做”。

至于“下一步如何做”则与“当下该如何做”是一样的。

比如我们写的dfs(step)函数的主要功能就是解决当你在第step个盒子的时候你该怎么办

通常的方法就是把每一种可能都去尝试一遍(一般用for循环来遍历)。

当前这一步解决后便进入下一步dfs(dfs + 1)

下一步的解决方法和当前这步解决方法是完全一样的

再看个递归路线图

1、访问顶点a
2、依次从a的未被访问的邻接点出发,对图进行深度优先遍历;直至图中所有和a有路径相通的顶点被访问💪
3、若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止

路径:

a  b  d  h  d  b  e  i  e  j  e  b  a  c  f  k  f  c  g(橙色表示初次经过)

过程描述:

第一条路访问了a,b,d,h,走到底了,就从h退回(1)d,发现节点d没有除h以外的没访问过的,就退回(2)b,b有除d以外未访问过的e,所以又从e开始进行dfs...

被访问的顺序:

a --> b --> d --> h --> e --> i --> j --> c --> f --> k --> g

2,解救小哈

 题目

1,有一天,我的女朋友一个人去玩迷宫,因为方向感很差,迷路了,我得知后马上去解救她

2,迷宫由m行n列的单元格组成(m和n都小于100),每个单元格要么是空地,要么是障碍

3,我的任务是帮助女朋友找到一条从迷宫起点通向女朋友位置的最短路径

4,注意障碍是不能走的,也不能走到迷宫外

思路

开始套模板吧!

1,首先我们用一个二维数组a存储这个迷宫

2,假设一开始我在迷宫入口处(1,1),女朋友在(p, q),就要找(1,1)到(p,q)的最短路径

3,一开始只能往右或下走,到底往哪个方向走呢?只能一个一个方向尝试

4,于是我们建立一个方向数组,使用循环很容易得到下一步坐标

int next[4][2] = { //方向数组,循环得到下一步坐标
        {-1,0}, //上
        {1, 0}, //下
        {0,-1}, //左
        {0, 1}};//右

遍历所有点怎么遍历呢?

int tx, ty; //作为临时变量
    for(int i = 0; i < 4; ++i) {
        tx = x + next[i][0];
        ty = y + next[i][1];
}

接下来,还需要补充遍历的内容

1,判断是否超出迷宫范围

2,判断到达的地方是否是空地且没有走过 

int tx, ty; //作为临时变量
    for(int i = 0; i < 4; ++i) {
        tx = x + next[i][0];
        ty = y + next[i][1];
        //判断越界
        if(tx < 1 || ty < 1 || tx > m || ty > n)
            continue; //跳出本次循环
        //空地且未走过
        if(a[tx][ty] == 0 && book[tx][ty] == 0) {
            book[tx][ty] = 1; //标记
            dfs(tx, ty, step + 1); //递归
            book[tx][ty] = 0; //取消标记
        }
    }

接下来,需要判断边界,也就是是否到达了女朋友那里,我们用ans保留当前最短路径

//判断边界,本题为小哈位置
    if(x == p && y == q) {
        ans = min(ans, step);
        return; //返回上一步
    }

完整代码 

#include<iostream>
using namespace std;
int a[100][100], book[100][100], ans = 6666666;
int p, q; //小哈坐标
int m, n; //迷宫的行,列
void dfs(int x, int y, int step)
{
    int next[4][2] = { //方向数组,循环得到下一步坐标
        {-1,0}, //上
        {1, 0}, //下
        {0,-1}, //左
        {0, 1}};//右
    //枚举四种走法
    int tx, ty; //作为临时变量
    for(int i = 0; i < 4; ++i) {
        tx = x + next[i][0];
        ty = y + next[i][1];
        //判断越界
        if(tx < 1 || ty < 1 || tx > m || ty > n)
            continue; //跳出本次循环
        //空地且未走过
        if(a[tx][ty] == 0 && book[tx][ty] == 0) {
            book[tx][ty] = 1; //标记
            dfs(tx, ty, step + 1); //递归
            book[tx][ty] = 0; //取消标记
        }
    }
    //判断边界,本题为小哈位置
    if(x == p && y == q) {
        ans = min(ans, step);
        return; //返回上一步
    }
}
int main()
{
    int startx, starty; //初始位置坐标
    cin>>m>>n;
    for(int i = 1; i <= m; ++i)
        for(int j = 1; j <= n; ++j)
            cin>>a[i][j];
    cin>>startx>>starty>>p>>q;
    book[startx][starty] = 1; //标记初始已走过
    dfs(startx, starty, 0);
    cout<<ans;
    return 0;
}
5 4
0 0 1 0
0 0 0 0
0 0 1 0
0 1 0 0
0 0 0 1
1 1 4 3
7

原方案通过二维数组book,记录在迷宫中走过的点,迷宫越大,book数组占的空间越大

下面是在原代码基础上的优化,优化方法如下:

将到达过的点,在二维数组a中位置的值改为-1,未到达的点依然为0,障碍依然为1,容易区分,且占用内存少

优化代码

#include<iostream>
using namespace std;
int a[100][100], ans = 6666666;
int p, q; //小哈坐标
int m, n; //迷宫的行,列
void dfs(int x, int y, int step)
{
    int next[4][2] = { //方向数组,循环得到下一步坐标
        {-1,0}, //上
        {1, 0}, //下
        {0,-1}, //左
        {0, 1}};//右
    //枚举四种走法
    int tx, ty; //作为临时变量
    for(int i = 0; i < 4; ++i) {
        tx = x + next[i][0];
        ty = y + next[i][1];
        //判断越界
        if(tx < 1 || ty < 1 || tx > m || ty > n)
            continue; //跳出本次循环
        //空地且未走过
        if(a[tx][ty] == 0) {
            a[tx][ty] = -1; //标记走过
            dfs(tx, ty, step + 1); //递归
            a[tx][ty] = 0; //取消标记
        }
    }
    //判断边界,本题为小哈位置
    if(x == p && y == q) {
        ans = min(ans, step);
        return; //返回上一步
    }
}
int main()
{
    int startx, starty; //初始位置坐标
    cin>>m>>n;
    for(int i = 1; i <= m; ++i)
        for(int j = 1; j <= n; ++j)
            cin>>a[i][j];
    cin>>startx>>starty>>p>>q;
    a[startx][starty] = -1; //标记初始已走过
    dfs(startx, starty, 0);
    cout<<ans;
    return 0;
}

仔细观察, 我们只修改了

第2行(去掉了全局变量book数组的声明

第23,25行(标记走过和未走过时,book改为a,赋值从1变成-1

第42行(初始标记走过从book[startx][starty] = 1改为a[startx][starty] = -1

-----------------------------------------------------------------------------------------------------------

例子源码和题目 

其中1,2可以去掉if判断边界中的return,3不能去掉,关于为什么,我猜是3中dfs递归时,存在两个变量

 1,小学奥数

Ubuntu Pastebin(源码)

□□□ + □□□ = □□□
将数字1 ~ 9分别填入 □ 种,每个数字只能使用一次使等式成立

比如173 + 286 = 459 与 286 + 173 = 459 为一种可能

#include<iostream>
#include<cstdio> //printf()
using namespace std;
int a[10], book[10], ans = 0; //全局变量
void dfs(int place)
{
    for(int i = 1; i <= 9; ++i) {
        if(book[i] == 0) {//数字i未被选中
            a[place] = i; //扑克i放入第place号盒子
            book[i] = 1; //标记
            dfs(place + 1); //递归
            book[i] = 0; //取消标记
        }
    }
    if(place == 10) { //来到不存在的第10个盒子,已结束
        if(a[1]*100+a[2]*10+a[3] + a[4]*100+a[5]*10+a[6]
           == a[7]*100+a[8]*10+a[9]) {
            ans++;
            printf("%d%d%d+%d%d%d=%d%d%d\n",a[1],a[2],a[3],
                   a[4],a[5],a[6],a[7],a[8],a[9]);
        }
        return; //返回上一个盒子
        //这里return可去掉,输出不变
    }
}

int main()
{
    dfs(1); //从第一个盒子开始
    cout<<ans / 2<<endl;
    return 0;
}
......(一共336行,所以是168种可能)
748+215=963
752+184=936
754+182=936
762+183=945
763+182=945
782+154=936
782+163=945
783+162=945
784+152=936
168

 2,全排列

Ubuntu Pastebin (源码)

小哼要将编号为1,2......n的n张扑克放到编号为1,2......n的n个盒子里,每个盒子有且只能放一张扑克,问一共有多少种不同放法(全排列)(n <= 9)

#include<iostream>
using namespace std;
int a[10], book[10], n, ans = 0;
void dfs(int place)
{
    for(int i = 1; i <= n; ++i) {
        if(book[i] == 0) { //数字i没放入
            a[place] = i; //把i放入第place个盒子
            book[i] = 1; //标记
            dfs(place + 1); //递归下一个盒子
            book[i] = 0; //取消标记
        }
    }
    if(place == n + 1) {//到达最后一个盒子的下一个
        ans++; //总共的可能
        for(int i = 1; i <= n; ++i)
            cout<<a[i];
        cout<<endl;
        return; //返回上一个盒子(最近一次调用dfs的地方)
        //return可去
    }
}
int main()
{
    cin>>n;
    dfs(1); //从第一个盒子开始
    cout<<ans<<endl;
    return 0;
}
3
123
132
213
231
312
321
6

4的话有24种,5的话有120种....

 3,组队

Ubuntu Pastebin(源码)

2019蓝桥杯C/C++B组ヽ(✿゚▽゚)ノ

作为篮球队教练,你需要从以下名单中选出 1 号位至 5 号位各一名球员,
组成球队的5人首发阵容。
每位球员担任 1 号位至 5 号位时的评分如下表所示。请你计算首发阵容 1
号位至 5 号位的评分之和最大可能是多少?

在这里插入图片描述

#include<iostream>
using namespace std;
int book[20], max_score = 0;
int a[20][6] = //声明初始化二维数组存储表格数据
{
    {1,97,90,0,0,0},
    {2,92,85,96,0,0},
    {3,0,0,0,0,93},
    {4,0,0,0,80,86},
    {5,89,83,97,0,0},
    {6,82,86,0,0,0},
    {7,0,0,0,87,90},
    {8,0,97,96,0,0},
    {9,0,0,89,0,0},
    {10,95,99,0,0,0},
    {11,0,0,96,97,0},
    {12,0,0,0,93,98},
    {13,94,91,0,0,0},
    {14,0,83,87,0,0},
    {15,0,0,98,97,98},
    {16,0,0,0,93,86},
    {17,98,83,99,98,81},
    {18,93,87,92,96,98},
    {19,0,0,0,89,92},
    {20,0,99,96,95,81}
};
//index表示当前位置, sum是当前组合总分
void dfs(int index, int sum)
{
    if(index == 6)//一个if判断编辑
    {
        max_score = max(max_score, sum);
        return; //返回上一步
        //return不能去掉
    }
    for(int i = 0; i < 20; i++)//一个for遍历20个队员
        if(book[i] == 0)//如果他没被访问
        {
            book[i] = 1;//标记
            dfs(index+1, sum+a[i][index]);//递归
            book[i] = 0;//取消标记
        }
}
int main()
{
    dfs(1, 0); //第一个成员开始,总分为0
    cout<<max_score<<endl;
    return 0;
}
490

 --------------------------------------------------------------------------------------

递归中的return

1,C++ 递归函数中的return是指:
从被调用函数返回到主调函数中继续执行,并非一遇到return整个递归结束

2,return 语句,顾名思义是终止当前正在执行的函数并将控制权返回到调用该函数的地方

3,在void的函数中也可以多次使用return,功能和循环中的break一样,在中间位置提前退出正在执行函数,也就是回到原来位置执行下一行代码

4,return; 表示结束本次函数

5,⭐递归中的return常用来作为递归终止的条件,当达到递归终止条件时,首先return的是最底层调用的函数,return之后,继续执行上一层调用该函数之后的代码⭐

在我对return进行理解时,这篇200收藏的文章给了我一定启发关于递归中return的理解(最浅显易懂)_Pledgee的博客-CSDN博客_递归函数return怎么理解

--------------------------------------------------------------------------------------

dfs中隐式return

隐式return的情况是,执行完函数后,自动返回上一级

 1,走方格

题目

给定一个m*n方格阵,沿着方格边线走,从左上角(0, 0)开始,每次只能往右或往下走一个单位距离,问走到右下角(m, n)一共有多少种不同走法

输入

一行包含两个整数 m 和 n (m >= 1 && m <= 10) && (n >= 1 && n <= 10)

输出

输出一个整数,表示走法数量

解析 

代码

#include<iostream>
using namespace std;
int m, n, num = 0;
int dfs(int x, int y)
{
    if(x == m && y == n)
        num++;//走法数量+1
    else
    {
        if(x < m)
            dfs(x + 1, y);//递归行加一
        if(y < n)
            dfs(x, y + 1);//递归列加一
    }
}
int main()
{
    while(cin>>m>>n)
    {
        dfs(0, 0);
        cout<<num<<endl;
        num = 0;
    }
    return 0;
}
3 2
10
5 5
252
10 10
184756

2,例子2

例子2只为说明隐式return(不需要return的情况)

#include <iostream>
using namespace std;

void dfs(int n)
{
    cout << "level: " << n << endl;/*1*/
    if (n < 4)
        dfs(n + 1);
    cout << "level: " << n << endl;/*2*/
}
int main()
{
    dfs(1);
    return 0;
}

输出:

lever: 1(执行/*1*/)
lever: 2(执行/*1*/)
lever: 3(执行/*1*/)
lever: 4(执行/*1*/)
lever: 4(执行完/*2*/后,函数返回上一级dfs递归的下一行
lever: 3(同上)
lever: 2(同上)
lever: 1(同上)

level: 1
level: 2
level: 3
level: 4
level: 4
level: 3
level: 2
level: 1
  • 16
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千帐灯无此声

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值