稳重的DFS学习

(题目的意思是我wenzhong的DFS学习,被我骗进来了吧)

蓝桥杯考试在即,真的贼慌,找了点题刷了刷,更慌了,尤其是做了14年B组真题,感觉自己去了就是炮灰,本来想着发个线性dp和背包dp的文章,结果做题的时候发现,自己连dfs都不会,吓得我连忙来写个dfs学习巩固一下

注:以下学习过程,如果在数据结构课上老师讲过图的基本dfs(我破二本老师都会讲,应该所有老师都会讲吧,不会的看这里),对于理解起来应该简单些,但是我毕竟想要打蓝桥杯,所以写的东西偏向于算法。

(其实无论怎么样,dfs都是一种搜索的算法,基本上会把所有的可能都会访问到,所以这种算法的时间复杂度肯定很高,一般来说会是n的阶乘,甚至是n^n,数值一般不会太大,数据范围差不多就是10)

越是做dfs越能感觉得到,dfs就是一种栈的思想,先进后出

那么废话不多说,上题目辣

目录

第一题

理解一下题意,直接上代码 

第二题 

理解一下题目,直接上代码 

第三题

先看我的代码

那么接下来,我直接上剪枝优化

归纳和总结


第一题

题目源地址

 P1784 数独

 题目描述

数独是根据9*9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫内的数字均含 1 - 9 ,不重复。每一道合格的数独谜题都有且仅有唯一答案,推理方法也以此为基础,任何无解或多解的题目都是不合格的。

芬兰一位数学家号称设计出全球最难的“数独游戏”,并刊登在报纸上,让大家去挑战。

这位数学家说,他相信只有“智慧最顶尖”的人才有可能破解这个“数独之谜”。

据介绍,目前数独游戏的难度的等级有一到五级,一是入门等级,五则比较难。不过这位数学家说,他所设计的数独游戏难度等级是十一,可以说是所以数独游戏中,难度最高的等级。他还表示,他目前还没遇到解不出来的数独游戏,因此他认为“最具挑战性”的数独游戏并没有出现。

输入格式

一个未填的数独。

输出格式

填好的数独。

输入输出样例


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


输出 
8 1 2 7 5 3 6 4 9 
9 4 3 6 8 2 1 7 5 
6 7 5 4 9 1 2 8 3 
1 5 4 2 3 7 8 9 6 
3 6 9 8 4 5 7 2 1 
2 8 7 1 6 9 5 3 4 
5 2 1 9 7 4 3 6 8 
4 3 8 5 2 6 9 1 7 
7 9 6 3 1 8 4 5 2

理解一下题意,直接上代码 

#include<bits/stdc++.h>
using namespace std;
vector<vector<int>> dt(10,vector<int>(10,-1));
//检查函数,什么?检查什么??你说检查什么!!!
bool check(int x,int y,int n){
    // 检查行
    for(int j=1;j<=9;j++)
        if(dt[x][j]==n) return false;
    // 检查列
    for(int i=1;i<=9;i++)
        if(dt[i][y]==n) return false;
    // 检查3x3宫格
    int bx=(x-1)/3 * 3+1, by=(y-1)/3 * 3+1;
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if(dt[bx+i][by+j]==n) return false;
    return true;
}

void dfs(int x,int y,bool& f){
    //顺序4,补全特殊情况
    if (f)return ;
    if (x==10) {
        f=true;
        return;//正常退出位置,说明找到了
    }

    /*但是聪明的你发现了,回溯的时候不会执行这一句
    解决方法两种,第一种转变为bool,在循环过程中加一个return
    第一种改变代码太多,我直接写第二种,增加一个标识的变量*/


    //这一步可以放在循环里面去写,写在外面是因为我改代码的时候拿出来测试错误了
    //感官上来说,更清晰一点不是吗
    //这一步是给下一个调用的dfs的坐标位置
    int xx = x, yy = y + 1;
    if (yy == 10) {
        xx++;
        yy = 1;
    }
    //顺序3,给空位置赋值
    if (dt[x][y]==0){
        for(int i=1;i<=9;i++){ //进行赋值操作

            if(check(x,y,i)){
                dt[x][y]=i;

                dfs(xx,yy,f);//写到这一步会不会有空虚的感觉
                //别急还没写完,用样例开始走一遍代码会发现漏洞百出

                if(f) return;//这一步难以理解,我写的这个变量f很不好,可是我想不到什么好办法

                dt[x][y]=0;//顺序4,回溯

            }//层层递归就会发现,这一段就有栈的思想,一直调用先进入的最后出去,最后调用的先出去
             //其实感觉这一段才是主要核心,其他地方,可以背模版
        }
    }else 
        dfs(xx,yy,f);//继续下一个为止

    return;//未能找到符合条件的,只能被迫退出
}
int main(){
    for(int i=1;i<=9;i++)
        for(int j=1;j<=9;j++)
            cin>> dt[i][j];
    //顺序1,cin数据

    bool f=false;
    dfs(1,1,f);//顺序2.建立dfs,确定起始位置,false不知道是干什么的可以先往后看

    if (f){
        for(int i=1;i<=9;i++){
            for(int j=1;j<=9;j++)
                cout << dt[i][j]<<" ";
            cout<<endl;
        }
    }else cout << "想什么呢,肯定是代码错了";

}
//恭喜你看完了我写的一大堆,如果你觉得加一个bool f太麻烦,有没有更简单的方法
//那我只能说,有的兄弟有的:exit(-1);慎用慎用

 (那么通过这个例子,有没有内种先进后出的感觉,实际是算是一种递归的路径)

