算法基础6 —— DFS(全排列 + 数独游戏 + 子集 + 马走日 + N皇后 + 迷宫)

DFS的算法 —— 不撞南墙不回头

人的认识需要在继承已有的有关知识的基础上,经过多次反复才能深化,在人的认识深化过程中,既包括了从一般到特殊的演绎思维过程,也包括了从特殊到一般的归纳思维过程
—— 张海藩 《软件工程导论》


例1 全排列模板

求全排列问题的几种方法:

  • 暴力搜索法
  • next_permutation
  • DFS

DFS解决全排列问题的过程:(以实现123三个数字的全排列为例)
如下图,假设现在有三个粉粉的盒子,还有一个小明。小明手里持有三张卡片,每张卡片上的数字互不相同,且三张卡片上的数字分别是1,2,3,记一号卡片上的数字为1,二号卡片上的数字为2,三号卡片上的数字为3
在这里插入图片描述

初始时小明手中持有三个卡片。现在他走到了第一个盒子的前面,假设他把1号卡片放入了第一个盒子,然后走向第二个盒子,到达第二个盒子时他的手中还有2号和3号两张卡片,假设他又把2号卡片放入了第二个盒子,然后走向第三个盒子,此时他手中只剩下3号卡片,故他只能把3号卡片放入第三个盒子,最后他继续往前行走会发现撞到了墙壁(到达边界),此时就得到了第一个排列 —— 1 2 3
在这里插入图片描述
递归可以拆分为递进以及回归,以上是递进的过程,但是当遇到我们提前设置好的边界问题时,程序就需要回归。
小明从墙壁(边界)处开始回归,当回归到第三个盒子的位置时,回收第三个盒子里的3号卡片,这时他手中只有3号卡片,但是由于3号卡片已经放入过一次三号盒子,故不会这么着急再次放入,所以小明继续回归。当小明回归到第二个盒子的位置时,回收第二个盒子里的2号卡片,这时他手中有2号和3号两张卡片。所以他可以把3号卡片放入第二个盒子,然后继续递进,当到达第三个盒子的时候,再把2号卡片放入最后一个盒子,然后继续前进直至撞墙(到达边界)。此时就又得到了另一个排列 —— 1,3,2
在这里插入图片描述
小明再次从墙壁(边界)处开始回归,当回归到第三个盒子的位置时,回收第三个盒子里的2号卡片,这时他手中只有2号卡片,但是由于2号卡片刚刚放入过三号盒子,故不会这么着急再次放入,所以小明继续回归。当小明回归到第二个盒子的位置时,回收第二个盒子里的3号卡片,这时他手中有2号和3号两张卡片。但是由于3号卡片已经放入过一次二号盒子,3号卡片也已经放入过一次三号盒子,故不会这么着急再次放入,所以小明继续回归到一号盒子然后回收了1号卡片

现在小明和初始状态时一样,手中持有三个卡片。并且又处于第一个盒子的前面,假设他把2号卡片放入了第一个盒子,然后走向第二个盒子,到达第二个盒子时他的手中还有1号和3号两张卡片,假设他又把1号卡片放入了第二个盒子,然后走向第三个盒子,此时他手中只剩下3号卡片,故他只能把3号卡片放入第三个盒子,最后他继续往前行走会发现撞到了墙壁(到达边界),此时就得到了第三个排列 —— 2 1 3 … …
在这里插入图片描述

依次类推可以得到所有的排列结果

代码实现以及详细注释:

#include <iostream>
#include <cstdio>

using namespace std;

int res[100];//结果数组,用来存储结果
int vis[100];//标记数组,vis[i]=0表示数字i没有放入,1表示数字i已经放入
int n;//经验:由于dfs函数中要用到变量n,故n一般都定义为全局变量

