DFS提高——DFS常见模型
一、概述
DFS只能求出来是否能从起点到终点,不能求出来最短距离。
DFS一般代码比BFS要短一些因为不用维护队列,缺点是递归太深容易爆栈,栈空间应该是1MB左右
1、DFS——连通型
包括在BFS中见到的Flood Fill类型、图和树的遍历
一般题上是问能否从A点走到B点,或者从这个点出发能到达多少个符合要求的点
这类题一般是从棋盘上一个点到另一个点,每个点只需要走一遍,所以不需要恢复现场。
例题:1113. 红与黑 - AcWing题库
代码模板:
#include<iostream> #include<cstring> using namespace std; const int N=25; char g[N][N]; bool st[N][N]; int n,m,cnt=1,bex,bey,T; int dx[4]={0,-1,0,1};//左上右下 int dy[4]={-1,0,1,0}; void dfs(int x,int y) { for(int i=0;i<4;i++) { int tx=x+dx[i]; int ty=y+dy[i]; if(tx<0 || tx>=n || ty<0 || ty>=m)//出界 continue; if(st[tx][ty] || g[tx][ty]=='#')//用过了或者不能走 continue; st[tx][ty]=1;//标记 cnt++;//总数+1 dfs(tx,ty);//遍历下一个 } return ; } int main() { cin.tie(0); ios::sync_with_stdio(false); int a,b; while(cin>>m>>n,n||m)//n,m=0时结束 { memset(st,0,sizeof st);//别忘了多组数据时要重置st的 cnt=1; for(int i=0;i<n;i++) { for(int j=0;j<m;j++) { cin>>g[i][j]; if(g[i][j]=='@')//记下终点坐标 { bex=i; bey=j; } } } st[bex][bey]=1; dfs(bex,bey); cout<<cnt<<endl; } return 0; }
2、DFS——搜索顺序
搜索顺序类型的题,一般要考虑把所有顺序全部枚举到。
而且需要考虑恢复现场的问题,因为它是把当前棋盘看成一个整体,把棋盘当前的状态,转移成目标状态。
所以我们每次在搜索过程中,标记一个点往下DFS,回来的时候要把这个标记取消掉,方便这一层其他点进行DFS。
例题:1116. 马走日 - AcWing题库
代码模板:
#include<iostream> #include<cstring> using namespace std; const int N=30; int n,m,bx,by,T,res; bool st[N][N]; int dx[8]={-1,-2,-2,-1,1,2,2,1};//从左上顺时针到左下 int dy[8]={-2,-1,1,2,-2,-1,1,2}; void dfs(int x,int y,int cnt) { if(cnt==n*m)//结束条件 { res++; return ; } for(int i=0;i<8;i++)// { int tx=x+dx[i]; int ty=y+dy[i]; if(tx<0 || tx>=n || ty<0 || ty>=m)//出界 continue; if(st[tx][ty])//遍历过了 continue; st[tx][ty]=1;//标记遍历过了 dfs(tx,ty,cnt+1); st[tx][ty]=0;//恢复现场 } return ; } int main() { cin.tie(0); ios::sync_with_stdio(false); cin>>T; while(T--) { memset(st,0,sizeof st);//每次要重置st数组 res=0; cin>>n>>m>>bx>>by; st[bx][by]=1;//起点标记 dfs(bx,by,1); cout<<res<<endl; } return 0; }
3、DFS优化——剪枝与优化
常用5个方法:
1、优化搜索顺序
大部分情况下我们应该先搜分支较小的节点,一般通过对传入的数据先排个序
2、排除等效冗余
有些题在DFS并不需要考虑顺序,搜1 3 2 和搜1 2 3 是一个结果,一般我们用一个start变量记录当前搜到哪个点就行,下次从这个点往后,之前的不再搜索。
3、可行性剪枝
最常见的剪枝方式,如果不符合条件直接跳过
4、最优性剪枝
一般可以初始化一个变量记录当前最优解,如果递归时还没结束就已经超过了最优解,那么明显就不是答案,直接结束。
5、记忆化搜索(DP)
DP问题了,在这里不多讲
例题:167. 木棒 - AcWing题库
代码模板:
#include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 70; int n; int w[N];//小木棍长度 int sum,length;//小木棍长度总和,当前每根木棍长度 bool st[N];//标记哪些小木棍用过了,1代表用过了 bool dfs(int u,int cur,int start)//u是大棍个数,cur是当前大棍长度,start记录从哪开始遍历 { if(u*length==sum)//如果大棍个数*大棍长度=总和 return true;//成功 if(cur==length) //如果当前大棍的长度已经等于应该的长度,开一个新的大棍 return dfs(u+1,0,0); for(int i=start;i<n;i++)//遍历小棍 { if(st[i]||cur+w[i]>length) //如果当前小棍已经遍历过 或者 加上这个小棍大棍长度超了 continue;//跳过 st[i]=true;//标记小棍 if(dfs(u,cur+w[i],i+1))//递归 return true; st[i]=false;//恢复现场 if(!cur||cur+w[i]==length)//如果当前大棍长度为0 或者加上小棍刚好为length return false;//剪枝优化2,如果一个大棍的开头或者结尾失败,这层就失败 int j=i; while(j<n&&w[j]==w[i])//剪枝优化3,如果当前小棍不行,和它长度相同的都不行 j ++ ; i=j-1; } return false; } int main() { while(cin>>n,n)//读入,n==0时结束 { memset(st,0,sizeof st);//每次重置状态数组 sum=0;//记录小棍总和 for(int i=0;i<n;i++) { cin>>w[i];//读入小棍长度 sum+=w[i];//计算总和 } sort(w,w+n,greater<int>());//从大到小排序,搜索顺序的优化 length=1;//初始假设每根大棍长度应该为1,我觉得可以初始为最长的小棍长度. while(true)//剪枝优化1,大棍长度一定是总长度的约数 { if(sum%length==0 && dfs(0,0,0))//能整除而且可以让全部大棍长度为length { cout<<length<<endl;//就是最优解,因为length从小到大枚举 break; } length++;//如果不行长度+1 } } return 0; }
4、DFS优化——迭代加深优化
一般是部分情况DFS会搜到很深的地方,但是答案在另一个很浅的分支里,
而迭代加深的思想,就是我每次规定一个最深搜索层数depth,如果每找到答案,就把depth++
如果有一个10层的树,
考虑一个一般情况,
如果答案在第3层(如下面图左上角的树),我们要是用迭代加深优化只用搜到第3层就结束了,如果是一般DFS我们会将第一个分支搜完,把第二个分支搜完,一直搜到第三个分支才能搜到答案
乍一看没差多少,但是由于DFS中每层的点按照指数级增长,每一个分支你搜到最深处会很费时间,只要我们“远远的看见是个死胡同”,就不用进去了,迭代就是让我们“远远的看”。
再看一个最坏情况,我们假设是一个满二叉树(除了最后一层每个点都有两个儿子)
如果有一个10层的树,答案就在在第10层,对于第10层我们要搜2^10,前九层加起来一共2^10-1,所以即使再最坏情况下,也不会低于正常DFS,而且这还是只有两个儿子,当儿子数更多时,迭代的效果会非常明显。
例题:170. 加成序列 - AcWing题库
代码模板:
#include<iostream> #include<cstring> using namespace std; const int N=110; int n,p[N];//p记录路径 bool dfs(int u,int depth)//u为当前深度,depth为限制的最深深度 { if(u==depth)//当当前深度已经到达限制 return p[u-1]==n;//判断当前最后一个数是否等于n bool st[N]={0};//将st所有初始化为0,用来记录用过的数字 for(int i=u-1;i>=0;i--)//二重遍历,遍历u前所有可能的两个数的和 { for(int j=i;j>=0;j--) { int tem=p[i]+p[j]; if(tem<=p[u-1] || st[tem] || tem>n)//如果和小于上一位 或 大于n 或 已经用过了 continue;//跳过 st[tem]=1;//标记 p[u]=tem;//记下路径,不用恢复现场的原因是后面会覆盖当前值 if(dfs(u+1,depth))//如果后面成功了 return true; } } return false; } int main() { p[0]=1;//第一个点一定是1 while(cin>>n,n) { int depth=1;//深度为1,尽管每次会重复搜索,但把每层加起来的和也小于答案层的点的个数,当每个点分支越多时,就可以忽略掉 while(!dfs(1,depth))//当当前深度没有答案时,深度+1 depth++; for(int i=0;i<depth;i++)//输出答案 printf("%d ",p[i]); printf("\n"); } return 0; }
5、DFS优化——双向DFS
原理和双向BFS相类似,双向搜索可以减少中间指数级增长的搜索量。
例题:171. 送礼物 - AcWing题库
代码模板:
#include<iostream> #include<algorithm> #include<vector> using namespace std; typedef long long LL; const int N=1<<24; int n,m,k;//n是物品个数,m是最大重量,k是前半暴搜数量 int g[50],weights[N];//g是每个物品重量,weight是每种可能的和 int res,cnt;//res是答案,cnt是计数 void dfs(int u,int s)//u是当前遍历到的下标,s是当前的和,先暴搜前一半,打表 { if(u==k)//当下标遍历完时 { weights[cnt++]=s;//记录一下和 return ; } if((LL)s+g[u]<=m)//当 当前的和 与 当前下标对应的值 的和 小于最大重量 dfs(u+1,s+g[u]);//选择这个物品,继续递归 dfs(u+1,s);//不选这个物品 } void dfs2(int u,int s)//u是当前遍历到的下标,s是当前的和,暴搜后一半 { if(u==n)//当搜索到最后一个位置了,二分查找 { int l=0,r=cnt-1; while(l<r) { int mid=l+r+1>>1; if(weights[mid]+(LL)s<=m)//当当前中间值+当前的和<=最大重量时 l=mid; else r=mid-1; } if(weights[l]+(LL)s<=m)//当找出来的大于等于差值的最小值符合条件 res=max(res,weights[l]+s);//更新最优解 return; } if((LL)s+g[u]<=m) //如果当前的和+当前下标对应的物品的重量没有超过限制 dfs2(u+1,s+g[u]);//选这个物品 dfs2(u+1,s);//不选这个物品 } int main() { cin>>m>>n; for(int i=0;i<n;i++)//读入物品重量 cin>>g[i]; sort(g,g+n,greater<int>());//从大到小排序,优化搜索顺序 k=n/2;// 不+2是为了防止 n = 1时,出现死循环 dfs(0,0);//对前半数据打表 sort(weights,weights+cnt);//把表从小到大排序方便二分 int t=1;//手动去重 for(int i=1;i<cnt;i++) if(weights[i]!=weights[i-1]) weights[t++]=weights[i]; cnt=t; dfs2(k,0);//从k开始 cout<<res<<endl; return 0; }
6、DFS优化——IDA*
一般来说配合迭代加深一起。思想上和A*算法差不多,都是找一个估计函数。
例题:180. 排书 - AcWing题库
代码模板:
#include<cstring> #include<iostream> #include<algorithm> using namespace std; const int N = 15; int n; int q[N]; int w[5][N]; int f()//估计函数 { int cnt=0; for(int i=1;i<n-1;i++) if(q[i]!=q[i-1]+1)//如果当前位的数字不是上一位数字+1 cnt++;//不符合顺序的个数+1 return (cnt+2)/3;//一次操作最多修正3个顺序,向上取整计算估计距离 } bool check()//检查是否全部有序 { for(int i=0;i<n-1;i++) if(q[i+1]!=q[i]+1) return false; return true; } bool dfs(int depth,int max_depth)//depth是当前深度,max_depth是最大深度的限制 { if(depth+f()>max_depth)//如果当前深度加上预估函数值大于最大深度,跳出 return false; if(check())//如果当前满足题目要求,跳出 return true; for(int len=1;len<=n;len++)//遍历长度 { for(int l=0;l+len-1<n;l++)//遍历起点位置 { int r=l+len-1;//r为端的末尾 for(int k=r+1;k<n;k++)//从r+1开始遍历所有点 { memcpy(w[depth],q,sizeof q);//w暂存每层的状态,方便恢复现场 int x,y;// for(x=r+1,y=l;x<=k;x++,y++)//x从r+1走到k,先把r+1~k之间的字母移动到前面位置 q[y]=w[depth][x]; for(x=l;x<=r;x++,y++)//再把l~r即要插入的字段放到上面字段的后面 q[y]=w[depth][x]; if(dfs(depth+1,max_depth))//如果递归得到了答案,就结束 return true; memcpy(q,w[depth],sizeof q);//恢复现场,把之前的状态再恢复回来 } } } return false;//递归全部还是没找到答案,失败 } int main() { int T; cin>>T; while(T--) { cin>>n; for(int i=0;i<n;i++) cin>>q[i]; int depth=0; while(depth<5 && !dfs(0,depth))//迭代加深 depth++; if(depth>=5) puts("5 or more"); else cout<<depth<<endl; } return 0; }