(其实我写代码之后RE了,最后找AI帮看的代码,你们可以猜一下是哪里RE(Runtime Error))

第二题 

题目源地址

P1706 全排列问题

 题目描述

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

输入格式

一个整数 n。

输出格式

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

每个数字保留 5 个场宽。


输入

3


输出
    1    2    3
    1    3    2
    2    1    3
    2    3    1
    3    1    2
    3    2    1

理解一下题目,直接上代码 

#include<bits/stdc++.h>
using namespace std;
int n;
void dfs(vector<int>& nums,vector<bool>& v,int f){
    if (f==n) {
        for(int i=0;i<n;i++){
            cout  << "    " << nums[i];//这是特殊要求的输出格式
        }
        cout << endl;
        return;
    }
    for(int i=1;i<=n;i++){
        if (v[i]){
        	v[i]=false;//标记当前位置的数已经用的,下次dfs的时候就不要再用了
            nums[f]=i;

            dfs(nums,v,f+1);
            //这个f+1也是一个要理解的地方,因为暗含了回溯
            
            //回溯,记得回溯啊宝贝
            v[i]=true;
        }
    }
    return;
}
int main(){
    cin >> n;
    vector<int> nums(n,0);
    vector<bool> v(n+1,true);
    //这个标记数组的概念,可以自己拿样例试一遍,代码就是带着例子过一遍就看懂了

    //这个dfs分别送入了排列数组,标记数组 并且 新增了一个量 来表示v数组下标 会在代码中实时更新(维护了一个变量)
    dfs(nums,v,0);
    return 0;
}

(那么勤奋爱学,聪明机智的你,是否再一次感受到了栈的感觉,但是感觉又不完全一样,感觉像什么呢……树!!!!)

恰好笔者有一个全排列题的图可供参考 

(我觉得非常的生动形象)

那么好那么好,我觉得你肯定可以感觉得到,这两个题,存在基本一致的地方

整理归纳

(类型) dfs (传入数据){

        终止条件

        主体{

        按照题目要求的写

        再次递归调用dfs

        回溯

        }

return;//这个return也很重要

}//看吧,模版很短吧

传入数据:可选择的有很多,数独的题传入的xy是位置,全排列是一个数组,而且都存在一个标记v,f,可以写入的还有很多,但是根据我的经验来说,尽量就这两个,一个是位置,一个是需求的东西,其他杂七杂八的越写越乱

终止条件:像数独,把9*9的格子填完之后,会填写第十行,这个就是终止条件。全排列也简单,最长就只能排列n个数,多了就是终止

主体:这个应该就是最难的部分,要考虑的东西很多,包括回溯也在这一部分,这一部分太重要了,数独的这一部分就是,如果dfs到的这个格子没有被赋值,那我就尝试赋值继续进行dfs测试是否可行。全排列这一部分是选一个未被选择的数,进行排列,不断返回找出排列的不同结果。

以上初步养成一个规律,先读题,理解题(利用树的结构图是肢解题目过程中需要递归的地方),套用模版,逐步分析传入数据,终止条件和主体部分,完善并优化代码(回溯过程,标记数组,需求结果)

写完以上东西,感觉自己强的不可理喻,上难度上难度

第三题

原题链接

P1219 [USACO1.5] 八皇后 Checker Challenge

题目描述

一个如下的6*6的跳棋棋盘,有六个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。

上面的布局可以用序列2 4 6 1 3 5来描述,第 i 个数字表示在第 i 行的相应位置有一个棋子,如下:

行号 1 2 3 4 5 6

列号 2 4 6 1 3 5

这只是棋子放置的一个解。请编一个程序找出所有棋子放置的解。  
并把它们以上面的序列方法输出,解按字典顺序排列。  
请输出前3 个解。最后一行是解的总个数。

输入格式

一行一个正整数 n,表示棋盘是 n*n 大小的。

输出格式

前三行为前三个解,每个解的两个数字之间用一个空格隔开。第四行只有一个数字,表示解的总数。


输入
6


输出
2 4 6 1 3 5
3 6 2 5 1 4
4 1 5 2 6 3
4

数据范围 (6<=n<=13)

(有没有感觉跳跃幅度很大,其实就是未能完全的理解题)

(先抽象一遍题意,在棋盘上放置棋子,放置的时候要保证,棋子不在其他棋子的同一行,同一列,同一对角线,其实规则就是,棋子是世界象棋的皇后,在棋盘上放置n个皇后,要求皇后之间不可以互相吃到,题目名也叫八皇后嘛。最后归纳一下,我要在棋盘上放置皇后,保证皇后之间不可以互相吃,找出每一种可以摆放的可能性,并输出前三种可能和全部可能的总数) 