void dfs(int step)//step表示当前面临第几个盒子
{
    //递归出口,打印结果(三个数字,n = 3 -> step = 4)
    //递归出口的第二种写法:step > n
    if (step == n + 1)
    {
        for (int i = 1;i <= n;i++) printf("%d ",res[i]);
        puts(" ");//输出一种排列后换行
        return;//回溯
    }
    
    //递归搜索1 ~ n的全排列
    for (int i = 1;i <= n;i++)//从1开始枚举
    {
        if (vis[i] == 0)//vis[i] = 0表示i没有放入盒子
        {
            res[step] = i;//把i放入当前的盒子
            vis[i] = 1;//修改标记数组表示i已经放入了盒子(表示i号卡片已经不在手中)
            
            dfs(step + 1);//搜索下一个盒子的排列结果,搜索完毕之后需要回溯(回归)
            
            vis[i] = 0;//回溯,需要将标记数组修改回来表示i没有放入盒子(即回收之后卡片又回到了手中)
        }
    }
}

int main()
{
    cin >> n;
    dfs(1);//从第一个盒子开始搜索结果
    return 0;
}

练习:

全排列模板题1

全排列模板题2


例2 数独游戏

深度优先搜索应用于数独游戏中思想的体现:

  • 当前如何做
  • 下一步如何做

在这里插入图片描述
数独游戏描述:

  • 如上图棋盘,大棋盘有9行9列
  • 将大棋盘分为9个3行3列的小棋盘
  • 每个小棋盘中有几个数字
  • 现在需要我们往小棋盘中补充数字,补充完数字之后使得每个小棋盘中都包含1 ~ 9这几个数字
  • 还需要保证大棋盘的每一行都包含1 ~ 9这几个数字,每一列也都包含1 ~ 9这几个数字

棋盘初始化:

6 7 2 0 0 5 0 0 1
0 0 0 0 6 4 0 0 8
0 0 9 0 0 3 0 0 2
0 9 0 0 2 0 4 3 0
0 0 7 0 0 0 5 0 0
0 6 5 0 1 0 0 2 0
2 0 0 4 0 0 9 0 0
9 0 0 7 8 0 0 0 0
7 0 0 6 0 0 2 8 3

代码实现以及详细注释:

#include <iostream>
#include <cstdio>

using namespace std;

const int N = 10;//定义一个常量
int a[N][N];//开一个数组

bool check(int x,int y,int i)//检查数字i是否可以放入坐标(x,y)的位置
{
    /*
    check函数需要检查三点:
    1.同一行元素是否有重复
    2.同一列元素是否有重复
    3.小九宫格中的元素是否有重复
    */
    for (int k = 0;k < 9;k++)
    {
        if (a[x][k] == i) return false;//检查同一行元素中是否存在与i重复
        if (a[k][y] == i) return false;//检查同一列元素中是否存在与i重复
    }
    
    for (int p = (x / 3) * 3;p < (x / 3 + 1) * 3;p++)
        for (int q = (y / 3) * 3;q < (y / 3 + 1) * 3;q++)
            if (a[p][q] == i)
                return false;

    return true;
}

void dfs(int x,int y)//当前坐标为(x,y)时,如何处理
{
    if (x == 9)//到达边界,输出结果(棋盘第一个位置的坐标为(0,0))
    {
        for (int s = 0;s < 9;s++)
        {
            for (int t = 0;t < 9;t++) cout << a[s][t] << " ";
            puts(" ");
        }
    }
    
    if (a[x][y] == 0)//说明棋盘当前的位置无数字,可以放入数字
    {
        for (int i = 1;i <= 9;i++)//从1 ~ 9中选择可以符合条件的数字放入棋盘
        {
            if (check(x,y,i))//表示i可以放入坐标为(x,y)的位置
            {
                a[x][y] = i;//将i放入棋盘坐标为(x,y)的位置
                dfs(x + (y + 1) / 9,(y + 1) % 9);//递归处理下一个单元格
                a[x][y] = 0;//回溯
            }
        }
    }
    else dfs(x + (y + 1) / 9,(y + 1) % 9);//如果当前位置不为0,则直接处理下一个单元格
}

