搜索:BFS、DFS、剪枝
搜索问题,首先需要表示出题目的状态空间,即初始状态、可达状态和终结状态,并确定它们之间的转移条件。之后需要确定合适的搜索方式,这个搜索方式需要保证可以到达所有的可达情况而且没有死循环。最后,估计复杂度酌情剪枝或改变搜索方式。
关键在于:表示出完整的状态空间,根据状态空间大小估计时间复杂度,选择合适的解题模型
BFS是树的按层遍历的过程。题目要找到最少的操作数,可以考虑用BFS来搜索。因为BFS按层搜索的特性,在队列中发现第一个符合条件的位置,一定是最优解之一。BFS在图上以邻接矩阵的方式遍历,复杂度O(n2),邻接表为O(n+e)
较难的BFS问题,状态转移会有其他的附加条件。这种情况下,原来简单的BFS找队列中出现的第一个元素是不适用的。考虑新添一些状态,以此来覆盖完整的状态空间。例洛谷P3659,构造出完整的状态空间后遍历即可。
有一类问题需要来考虑分层图。关于分层图的具体思路和做法,见博客【常用技巧:分层图】。将题目提供的多种选择构造为新的边,这样状态空间就变得完整同时简单。在新图上遍历即可。例洛谷P1073
还有一类问题,需要结合状态压缩。基本模型是去往指定地点,但是图上有几种锁,需要找到特定的钥匙才能打开锁继续前进。这种问题,首先构造一个函数,可以求出将存放钥匙的单元格的钥匙状态。原本的图再添加一个钥匙的状态,得到完整的状态空间。当遇到锁时,看当前的钥匙状态是否可以和锁匹配,以这样的方式进行BFS即可解决问题。例洛谷P4011
#include<bits/stdc++.h> using namespace std; int n,m,p,k,v,maze[15][15][15][15],go[4][2]={0,1,0,-1,1,0,-1,0},cnt[15][15],key[15][15][15]; bool mark[15][15][2000]; struct node { int x,y,k,d; }; queue<node> q; int getkey(int x,int y) { int ans=0; for(int i=1;i<=cnt[x][y];i++){ ans|=(1<<(key[x][y][i]-1)); } return ans; } int BFS() { int startx=1,starty=1,startk; startk=getkey(startx,starty); q.push((node){startx,starty,startk,0}); mark[startx][starty][startk]=true; while(!q.empty()){ node now=q.front(); q.pop(); if(now.x==n&&now.y==m){ return now.d; } for(int i=0;i<4;i++){ int x=now.x+go[i][0]; int y=now.y+go[i][1]; int pass=maze[now.x][now.y][x][y]; if(x<=0||x>n||y<=0||y>m||pass<0||(pass&&(!(now.k&(1<<(pass-1)))))) continue; int nextk=now.k|getkey(x,y); if(mark[x][y][nextk]) continue; q.push((node){x,y,nextk,now.d+1}); mark[x][y][nextk]=true; } } return -1; } int main() { scanf("%d%d%d",&n,&m,&p); scanf("%d",&k); for(int i=1;i<=k;i++){ int a,b,c,d,e; scanf("%d%d%d%d%d",&a,&b,&c,&d,&e); if(e==0) maze[a][b][c][d]=maze[c][d][a][b]=-1; else maze[a][b][c][d]=maze[c][d][a][b]=e; } scanf("%d",&v); for(int i=1;i<=v;i++){ int a,b,c; scanf("%d%d%d",&a,&b,&c); cnt[a][b]++; key[a][b][cnt[a][b]]=c; } printf("%d\n",BFS()); return 0; }
DFS与树的先根遍历类似,是一个递归的过程。DFS相对于BFS代码量更小而且无需维护其他数据结构,所以可选择的情况下优先选择DFS。对于“能否到达”“是否存在”这样的简单搜索问题,DFS会更好些。
DFS常与枚举相结合。枚举可能得不到想要的结果,这时需要回溯,与DFS递归的过程完美契合。对于数独问题,枚举+DFS,或者是舞蹈链(听说的)。洛谷P1784
#include<bits/stdc++.h> using namespace std; int maze[15][15],x[100],y[100],num; bool flag; bool judge(int k,int n) { for(int i=1;i<=9;i++){ if(maze[x[n]][i]==k||maze[i][y[n]]==k) return false; } int a=(x[n]-1)/3*3; int b=(y[n]-1)/3*3; for(int i=1;i<=3;i++){ for(int j=1;j<=3;j++){ if(maze[i+a][j+b]==k) return false; } } return true; } void dfs(int n) { if(n==num){ for(int i=1;i<=9;i++){ for(int j=1;j<=9;j++){ if(j!=1) printf(" "); printf("%d",maze[i][j]); } printf("\n"); } flag=true; return; } else{ for(int i=1;i<=9;i++){ if(judge(i,n)&&!flag){ maze[x[n]][y[n]]=i; dfs(n+1); maze[x[n]][y[n]]=0; } } } return; } int main() { for(int i=1;i<=9;i++){ for(int j=1;j<=9;j++){ scanf("%d",&maze[i][j]); if(maze[i][j]==0){ x[num]=i; y[num]=j; num++; } } } dfs(0); return 0; }
难度更大一点,DFS用于实现记忆化搜索。记忆化搜索是一种动态优化思想,可以避免重复的搜索和访问。
剪枝:通过预先判断,放弃搜索不可能是解的点,来节省时间
(1)可行性剪枝,目前的状态不满足题目的某些要求,并且其子节点也一定不满足要求。dfs下走一条没有重复节点的路,如果当前点左右走过上下没走过或是上下走过左右没走过,可以直接剪掉,洛谷P1585。
(2)最优性剪枝,目前搜索到的节点的后续最优情况也不比当前的最优解好,就停止对当前节点的搜索,回溯到父节点搜索其他情况。常见的比如求最优解,如果发现当前情况下其他点就算取到最优值也不如目前的最优解,就可以直接剪掉了,洛谷P1559。
(3)交换搜索顺序,严格来说是对搜索方式的优化。改变搜索顺序可能会减少搜索的总状态数,有选择的挑选分支小的子树优先搜索。比如数独问题,优先选择空缺少的行上的点进行搜索,洛谷P1074。
对于DFS的题目,如果提交之后MLE,大概率是dfs爆栈,需要对深搜过程进行剪枝;TLE也是类似。


浙公网安备 33010602011771号