BFS的变形和模板
引:BFS的性质和效果
参考博文链接: CSDN博主「阿正的梦工坊」
性质:1,两段性;2,单调性
考虑,入队时有一个距离源点x的点
经过一次扩展,再接下来可能入队的点,只能距离源点是x+1,且不可能更大或更小
那么,下一次想要入队距离为x+2的点,必须要距离x的点出完,x+1的开始扩展
那么,队列里所以点与源点的距离差值只能是1(两段性得证)
且x+1的点必然在x的点和后面(单调性得证!!)
效果:1,取min;2,无视爆栈
1,快速取得最小
2,基于迭代的算法,是不会爆栈(无视空间限制)
1,flood fill模型
顾名思义:洪水填充法
转化:题面规定有不能填充的地方,求联通块
- 小技巧:八联通可以免去方向数组,循环一个3*3的方格,以搜索点为中心进行扩展
- 注意事项:
1,填涂颜色类型
P1162 填涂颜色
分析题目:这是要将闭合的“1”里面的“0”改写成“2”,然后输出
思考:直接找到闭合1不好找,那么就把闭合1外的所有0进行flood fill,输出时,除去外部标记0和1,就是圈内的0啦
思维点:逆向flood fill,从所有的边界开始搜索,特殊构造会隔断flood,所以要四面八方开火!
剩下的就没啥了,无非就是一些手打队列的细节
例题:AcWing 1097. 池塘计数
#include<bits/stdc++.h>
using namespace std;
#define x first
#define y second
typedef pair<int ,int > pii; yxc码风实锤咯
const int N = 1009,M=N*N;
char a[N][N];
bool st[N][N];
int ans,n,m;
pii q[M];
void bfs(int sx,int sy)
{
int hh=0,tt=0;
q[hh].x=sx; q[hh].y=sy;
while(hh<=tt)
{
for (int i = q[hh].x-1; i <= q[hh].x+1; i ++ )
{
for (int j = q[hh].y-1; j <= q[hh].y+1; j ++ )
{
if(i==q[hh].x&&q[hh].y==j)continue;
if(i<1||i>n||j<0||j>m)continue;
else if(a[i][j]=='W'&&!st[i][j])st[i][j]=1,q[++tt]={i,j};
}
}
hh++;
}
}
int main()
{
cin>>n>>m;
for (int i = 1; i <= n; i ++ )scanf("%s",a[i]+1);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
if(a[i][j]=='W'&&!st[i][j])bfs(i,j),ans++;
cout << ans;
}
2,最短路模型
特征:少数个体或者特征随着每一步操作而变化,我们只需要把这些操作提取成状态
理由:BFS是逐层遍历搜索树的算法,所有的状态按照入队的先后,具有层次单调性
处理题面: 权重相等或者01类型,别的其实也行,但是多多少少有点难度
3,多源BFS
考虑原始bfs都是从一个源点开始扩展,往往每一次扩展对应了一次计数或者其他什么特征
可是对于距离类型,每次入队一个效率不高,因为不需要入队计数,所以直接全部入!!(多源点起搜)
简单栗子:
- 题面:01矩阵中,求得所以的点到最近的1的最短距离
- 思路:易知所有的1到1都是距离为0,我们可以从 d i s t [ 0 ] dist[0] dist[0] 即所有的1开始,扩展0,由于单调和两段性,第一次扩展到的时候就是最短距离
- 做法:直接把所有的 1 1 1 入队,搜!
4,最小步数模型
5,双端队列广搜
6,双向广搜
双向广搜主要用在最小步数模型(也称状态图模型)里面,这里整个状态空间一般是指数级别的,用双向广搜可以极大地提高运行效率。
双向广搜,顾名思义,就是从起点和终点都进行bfs搜索,然后两者相遇的话,就代表找到了合适的从起点到终点的路径。下图展示了起点A和终点B之间能搜到的最小步数的计算过程。
-
双向广搜需要2个哈希表,unordered_map<string, int> da, db; ,分别记录从起点状态(状态:这里是字符串)到中间某状态的最小步数,从终点状态到中间某状态的最小步数。最终的答案就可以通过相加的方式来求。
-
双向广搜同样需要两个队列,q[a]和q[b],来分别进行bfs。当两个队列里面同时有值的时候:while(qa.size() && qb.size()),再进行扩展搜索;如果有1个为空,说明从起点到终点是不可达的,也就是无解的。
-
这里用一个标志位t来记录 双向广搜是否相遇。
#include<bits/stdc++.h>
using namespace std;
const int N = 6;
int n;
string a[N],b[N];
// 扩展函数
// 参数:扩展的队列,到起点的距离,到终点的距离,规则,规则
//返回值:满足条件的最小步数
int extend(queue<string>& q, unordered_map<string,int>& da, unordered_map<string, int>& db,
string a[], string b[]){
// 取出队头元素
string t = q.front();
q.pop();
for(int i = 0; i < t.size(); i ++) // t从哪里开始扩展
for(int j = 0; j < n; j ++) // 枚举规则
//如果t这个字符串的一段= 规则,比如= xyz,才可以替换
if(t.substr(i, a[j].size()) == a[j]){
// 变换之后的结果state:前面不变的部分+ 变化的部分 + 后面不变的部分
// 比如abcd ,根据规则abc--> xu,变成 xud,这里的state就是xud
string state = t.substr(0,i) +b[j] + t.substr(i + a[j].size());
// state状态是否落到b里面去,两个方向会师,返回最小步数
if(db.count(state)) return da[t] + 1 + db[state];
// 如果该状态之前已扩展过,
if(da.count(state)) continue;
da[state] = da[t] + 1;
q.push(state);
}
return 11;
}
// 从起点和终点来做bfs
int bfs(string A, string B){
queue<string> qa, qb; // 两个方向的队列
//每个状态到起点的距离da(哈希表),每个状态到终点的距离db哈希表
unordered_map<string, int> da, db;
// qa从起点开始搜,qb从终点开始搜
qa.push(A), da[A] = 0; // 起点A到起点的距离为0
qb.push(B), db[B] = 0; // 终点B到终点B的距离为0
// qa和qb都有值,说明可以扩展过来,否则说明是不相交的
while(qa.size() && qb.size()){
int t; // 记录最小步数
// 哪个方向的队列的长度更小一些,空间更小一些,从该方向开始扩展,
// 时间复杂度比较平滑,否则有1个点会超时
if(qa.size() <= qb.size())
t = extend(qa, da, db, a, b);
else t = extend(qb, db, da, b, a);
// 如果最小步数在10步以内
if( t <= 10) return t;
}
return 11; // 如果不连通或者最小步数>10,则返回大于10的数
}
int main(){
string A, B;
cin >> A >> B;
// 读入扩展规则,分别存在a数组和b数组
while( cin >> a[n] >> b[n]) n ++;
int step = bfs(A,B);
if(step > 10) puts("NO ANSWER!");
else cout << step << endl;
}
7,A stra
要素:
- f ( x ) f(x) f(x) 估价函数:作为当前点位到终点的价值的估计值,保证比实际最优价还要小
- p r i o r i t y q u e u e : p q priority~queue :pq priority queue:pq 作为优先队列BFS的变体优化,他的存在是必要的
- s t r u c t : n o d e struct :node struct:node 作为pq内部的优先操作对象,需要对他的运算符进行重载
A s t a r A star Astar 的建模中对距离的叙述,从 s t a t e state state 到 e d ed ed 的估价是符合实际中的距离增长趋势和规律的,规划和实际情况差距越小,对搜索的优化效果更好
经验得知:
1,k短路中一般使用最短路作为估价函数
2,八数码中使用当前所有点位到目标状态对应点位的曼哈顿距离作为估价
简单来说A*本来就不是一个用于解最优解的算法而只是一个渐进最优的算法,在oi上一般来说是用于骗分而非得到正解,除非是special judge
例
题意:求1号点到n号点的次短路长度,要求不经过重复的点位
每个状态第一次出队,就是初始状态到它的最小代价。
事实上,每个状态第k次出队,就是初始状态到它的第k小代价。(由数学归纳法)
并且每个状态的第k小代价,必是由某一个出队k−1次的状态扩展得到的
- 具体实现中只需要一个计数器记录出队次数即可
#include<bits/stdc++.h>
#define rep(i,j,k) for(int i=j;i<=k;i++)
using namespace std;
const int N =205;
const int M= 4e4+9;
typedef pair<double,double>pdd;
int m,d,n;
pdd p[N];
int nxt[M],h[N],e[M],idx;
double w[M];
double f[N];
bool vis[N];
struct spp
{
int num;
double v;
};
struct node
{
int id;
double dis;
bool st[205];
node ()
{
memset(st,0,sizeof st);
}
};
priority_queue<spp>spq;
priority_queue<node>pq;
//dist就是f(x),g就是估价,还要开一个vis数组判重,否则只有70分
bool operator < (const spp &x,const spp &y)
{
return x.v > y.v;
}
bool operator < (const node &x,const node &y)
{
return f[x.id]+x.dis > f[y.id]+y.dis;
}
void add(int x,int y,double z)
{
nxt[++idx]=h[x];
h[x]=idx;
e[idx]= y;
w[idx]=z;
}
double qd(pdd x,pdd y)
{
double res= (double)(x.first- y.first)* (x.first- y.first);
res+= (double)(x.second- y.second)* (x.second- y.second);
return (double)sqrt(res);
}
void dij_path()
{
memset(f,127,sizeof f);
memset(vis,0,sizeof vis);
f[n]=0;
spq.push((spp){n,0});
while (spq.size())
{
spp u= spq.top();
spq.pop();
if(vis[u.num])continue;
vis[u.num]=1;
for(int i=h[u.num];~i;i=nxt[i])
{
int j=e[i];
if(f[j]>f[u.num]+w[i])
{
f[j]=f[u.num]+w[i];
spq.push((spp){j,f[j]});
}
}
}
}
void A_star()
{
node x;
x.dis=0;
x.id=1;
pq.push(x);
int tot = 0;
while (pq.size())
{
node u = pq.top();
pq.pop();
if(u.id==n)tot++;
if(tot==2)
{
printf("%.2lf",u.dis);
return ;
}
int id= u.id;
for(int i=h[id];~i;i=nxt[i])
{
int j=e[i];
if(u.st[j])continue;
node y = u;
y.st[j]=1;
y.dis=y.dis+w[i];
y.id=j;
pq.push(y);
}
}
puts("-1");
}
int main()
{
memset(h,-1,sizeof h);
scanf("%d%d",&n,&m);
for(int i= 1;i<= n;i++)scanf("%lf%lf",&p[i].first,&p[i].second);
rep(i,1,m)
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b,qd(p[a],p[b]));
add(b,a,qd(p[a],p[b]));
}
dij_path();
A_star();
}