int main()
{
    for (int m = 0;m < 9;m++)
        for (int n = 0;n < 9;n++) 
            cin >> a[m][n];
    dfs(0,0);//从大棋盘的第一个位置开始搜索结果
    return 0;
}

例3 子集模板

递归搜索树:第一层考虑第一个数,第二层考虑第二个数,第三层考虑第三个数
在这里插入图片描述
写法一:
模拟过程中dfs函数:
① 当前考虑的是第几个数
② 前面的数有没有选

#include <iostream>

using namespace std;

const int N = 20;
int vis[N];//vis[i]=0表示i没被选择,vis[i]=1表示i被选择
int n;

void dfs(int step)//step表示当前枚举到第几层/个数字
{
    if (step == n + 1)//递归出口
    {
        for (int i = 1;i <= n;i++)
            if (vis[i] == 1) 
                cout << i << " ";
        cout << endl;
        return;//回溯
    }
    
    vis[step] = 1;//选择当前数字i,修改标记数组
    dfs(step + 1);//搜索下一层数字
    vis[step] = 0;//恢复现场
    
    dfs(step + 1);//不选当前数字,直接搜索下一层数字
}

int main()
{
    cin >> n;
    dfs(1);//从第1个数枚举
    return 0;
}

写法二:利用栈结构实现列举子集

#include <iostream>
using namespace std;

const int N = 100;
int a[N];
int n;

//利用栈来存储结果
struct stack
{
    int res[N];//用来存储结果
    int top = 1;//栈顶指针
};
struct stack st;

void dfs(int cur)//cur表示当前面临第几个数字,选/不选
{
    if (cur == n + 1)//递归出口
    {
        for (int i = 1;i < st.top;i++) cout << st.res[i] << " ";//输出子集
        puts(" ");//换行
        return;//回溯
    }

    //选当前数字进入子集
    st.res[st.top++] = a[cur];//选,即入栈
    dfs(cur + 1);//递归搜索下一个数字
    st.top--;//恢复现场,即出栈
    
    //不选当前数字进入子集
    dfs(cur + 1);
}

int main()
{
    cin >> n;
    for (int i = 1;i <= n;i++) a[i] = i;//初始化数组a[n] = {0,1,2,3,4,5,6... ...n}
    dfs(1);//从第1个数字开始搜索
    return 0;
}

练习:

子集问题模板题

例4 马走日

原题链接

题目描述
马在中国象棋以日字形规则移动。 请编写一段程序,给定 n ∗ m 大小的棋盘,以及马的初始位置(x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点。

输入格式
第一行为整数 T,表示测试数据组数。
每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标 n,m,x,y。

输出格式
每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,若无法遍历棋盘上的所有点则输出 0。

数据范围
1 ≤ T ≤ 9 , 1 ≤m , n ≤ 9 , 0 ≤ x ≤ n − 1 , 0 ≤ y ≤ m − 1

方向数组的构造:
在这里插入图片描述
以5x5的棋盘为例,可以看出需要25步可以遍历完整个棋盘
递归出口

if (cnt == n * m) 
{
	ans++;
	return;
}

在这里插入图片描述

#include <iostream>

using namespace std;

const int N = 10;
int n,m,x,y;
int ans;//表示结果
bool vis[N][N];//表示当前点是否被遍历过
int dir[8][2] = {{-1,-2},{1,-2},{-2,-1},{2,-1},{-2,1},{2,1},{-1,2},{1,2}};//方向数组

void dfs(int x,int y,int cnt)//cnt表示在棋盘上的第几个点
{
    if (cnt == n * m)//递归出口
    {
        ans++;//棋盘被遍历完毕,方案数+1
        return;//回溯
    }
    
    for (int i = 0;i < 8;i++)
    {
        int x1 = x + dir[i][0];
        int y1 = y + dir[i][1];
        
        //根据数据范围得x不能等于n,y不能等于m,vis[x1][y1] == 1表示该点已经被遍历过
        if (x1 < 0 || y1 < 0 || x1 >= n || y1 >= m || vis[x1][y1] == 1) continue;
        
        vis[x][y] = 1;//修改标记数组
        dfs(x1,y1,cnt + 1);//遍历下一个点
        vis[x][y] = 0;//恢复现场
    }
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        ans = 0;//多组数据,ans需要重置
        cin >> n >> m >> x >> y;
        dfs(x,y,1);//从第1个点开始遍历
        cout << ans << endl;
    }
    return 0;
}

