前言
唤我沈七就行。
又是拖更的两周~
因为开学将至,学校竞赛班也要在开学前的月底来一场测试,所以我就加快了学习算法的进度,最近两周涉猎了DFS、BFS、背包DP、线性DP。也整理了不少笔记,很快就会陆续更新啦~
今天带来的是骗分神器–DFS
基本概念
DFS:是Depth First Search的简称 ,即深度优先搜索算法:
是一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支。
当节点v的所在边都己被探寻过或者在搜寻时结点不满足条件,搜索将回溯到发现节点v的那条边的起始节点。整个进程反复进行直到所有节点都被访问为止。
DFS是通过递归来实现的,也就是函数自己调用自己。
所以通俗的说DFS通过递归枚举所有情况,然后从中找到与题设相符的方案。
因为要枚举所以情况属于盲目搜索,最糟糕的情况算法时间复杂度为O(!n)。
所以DFS不算是最有效率的算法,但一定是考虑情况最全面的算法。
算法思想
回溯法是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。
但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
回溯是DFS最精妙的一步,在算法模板中往往体现在恢复现场。
即在调用完函数自身后,恢复初始状态。
这是因为DFS要枚举所有情况,而枚举方式不是想BFS那样一层一层的,而是一条路走到尾,他每走一步都需要在走的路上做上标记,防止重复走同一条路。
如果没路了,再通过回溯,走下条路。而回溯在这里的作用就是按原路返回,但是因为路上已经做了标记,所以回溯其实就是撤销上一步做的标记(恢复现场)。不然是没办法返回走下一条路的。
如果不理解的没关系,我当初学的时候也是懵懵懂懂,先接着往下看~
常用模板
void dfs(int step) { 判断边界//判断是否符合条件 { 相应操作 } 枚举每一种可能 { 标记 //防止多次重复枚举同一个方案 继续下一步dfs(step+1) 恢复初始状态(回溯的时候要用到)//恢复现场 //这就是DFS的精髓之处 //为的就是不影响下一种方案的枚举 } }
三种枚举方式
DFS通常有三种枚举方式,接下来我们一一来介绍
指数型枚举
从 1∼n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案
分析: 从1~n依次考虑 选 和 不选 两种情况。 一共 n 个数,每个数有 2 种情况。总共方案数为 2的n次方。所以被称为指数型枚举。
详细的解释都在注释里
const int N = 1e1 + 6; 定义一个常量N int n; int st[N]; 记录每个位置当前的状态:0表示还没考虑,1表示选它,2表示不选它 void dfs(int u) 枚举的第几个数字 { if(u > n) { 终止条件,因为题目要求一个就n个数 所以只有 u>n 就输出枚举的方案 for(int i = 1; i <= n; i++) if(st[i] == 1) printf("%d ", i); puts(""); return; } st[u] = 1; 选它的分支 dfs(u + 1); st[u] = 0; 恢复现场,以便进行下一个分支 st[u] = 2; 不选它的分支 dfs(u + 1); st[u] = 0; 恢复现场 } int main(void) { scanf("%d", &n); dfs(1); return 0; }
排列型枚举
把 1∼n 这 n 个整数排成一行后随机打乱顺序,输出所有可能的次序。
分析:依次枚举 1~n数 放到哪个位置,排列问题需要考虑顺序,需要book数组来判重。
const int N = 10; int st[N]; 存储方案 bool book[N]; 标记数字是否被用过,true表示被用过,false表示没被用过 定义在里全局变量,所以现在book数组都是false 表示都没被用过 int n; void dfs(int u){ 当前枚举第u位 if(u > n){ for(int i = 1; i <= n; i ++) printf("%d ", st[i]); 打印方案 puts(""); return ; } for(int i = 1; i <= n; i ++){ 依次枚举每个数 if(!book[i]) 如果当前数没被用过 { st[u] = i; 在第u位是i book[i] = true; 标记用过 dfs(u + 1); 递归到下一位 恢复现场,以便回溯其他情况 st[u] = 0; 可省略 因为写不写都会被 st[u]=i; 这一层给覆盖掉 book[i] = false; 不可省 } } } int main(){ scanf("%d", &n); dfs(1); return 0; }
组合型枚举
从 1∼n 这 n 个整数中随机选出 m 个,按字典序输出所有可能的选择方案。
组合问题和排列问题不同,不需要考虑顺序,即 1234 和 4321 都是一种组合.
因为要按字典序输出,所以需要从上一次枚举的数开始来依次枚举,控制局部枚举的数,使得新枚举的数比之前的大。
组合问题不需要考虑顺序,需要x来记录上一次的枚举的数。这样也就可以保证同一种组合只有一种顺序被打印。
因为是从上一次枚举的数开始枚举所以不会保存选用重复数字, 所以不需要定义去重数组book。
int n,m; int cnt[30]; 记录方案 void dfs(int u,int x){ u 记录枚举了几位 x记录了枚举了到了几 if(u>m) { 边界 for(int i=1;i<=n;i++) cout<<cnt[i]<<" "; puts(""); 换行 return; } for(int i=x;i<=n;i++) 从x开始枚举 { cnt[u]=i; dfs(u+1,i+1); cnt[u]=0; 恢复现场 } } int main() { cin>>n>>m; dfs(1,1); return 0; }
完结散花
ok以上就是对 暴力搜索之DFS 的全部讲解啦,很感谢你能看到这儿啦。如果有遗漏、错误或者有更加通俗易懂的讲解,欢迎小伙伴私信我,我后期再补充完善。
题目练习
检验知识是否学会当然是不断刷题啦,下面我给出一部分DFS相关的一些经典题目。
后续如果我有时间就更新这部分知识的题解,到时候就可以配套学习啦。
参考文章
参考文献
https://blog.csdn.net/weixin_43272781/article/details