先看我错误的代码

#include<bits/stdc++.h>
using namespace std;
int ans=0;int n;
void check(int x,int y,vector<vector<bool>>& v){
    //同行同列标记成false
    for(int i=1;i<=n;i++){
        v[x][i]=false;
        v[i][y]=false;
    }
    //对角线标记成false
    //八向标记,斜向
    for(int i = 1; i <= n; i++) {
        if(x-i >= 1 && y-i >= 1) v[x-i][y-i] = false; // 左上
        if(x+i <= n && y+i <= n) v[x+i][y+i] = false; // 右下
    }
    for(int i = 1; i <= n; i++) {
        if(x-i >= 1 && y+i <= n) v[x-i][y+i] = false; // 右上
        if(x+i <= n && y-i >= 1) v[x+i][y-i] = false; // 左下
    }
    return ;
}
void dfs(int row, vector<vector<bool>> table, vector<int>& nums) {
    if(row == n+1) {
        //只要三个输出,在没满三个之前输出,满了就只++
        if(ans < 3) {
            for(int i=1; i<=n; i++) cout << nums[i] << " ";
            cout << endl;
        }
        ans++;
        return;
    }

    for(int col=1; col<=n; col++) {
        if(table[row][col]) {
            vector<vector<bool>> new_table(table); //复制一份table方便修改和回溯
            nums[row] = col;         
            check(row, col, new_table); 
            dfs(row+1, new_table, nums); 
            //虽然没有明显的回溯,但是有实际上全都回溯了
            //nums[row] = 0;可以省略
        }
    }
}
int main() {
    cin >> n;
    vector<vector<bool>> table(n+1, vector<bool>(n+1, true));
    vector<int> nums(n+1, 0);  // 索引1-n

    //题目其实有隐藏的提醒,每一行只会有一个皇后存在,所以不需要遍历整个table
    //这个是个很大的优化,要不遍历整个table包超时的
    dfs(1, table, nums); 
    cout << ans;
    return 0;
}

 那么看完上面我写的代码之后,是不是感觉自己完全ok了,实际上在佬看来还是有很大的优化空间的(因为我没有过全部样例)

那么接下来,我直接上剪枝优化

#include<bits/stdc++.h>
using namespace std;
int ans = 0;int n;
bool check(int x, int y, vector<int>& nums) {
    for (int i = 1; i < x; i++) {
        // 判断是否在同一列
        if (nums[i] == y) return false;
        // 判断是否在同一对角线,即两个坐标差值绝对值是否一样
        if (abs(nums[i] - y) == abs(i - x)) return false;
    }
    //没有判断是否在同一行,因为这个代码的逻辑就是每一行之存在一个
    return true;
}

void dfs(int row, vector<int>& nums) {
    if (row == n + 1) {
        // 只要三个输出,在没满三个之前输出,满了就只++
        if (ans < 3) {
            for (int i = 1; i <= n; i++) cout << nums[i] << " ";
            cout << endl;
        }
        ans++;
        return;
    }

    for (int col = 1; col <= n; col++) {
        // 因为删去了标记数组,没法简单粗暴的判定这个点是否可以放置皇后
        // 所以改变思路,判断当前位置在不在之前行的皇后可以吃掉的位置
        if (check(row, col, nums)) {
            // 不在之前所有皇后可以吃的地方
            nums[row] = col;
            dfs(row + 1, nums);
            nums[row] = 0;
            // 这个记得回溯啊
        }
    }
}

int main() {
    cin >> n;
    // vector<vector<bool>> table(n+1, vector<bool>(n+1, true)); 去除标记数组
    vector<int> nums(n + 1, 0);  // 索引1-n
    dfs(1, nums);
    cout << ans;
    return 0;
}//那么你看这个优化在了什么地方呢
//其一,去掉了table,是不是让dfs调用的时候更简单了
//其二,改变了for循环的判定,因为没有了table,使得无法通过棋盘判定是否可以放皇后
//所以我们改变check判定,减少多余的时间浪费
//提一个专业的,因为不需要更新table,直接的减少了时间复杂度,大概是从O(n多)变成了O(n)

还有更难的dfs,比如必须要剪枝才能执行,记忆化搜索(有点dp的感觉)

我还在网上看,有的老师会把dfs分成两类(递归dfs和用栈的dfs,但是我比较菜我看不懂)

附加图片和讲解视频地址

讲解视频源地址

归纳和总结

那么小小的归纳一下写的dfs吧

dfs说白了就是一种利用空间换取时间的算法,会因为大量的调用,如果数值过大,会很容易段错误TLE,那么每次调用dfs的时候,尽量不要多的产生过多计算(最简单的方法就是,dfs的形参尽可能的少)

而我们在抽象题的时候,一般dfs都会成为一个树状结构,利用树状结构我们就可以清晰的看到递归的流程,而抽象的树状结构越是清晰,就越是容易剪枝,所以说,做题,尤其是dfs,把题抽象出来,得到本质最关键,也就是得到主体部分 。

(那么好,算法dfs就暂时写到2025年4月9日11:07:46)

(封面AI生成,不出意外,算法的文章都用这个图片了)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值