例5 N皇后问题

问题描述:同一行,同一列以及同一对角线(包括所有的主对角线和副对角线)只允许一个皇后存在

问题主体:需要解决的是找到每一行的皇后可以放置的位置/列号

以八皇后为例:
解题关键:设a[i]表示第i行的皇后放置于何处,a[i] = 3则第i行的皇后放置在第三列上

在这里插入图片描述

编写代码计算N皇后的摆法总数:

#include <iostream>

using namespace std;

const int N = 10;
int a[N];//a[N]表示第N行的皇后放在第a[N]列上,默认每行只放置一个皇后,初始时a[]全为0
int cnt = 0;//n个皇后一共有cnt种摆法
int n;

bool check(int x,int y)//验证皇后是否可以放置在第x行第y列上(以x = 5,y = 1为例)
{
    //每行只放置一个皇后,故检查各列和主、副对角线即可
    for (int i = 1;i <= x;i++)//枚举x之前的行即可
    {
        if (a[i] == y) return false;//此时i用来枚举行号,a[i] == y表示其余行的第y列有皇后
        if (x + y == i + a[i]) return false;//副对角线上已有皇后
        if (x - y == i - a[i]) return false;//主对角线上已有皇后
    }
    return true;//皇后可以放置在第x行第y列上
}

void dfs(int row)//row:行,表示在第row行寻找皇后的位置
{
    if (row == n + 1)//递归出口
    {
        cnt++;
        return;//回溯
    }
    
    for (int i = 1;i <= n;i++)//从第1行开始摆放皇后,可以摆放在1~9列,用i来枚举列
    {
        if (check(row,i))//如果可以把皇后放置在row行的i列
        {
            a[row] = i;//放置皇后于第i列
            dfs(row + 1);//搜索下一行的皇后位置
            a[row] = 0;//恢复现场
        }
    }
}

int main()
{
    cin >> n;
    dfs(1);//从第1行开始摆放皇后
    cout << cnt;//输出n皇后的摆法数量
    return 0;
}

练习:
N-皇后经典例题

例6 迷宫

在这里插入图片描述
测试样例

5 4
1 1 4 3
1 1 2 1
1 1 1 1
1 1 2 1
1 2 1 1
1 1 1 2

迷宫代码:

#include <iostream>

using namespace std;

const int N = 100;
int m,n;//地图有m行n列
int startx,starty;//表示起点坐标
int p,q;//(p,q)表示终点坐标
int ans = 0x3f3f3f3f;//ans表示最小的步数
int vis[N][N];//标记数组,0表示未被访问,1表示访问过
int a[N][N];//初始化地图,a[i][j] = 1表示空地,a[i][j] = 2表示障碍物


