1. Flood Fill 和 最短路模型:
基础的宽搜题,一般普通宽搜板子就能过
题目链接:
Flood Fill:
最短路模型:
2. 多源BFS:
对于有多个起点的宽搜来说如果按照单源宽搜模板,做法将是:将每一个起点跑一边宽搜板子,每个点记录一下 ----> (在该起点的情况下,该点到起点的最短路),最后取一个min。
这个做法是不可取的,既增加了码量,又大大提高了时间复杂度。
正确做法是将所有起点入队,根据宽搜队列的单调性,保证了某个点只要能够被搜到(第一次),就是正确答案,再将其入队即可。
题目链接:
代码如下:#include<bits/stdc++.h> using namespace std; #define sf(x) scanf("%lld",&x) #define sff(x,y) scanf("%lld%lld",&x,&y) #define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0) #define pii pair<int,int> #define f first #define s second #define int long long const int N=1010; int m,n; char g[N][N]; //地图 int d[N][N]; //每个点到起点的最短路 int vx[]={0,0,1,-1},vy[]={1,-1,0,0}; //向量数组 queue<pii> qu; void bfs() { while(qu.size()) { auto t=qu.front(); qu.pop(); for(int i=0;i<4;i++) { int x=vx[i]+t.f,y=vy[i]+t.s; if(x<0||x>=m||y<0||y>=n) continue; if(d[x][y]!=-1) continue; d[x][y]=d[t.f][t.s]+1; qu.push({x,y}); } } } signed main() { sff(m,n); //方便判断此点是否被搜过,少写一个st数组 memset(d,-1,sizeof d); for(int i=0;i<m;i++) { for(int j=0;j<n;j++) { cin>>g[i][j]; //将所有起点入队 if(g[i][j]=='1') qu.push({i,j}),d[i][j]=0; } } //跑一遍bfs即可 bfs(); //输出d数组即答案 for(int i=0;i<m;i++) { for(int j=0;j<n;j++) cout<<d[i][j]<<" "; puts(""); } }
3. 最小步数模型:
最小步数模型其实就是将原来的最短路模型中的每个点抽象成了一种状态,一种状态操作一次后变成另一种状态,其实就等价于原来的一个点走到另一个点(距离为1)
题目链接:AcWing 1107. 魔板
代码如下:
#include<bits/stdc++.h> using namespace std; #define sf(x) scanf("%lld",&x) #define sff(x,y) scanf("%lld%lld",&x,&y) #define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0) #define pii pair<int,int> #define f first #define s second #define int long long #define umap unordered_map string sta="12345678",en,res; umap<string,int> d; //存每一个状态到初始状态的距离 umap<string,pair<char,string>> las; //存每个状态由哪个状态转移过来和所用的操作 //我选择直接暴力哈哈 string caozuo_A(string a) { reverse(a.begin(), a.end()); return a; } string caozuo_B(string a) { string b = "--------"; b[0] = a[3]; for (int i = 0; i < 3; i++) b[i + 1] = a[i], b[i + 4] = a[i + 5]; b[7] = a[4]; return b; } string caozuo_C(string a) { string b = "--------"; b[0] = a[0]; b[1] = a[6]; b[2] = a[1]; b[3] = a[3]; b[4] = a[4]; b[5] = a[2]; b[6] = a[5]; b[7] = a[7]; return b; } void bfs() { queue<string> qu; qu.push(sta); d[sta]=1; while(qu.size()) { string t =qu.front(); qu.pop(); string str[5]; str[0]=caozuo_A(t); str[1]=caozuo_B(t); str[2]=caozuo_C(t); for(int i=0;i<3;i++) { string ss=str[i]; if(d[ss]) continue; d[ss]=d[t]+1; las[ss]={'A'+i,t}; if(ss==en) return ; qu.push(ss); } } } signed main() { int t; for(int i=0;i<8;i++) cin>>t,en+=to_string(t); bfs(); cout<<d[en]-1<<endl; //因为开始设的起点到自己的距离为1 while(sta!=en) //倒推一遍 { auto t=las[en]; res+=t.f; en=t.s; } reverse(res.begin(),res.end()); cout<<res; }
4.双端队列广搜
在广搜中要想保证答案正确,就必须保证队列中元素的具有两端性和单调性(单调递增),如果队列中前面的元素大于后面的元素,则不能保证搜出来的答案最优,一般的广搜权值都是1所以就正常插入队尾就能保证队列的单调性和两端性,当权值有1和0时,如果还一股脑插入队尾,则不能保证队列的单调性,假设a和b为队列中前两个元素,d[a]=d[b]=x;a先出队,搜到的下一个点的权值为1,将x+1插入队尾,在让b出队,假设b搜到的点权值为0,则插入队尾的值为x,x+1>x,所以该队列不是一个单调递增的队列,不能保证答案的正确性
。
那该怎样解决呢,这是后就可以用双端队列来解决,假设搜到的点为0则插入队头,如果为1,则插入队尾,这样一定可以保证队列的单调性和两端性,故能保证答案正确。
题目链接:AcWing 175. 电路维修
代码如下:
#include <cstring> #include <iostream> #include <algorithm> #include <deque> #define x first #define y second using namespace std; typedef pair<int, int> PII; const int N = 510, M = N * N; int n, m; char g[N][N]; int dist[N][N]; bool st[N][N]; int bfs() { memset(dist, 0x3f, sizeof dist); memset(st, 0, sizeof st); dist[0][0] = 0; deque<PII> q; //用双端队列来存 q.push_back({0, 0}); char cs[] = "\\/\\/"; int dx[4] = {-1, -1, 1, 1}, dy[4] = {-1, 1, 1, -1}; int ix[4] = {-1, -1, 0, 0}, iy[4] = {-1, 0, 0, -1}; while (q.size()) { PII t = q.front(); q.pop_front(); if (st[t.x][t.y]) continue; st[t.x][t.y] = true; for (int i = 0; i < 4; i ++ ) { int a = t.x + dx[i], b = t.y + dy[i]; if (a < 0 || a > n || b < 0 || b > m) continue; int ca = t.x + ix[i], cb = t.y + iy[i]; int d = dist[t.x][t.y] + (g[ca][cb] != cs[i]); if (d < dist[a][b]) { dist[a][b] = d; if (g[ca][cb] != cs[i]) q.push_back({a, b}); else q.push_front({a, b}); } } } return dist[n][m]; } int main() { int T; scanf("%d", &T); while (T -- ) { scanf("%d%d", &n, &m); for (int i = 0; i < n; i ++ ) scanf("%s", g[i]); int t = bfs(); if (t == 0x3f3f3f3f) puts("NO SOLUTION"); else printf("%d\n", t); } return 0; }
5.双向广搜
双向广搜是用两个队列,一个队列从起点开始搜,一个队列从终点开始搜(已知了其实状态和终点状态),在搜索空间很大的时候,双向广搜可以节省很多空间和时间
假设一个节点能扩展n个节点,扩展m层,单向广搜扩展出的数量就是 (1 - n^m) ⁄ ( 1 - n )
而双向广搜同样扩展m层,总结点数为 2*( 1 - n^m/2) / ( 1 - n ) ,在数据范围较大的时候,优势就体现出来了
题目链接:AcWing 190. 字串变换
代码如下:
#include<bits/stdc++.h> using namespace std; #define sf(x) scanf("%lld",&x) #define sff(x,y) scanf("%lld%lld",&x,&y) #define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0) #define pii pair<int,int> #define f first #define s second #define int long long #define umap unordered_map const int N=10; string a[N],b[N]; string A,B; int n; int expand(queue<string> &pa , umap<string ,int>&ma ,umap<string ,int>&mb,string a[],string b[]) { string ss=pa.front(); int d=ma[ss]; while(pa.size()&&ma[pa.front()]==d) { ss=pa.front(); pa.pop(); for(int i=0;i<n;i++) for(int j=0;j<ss.size();j++) { if(ss.substr(j,a[i].size())==a[i]) { string r=ss.substr(0, j) + b[i] + ss.substr(j + a[i].size()); if(mb.count(r)) return ma[ss]+mb[r]+1; if(ma.count(r)) continue; ma[r]=ma[ss]+1; pa.push(r); } } } return 11; } int bfs() { queue<string> qa,qb; umap<string ,int> ma,mb; qa.push(A); qb.push(B); ma[A]=ma[B]=0; int step=0; while(qa.size()&&qb.size()) { int t; if(qa.size()<qb.size()) t=expand(qa,ma,mb,a,b); else t=expand(qb,mb,ma,b,a); if(t<=10) return t; if(++step==10) return 11; } return 11; } signed main() { cin>>A>>B; while(cin>>a[n]>>b[n])n++; if(A==B) cout<<0<<endl; else{ int t=bfs(); if(t<=10) cout<<t<<endl; else cout<<"NO ANSWER!"<<endl; } }
6. A*(A star)
什么时候能用得到A* 算法呢? 当搜索空间很大时,就可以用A*算法来优化,它可以在只搜索很小的空间就找到正确答案。
它的原理是:用一个优先队列来存储d,d就是某个点到起点的实际距离+该点到终点的预估距离(这个预估距离可以用启发函数进行计算,启发函数可以自己写,也可以多积累一些常用的,但是前提是想要保证答案正确,每个d都必须<=起点到终点的实际距离),每次弹出最小的d,然后用d来更新他的邻点,如果可以更新就将其入队。
还要注意的是A*算法并不能想普通bfs那样在入队时进行判重,也不能像dijsktra那样在出队是进行判重,为什么呢,因为在终点出队之前,每回出队的点都不能保证是到起点的最短路,只是d最小而已,而d里面有个预估距离,预估在满足条件的情况下可以很小,假设点i在起点到终点的最短路上,j点不在,d[i]是可能>d[j],所以就j先出队,假设最坏情况下搜到了终点,此时的d就等于终点到起点经过j的那条路径,但是走i到终点<走j到终点,所以此时虽然入队了但是不会出队,而是这回来走i搜到终点再出队,中途有些点会被更新第二次,所以除了终点其余点第一次出队不一定是最短路。
题目链接:AcWing 179. 八数码
代码如下:
#include<bits/stdc++.h> using namespace std; #define sf(x) scanf("%lld",&x) #define sff(x,y) scanf("%lld%lld",&x,&y) #define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0) #define pii pair<int,int> #define f first #define s second #define int long long #define umap unordered_map int vx[]={-1,0,1,0}; int vy[]={0,1,0,-1}; char op[]={'u','r','d','l'}; //启发函数,计算每个状态的每个值到终止状态的曼哈顿距离之和 int f(string state) { int res = 0; for (int i = 0; i < state.size(); i ++ ) if (state[i] != 'x') { int t = state[i] - '1'; res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3); } return res; } string bfs(string sta) { string end ="12345678x"; umap<string,int> dist; umap<string ,pair<string ,char>> prev; //记录一下有哪个状态转移过来 priority_queue<pair<int,string>, vector<pair<int ,string>>,greater<pair<int,string>>> heap; heap.push({f(sta),sta}); dist[sta]=0; while(heap.size()) { auto t=heap.top(); heap.pop(); string state=t.s; if(state==end) break; int step=dist[state]; //找到x的下标 int x,y; for(int i=0;i<state.size();i++) if(state[i]=='x') { x=i/3,y=i%3; break; } string source =state; for(int i=0;i<4;i++) { int a=x+vx[i],b=y+vy[i]; if(a>=0&&a<3&&b>=0&&b<3) { swap(state[x*3+y],state[a*3+b]); //如果可以更新就入队 if(!dist.count(state)||dist[state]>step+1) { dist[state]=step+1; prev[state]={source,op[i]}; heap.push({dist[state] + f(state), state}); } swap({state[x*3+y]},state[a*3+b]); } } } //倒推一遍即可 string res; while(end!=sta) { res+=prev[end].s; end=prev[end].f; } return res; } signed main() { string g,c,sep; while(cin>>c) { g+=c; if(c!="x") sep+=c; } int t=0; //计算逆序对的数量,如果逆序对为奇数一定无解 for(int i=0;i<sep.size();i++) for(int j=i+1;j<sep.size();j++) if(sep[i]>sep[j]) t++; if(t%2) cout<<"unsolvable"<<endl; else cout<<bfs(g)<<endl; }