void dfs(int x,int y,int step)//初始点在x行y列,step表示当前走的步数
{
    if (x == p && y == q)//到达终点
    {
        if (step < ans) 
            ans = step;
            
        return;//回溯
    }
    
    if (a[x][y + 1] == 1 && vis[x][y + 1] == 0)//可以向右试探
    {
        vis[x][y + 1] = 1;//将右边的点标记为已访问
        dfs(x,y + 1,step + 1);//向右深搜
        vis[x][y + 1] = 0;//恢复现场
    }

    if (a[x + 1][y] == 1 && vis[x + 1][y] == 0)//可以向下试探
    {
        vis[x + 1][y] = 1;//将下边的点标记为已访问
        dfs(x + 1,y,step + 1);//向下深搜
        vis[x + 1][y] = 0;//恢复现场
    }

    if (a[x][y - 1] == 1 && vis[x][y - 1] == 0)//可以向左试探
    {
        vis[x][y - 1] = 1;//将左边的点标记为已访问
        dfs(x,y - 1,step + 1);//向左深搜
        vis[x][y - 1] = 0;//恢复现场
    }
    
    if (a[x - 1][y] == 1 && vis[x - 1][y] == 0)//可以向上试探
    {
        vis[x - 1][y] = 1;//将上边的点标记为已访问
        dfs(x - 1,y,step + 1);//向上深搜
        vis[x - 1][y] = 0;//恢复现场
    }
}

int main()
{
    cin >> m >> n;//读入地图的行数列数(5,4)
    cin >> startx >> starty >> p >> q;//读入起点(1,1)和终点坐标(4,3)
    
    for (int i = 1;i <= m;i++)//读入地图
        for (int j = 1;j <= n;j++)
            cin >> a[i][j];
            
    vis[startx][starty] = 1;//将起点坐标标记已搜索
    dfs(startx,starty,0);//从起点坐标开始搜索
    
    cout << ans;
    return 0;
}

使用循环改进代码:

#include <iostream>

using namespace std;

const int N = 100;
int m,n;//地图有m行n列
int startx,starty;//表示起点坐标
int p,q;//(p,q)表示终点坐标
int ans = 0x3f3f3f3f;//ans表示最小的步数
int vis[N][N];//标记数组,0表示未被访问,1表示访问过
int a[N][N];//初始化地图,a[i][j] = 1表示空地,a[i][j] = 2表示障碍物
int dx[4] = {0,1,0,-1};//方向数组
int dy[4] = {1,0,-1,0};

void dfs(int x,int y,int step)//初始点在x行y列,step表示当前走的步数
{
    if (x == p && y == q)//到达终点
    {
        if (step < ans) 
            ans = step;
            
        return;//回溯
    }
    
    for (int k = 0;k < 4;k++)
    {
        int new_x = x + dx[k];
        int new_y = y + dy[k];
        
        if (a[new_x][new_y] == 1 && vis[new_x][new_y] == 0)//是空地且未被访问,则可以试探
        {
            vis[new_x][new_y] = 1;//标记为已访问
            dfs(new_x,new_y,step + 1);//深搜
            vis[new_x][new_y] = 0;//恢复现场
        }
    }
}

int main()
{
    cin >> m >> n;//读入地图的行数列数(5,4)
    cin >> startx >> starty >> p >> q;//读入起点(1,1)和终点坐标(4,3)
    
    for (int i = 1;i <= m;i++)//读入地图
        for (int j = 1;j <= n;j++)
            cin >> a[i][j];
            
    vis[startx][starty] = 1;//将起点坐标标记已搜索
    dfs(startx,starty,0);//从起点坐标开始搜索
    
    cout << ans;
    return 0;
}

例7 染色

DFS总结与模板

首先,以一个未被访问过的顶点作为起始顶点,沿当前顶点的边走到未访问过的顶点,当没有未访问过的顶点时,则回到上一个顶点,继续试探访问别的顶点,直到所有的顶点都被访问过。显然,深度优先遍历是沿着某一条分支遍历直到末端(到达边界),然后回溯,再沿着另一条进行同样的遍历,直到所有的顶点都被访问过为止.在这里插入图片描述

理解DFS算法的关键:DFS在于解决"当下该如何做"。至于"下一步如何做"其实和"当下该如何做"是一样的过程

实现的方法一般是递归(自己调用自己)

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值