文章目录
基于BFS的搜索
bfs需要借助队列实现,按照层次的搜索顺序,现比dfs需要占用更多的内存且办法复杂度更高,但不会存在爆栈风险并且效率更高。
连通性模型-Flood Fill
flood fill算法可以在线性的时间复杂度内找到一个点的所有连通点,并且可以在搜索的过程中记录联通点数、边缘点的相关数据等。
算法的主要思路为:
//所有点仅入队和出队一次,可以在出队的时候统计节点数
将起始点加入队列并标记
while 队列不空
u=取出队列头节点
for 枚举所有与u相邻的点
相邻点加入队列,并标记
统计其它信息
例题 acwing:1097. 池塘计数
农夫约翰有一片 N∗M 的矩形土地。最近,由于降雨的原因,部分土地被水淹没了。
现在用一个字符矩阵来表示他的土地。
每个单元格内,如果包含雨水,则用”W”表示,如果不含雨水,则用”.”表示。
现在,约翰想知道他的土地中形成了多少片池塘。每组相连的积水单元格集合可以看作是一片池塘。
每个单元格视为与其上、下、左、右、左上、右上、左下、右下八个邻近单元格相连。
请你输出共有多少片池塘,即矩阵中共有多少片相连的”W”块。
10 12
W........WW.
.WWW.....WWW
....WW...WW.
.........WW.
.........W..
..W......W..
.W.W.....WW.
W.W.W.....W.
.W.W......W.
..W.......W.
分析:可以遍历所有点当遇见w是用一边flood fill,并cnt++
时间复杂度:
O
(
n
)
O(n)
O(n) (n为节点数)
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef pair<int,int> PII;
const int N=1009;
char g[N][N];
bool st[N][N];
int n,m;
PII que[N*N];//数组模拟队列
void bfs(int x,int y)
{
st[x][y]=1;
int hh=0;
int tt=-1;
que[++tt]={x,y};
while(hh<=tt)
{
PII u=stk[hh++];
for(int i=-1;i<=1;i++)
for(int j=-1;j<=1;j++)
{
if(i==0&&j==0) continue;
int tx=u.x+i;
int ty=u.y+j;
if(tx<0||tx>=n||ty<0||ty>=m||st[tx][ty]||g[tx][ty]!='W') continue;
st[tx][ty]=1;
que[++tt]={tx,ty};
}
}
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
cin>>g[i];
int cnt=0;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
{
if(g[i][j]=='W'&&!st[i][j])
{
bfs(i,j);
cnt++;
}
}
cout<<cnt;
return 0;
}
相关习题
1、边缘分析 acwing:1106. 山峰和山谷
最短路模型
bfs仅能解决当权值或者多权值最短路问题。在思考问题时可以尝试反转起点和终点。路径输出仅需开一个数组记录每个点的前躯节点,最后反向遍历即可。一般要求输出字典序最小时只需在遍历一个点的所有连通点时按照字典序遍历即可。
单权值最短路
当权值最短路,即所有边的权值相等。可以用于计算某个点到其余所有连通点的最短路径,第一入队时即为最短路径且一个点仅入队出队一次。
算法思路:
//与flood fill现比,需要多开一个记录距离的数组
将起始点加入队列并标记,和初始化距离
while 队列不空
u=取出队列头节点
for 枚举所有没有标记且与u相邻的点
相邻点加入队列,并标记
记录节点距离
例题 acwing:1076. 迷宫问题
给定一个 n×n 的二维数组,
它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。数据保证至少存在一条从左上角走到右下角的路径。
分析:可以利用bfs求最短路,并记录每个点的前躯节点。最后输出路径即可
时间复杂度:
O
(
n
)
O(n)
O(n)
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef pair<int,int> PII;
const int N=1009;
int g[N][N];
PII pre[N][N];
PII que[N*N];
int n;
int main()
{
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++) cin>>g[i][j];
int nx[]={0,-1,0,1};
int ny[]={-1,0,1,0};
memset(pre,-1,sizeof pre);
int hh=0,tt=-1;
pre[n-1][n-1]={n-1,n-1};
que[++tt]={n-1,n-1};
while(hh<=tt)
{
auto u=que[hh++];
for(int i=0;i<4;i++)
{
int tx=u.x+nx[i];
int ty=u.y+ny[i];
if(tx<0||tx>=n||ty<0||ty>=n||g[tx][ty]||pre[tx][ty].x!=-1) continue;
pre[tx][ty]={u.x,u.y};
que[++tt]={tx,ty};
if(tx==0&&ty==0)
{
PII t={0,0};
while(1)
{
cout<<t.x<<' '<<t.y<<endl;
if(t.x==n-1&&t.y==n-1) break;
t=pre[t.x][t.y];
}
return 0;
}
}
}
return 0;
}
相关习题
1、一维最短路 acwing:1100. 抓住那头牛
双权值最短路-双端队列广搜
双权值最短路更像迪杰斯特拉算法,每个点可多次入队,第一次出队时为最短路。但不需要借助优先队列,只需每次入队将大权值加入队尾小权值加入队首即可。
例题 acwing:2019. 拖拉机
干了一整天的活,农夫约翰完全忘记了他把拖拉机落在田地中央了。
他的奶牛非常调皮,决定对约翰来场恶作剧。
她们在田地的不同地方放了 N 捆干草,这样一来,约翰想要开走拖拉机就必须先移除一些干草捆。
拖拉机的位置以及 N 捆干草的位置都是二维平面上的整数坐标点。
拖拉机的初始位置上没有干草捆。
当约翰驾驶拖拉机时,他只能沿平行于坐标轴的方向(北,南,东和西)移动拖拉机,并且拖拉机必须每次移动整数距离。
例如,驾驶拖拉机先向北移动 2 单位长度,然后向东移动 3 单位长度。
拖拉机无法移动到干草捆占据的位置。
请帮助约翰确定他需要移除的干草捆的最小数量,以便他能够将拖拉机开到二维平面的原点。
分析:直接采用双端队列广搜即可,当原点出队时直接退出循环即可
时间复杂度:
O
(
n
)
O(n)
O(n)
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef pair<int,int> PII;
const int N=1009;
bool g[N][N];
bool st[N][N];
int dist[N][N];
int nx[]={-1,0,1,0};
int ny[]={0,-1,0,1};
int bfs(int x,int y)
{
memset(dist,0x3f,sizeof dist);
memset(st,0,sizeof st);
deque<PII> que;
dist[x][y]=0;
que.push_front({x,y});
while(que.size())
{
auto u=que.front();
que.pop_front();
if(u.x==0&&u.y==0) return 0;
if(st[u.x][u.y]) continue;
st[u.x][u.y]=1;
for(int i=0;i<4;i++)
{
int tx=u.x+nx[i];
int ty=u.y+ny[i];
if(tx<0||tx>=N||ty<0||ty>=N||st[tx][ty]) continue;
int d=dist[u.x][u.y]+g[tx][ty];
if(d<dist[tx][ty])
{
dist[tx][ty]=d;
if(g[tx][ty])
{
que.push_back({tx,ty});
}else
{
que.push_front({tx,ty});
}
}
}
}
return -1;
}
int main()
{
int n,x,y;
cin>>n>>x>>y;
for(int i=0;i<n;i++)
{
int a,b;
cin>>a>>b;
g[a][b]=1;
}
bfs(x,y);
cout<<dist[0][0];
return 0;
}
相关习题
1、acwing:175. 电路维修
多起点最短路-多源BFS
多起点最短路问题,现比单起点问题的只需要在初始化时将所有起点入队即可。
例题 acwing:173. 矩阵距离
给定一个 N 行 M 列的 01 矩阵 A,A[i][j] 与 A[k][l] 之间的曼哈顿距离定义为:
dist(A[i][j],A[k][l])=|i−k|+|j−l|
输出一个 N 行 M 列的整数矩阵 B,其中:
B[i][j]=min1≤x≤N,1≤y≤M,A[x][y]=1dist(A[i][j],A[x][y])
分析:需要计算所有0点距离最近的1点距离,直接利用多源bfs求解即可
时间复杂度:
O
(
n
)
O(n)
O(n)
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef pair<int,int> PII;
const int N=1009,M=N*N;
char g[N][N];
int dist[N][N];
PII que[M];
int n,m;
void bfs()
{
int nx[]={1,0,-1,0};
int ny[]={0,1,0,-1};
memset(dist,-1,sizeof dist);
int hh=0,tt=-1;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
{
if(g[i][j]=='1')
{
que[++tt]={i,j};
dist[i][j]=0;
}
}
while(hh<=tt)
{
auto u=que[hh++];
for(int i=0;i<4;i++)
{
int tx=u.x+nx[i];
int ty=u.y+ny[i];
if(tx<0||tx>=n||ty<0||ty>=m||dist[tx][ty]!=-1) continue;
dist[tx][ty]=dist[u.x][u.y]+1;
que[++tt]={tx,ty};
}
}
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++) cin>>g[i];
bfs();
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
cout<<dist[i][j]<<' ';
}
cout<<endl;
}
return 0;
}
最小步数模型
最小步数模型现比上文中的最短路模型的区别在于,最小步数模型考虑的是一个整体的状态经过多少次的合法操作变成终点状态,而最短路模型是在一个棋盘或迷宫中一个点到点的最短路。
最小步数模型我们只需要将整体状态看作一个点操作即可,在记录信息时可以利用unordered_map存储。
例题 acwing:1107. 魔板
Rubik 先生在发明了风靡全球的魔方之后,又发明了它的二维版本——魔板。
这是一张有 8 个大小相同的格子的魔板:
1 2 3 4
8 7 6 5
我们知道魔板的每一个方格都有一种颜色。
这 8 种颜色用前 8 个正整数来表示。
可以用颜色的序列来表示一种魔板状态,规定从魔板的左上角开始,沿顺时针方向依次取出整数,构成一个颜色序列。
对于上图的魔板状态,我们用序列 (1,2,3,4,5,6,7,8) 来表示,这是基本状态。
这里提供三种基本操作,分别用大写字母 A,B,C 来表示(可以通过这些操作改变魔板的状态):
A:交换上下两行;
B:将最右边的一列插入到最左边;
C:魔板中央对的4个数作顺时针旋转。
对于每种可能的状态,这三种基本操作都可以使用。
你要编程计算用最少的基本操作完成基本状态到特殊状态的转换,输出基本操作序列。
分析:该题为最小步数模型,可以利用bfs求解,输出步数可以利用记录前驱节点的方法最后在reverse反向即可。
时间复杂度:
0
(
n
)
0(n)
0(n)
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef pair<char,string> PII;
const int N=40329;
string que[N];
unordered_map<string,int> dist;
unordered_map<string,PII> pre;
char g[2][4];
void setg(string sta)
{
for(int i=0;i<4;i++)
{
g[0][i]=sta[i];
g[1][3-i]=sta[4+i];
}
}
string getg()
{
string ret;
for(int i=0;i<4;i++) ret+=g[0][i];
for(int j=3;j>=0;j--) ret+=g[1][j];
return ret;
}
string A(string &sta)
{
setg(sta);
for(int i=0;i<4;i++) swap(g[0][i],g[1][i]);
return getg();
}
string B(string &sta)
{
setg(sta);
char a=g[0][3],b=g[1][3];
for(int i=3;i>=1;i--)
{
g[0][i]=g[0][i-1];
g[1][i]=g[1][i-1];
}
g[0][0]=a;
g[1][0]=b;
return getg();
}
string C(string &sta)
{
setg(sta);
char c=g[0][2];
g[0][2]=g[0][1];
g[0][1]=g[1][1];
g[1][1]=g[1][2];
g[1][2]=c;
return getg();
}
void bfs(string sta,string end)
{
int hh=0,tt=-1;
que[++tt]=sta;
dist[sta]=0;
while(hh<=tt)
{
string u=que[hh++];
string s[3];
s[0]=A(u);
s[1]=B(u);
s[2]=C(u);
for(int i=0;i<3;i++)
{
if(dist.count(s[i])) continue;
que[++tt]=s[i];
dist[s[i]]=dist[u]+1;
pre[s[i]]={(i+'A'),u};
if(dist.count(end)) return;
}
}
}
int main()
{
string sta;
string end;
int x;
for(int i=0;i<8;i++)
{
cin>>x;
end+=char(x+'0');
}
for(int i=1;i<=8;i++) sta+=char(i+'0');
bfs(sta,end);
cout<<dist[end]<<endl;
if(dist[end]==0) return 0;
string u=end;
string ans;
while(1)
{
ans+=pre[u].x;
if(pre[u].y!=sta) u=pre[u].y;
else break;
}
reverse(ans.begin(),ans.end());
cout<<ans<<endl;
return 0;
}
BFS优化策略
首先以下优化方式仅适用于最短路模型和最小步数模型,单搜索空间非常大时,我们可以利用以下方法对搜索进行优化,使其尽可能快的搜索到答案。(每次选择搜索起点终点队列后需要搜索一层)
双向广搜
双向广搜,故名思意就是同时从起点和终点进行搜索,直到有相交点出现。这种方法可以将时间复杂度从 O ( a n ) O(a^n) O(an) 降低到 O ( a n / 2 ) O(a^{n/2}) O(an/2),并且在选择扩展两个方向时应该选择队列长度更短的一方,减少搜索次数。
例题 acwing:190. 字串变换
已知有两个字串 A, B 及一组字串变换的规则(至多 6 个规则):
A1→B1
A2→B2
…
规则的含义为:在 A 中的子串 A1 可以变换为 B1、A2 可以变换为 B2…。
例如:A=abcd B=xyz
变换规则为:
abc → xu ud → y y → yz
则此时,A 可以经过一系列的变换变为 B,其变换的过程为:
abcd → xud → xy → xyz
共进行了三次变换,使得 A 变换为 B。
分析:经计算直接利用bfs搜索时间复杂度过高,故这里可以采用双向bfs搜索。
时间复杂度:指数级别
#include<bits/stdc++.h>
using namespace std;
const int N=10;
string a,b;
string aits[N],bits[N];
int n;
int extend(queue<string>&questa,queue<string>&queend,unordered_map<string,int>&mapsta,unordered_map<string,int>&mapend,string aits[N],string bits[N])
{
int cen=questa.size();
while (cen--)
{
string u=questa.front();
questa.pop();
for(int k=0;k<n;k++)
{
for(int i=0;i<u.size();i++)
{
if(u.substr(i,aits[k].size())==aits[k])
{
string t=u.substr(0,i)+bits[k]+u.substr(i+aits[k].size());
if(mapsta.count(t)) continue;
mapsta[t]=mapsta[u]+1;
questa.push(t);
if(mapend.count(t)) return mapsta[t]+mapend[t];
}
}
}
}
return -1;
}
int bfs()
{
if(a==b) return 0;
queue<string> questa,queend;
unordered_map<string,int> mapsta,mapend;
mapsta[a]=0;
questa.push(a);
mapend[b]=0;
queend.push(b);
int step=0;
while(questa.size()&&queend.size())
{
if(questa.size()<queend.size())
{
int ret=extend(questa,queend,mapsta,mapend,aits,bits);
if(ret!=-1) return ret;
}else
{
int ret=extend(queend,questa,mapend,mapsta,bits,aits);
if(ret!=-1) return ret;
}
if(++step==10) return 11;
}
return 11;
}
int main()
{
cin>>a>>b;
while(cin>>aits[n]>>bits[n]) n++;
int ans=bfs();
if(ans>10) cout<<"NO ANSWER!"<<endl;
else cout<<ans<<endl;
return 0;
}
A*
A*算法为启发式搜索,只能计算某个状态到某个状态的最小步数。启发函数 f()计算的时某个状态到终点的估计步数,应该严格保证小于等于真实步数。当终点第一次出队时为最短距离,第n次出队为第n短距离。队列需要用优先队列代替,算法更像迪杰斯特拉算法。每个节点只要扩展出来的新节点可以更新以前的距离就可以入队,类比迪杰斯特拉把更新距离的点加入堆。
例题 acwing:179. 八数码
在一个 3×3 的网格中,1∼8 这 8 个数字和一个 X 恰好不重不漏地分布在这 3×3 的网格中。
例如:
1 2 3
X 4 6
7 5 8
在游戏过程中,可以把 X 与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 X
例如,示例中图形就可以通过让 X 先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
1 2 3 1 2 3 1 2 3 1 2 3
X 4 6 4 X 6 4 5 6 4 5 6
7 5 8 7 5 8 7 X 8 7 8 X
把 X 与上下左右方向数字交换的行动记录为 u、d、l、r。
现在,给你一个初始网格,请你通过最少的移动次数,得到正确排列。
分析:首先该题为最小步数某型可以采用bfs,但搜索空间过大故可以采用A*算法。八数码问题存在答案的充要条件时逆序对为偶数。
时间复杂度:指数级别
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
#include <unordered_map>
#define x first
#define y second
using namespace std;
unordered_map<string,int> mmap;
unordered_map<string,pair<string,char>> pre;
priority_queue<pair<int, string>, vector<pair<int, string>>, greater<pair<int, string>>> heap;
int f(string sta)
{
string end="12345678x";
int ret=0;
for(int i=0;i<sta.size();i++)
if(sta[i]!=end[i]) ret++;
return ret;
}
//该启发函数效果更好
// 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;
// }
bool bfs(string sta)
{
string end="12345678x";
heap.push({f(sta),sta});
mmap[sta]=0;
pre[sta]={"XX",'X'};
int nx[]={0,-1,0,1};
int ny[]={-1,0,1,0};
char g[]={'l','u','r','d'};
while(heap.size())
{
string node=heap.top().y;
heap.pop();
if(node==end) return true;
int k=node.find("x");
int x=k/3;
int y=k%3;
for(int i=0;i<4;i++)
{
int tx=x+nx[i];
int ty=y+ny[i];
if(tx<0||ty<0||tx>=3||ty>=3) continue;
int step=mmap[node];
string t=node;
swap(t[tx*3+ty],t[k]);
if(!mmap[t]||step+1<mmap[t])
{
mmap[t]=step+1;
heap.push({mmap[t]+f(t),t});
pre[t]={node,g[i]};
}
}
}
return false;
}
int main()
{
string s,c,ts;
string end="12345678x";
for(int i=0;i<9;i++)
{
cin>>c;
s+=c;
if(c!="x") ts+=c;
}
int t = 0;
for (int i = 0; i < ts.size(); i ++ )
for (int j = i + 1; j < ts.size(); j ++ )
if (ts[i] > ts[j])
t ++ ;
if (t % 2)
{
cout<<("unsolvable");
return 0;
}
if(bfs(s))
{
string ret;
while(end!=s)
{
ret+=pre[end].y;
end=pre[end].x;
}
reverse(ret.begin(),ret.end());
cout<<ret;
}else
{
cout<<"unsolvable"<<endl;
}
return 0;
}
相关习题
1、acwing:AcWing 178. 第K短路
基于DFS的搜索
dfs可以判断连通性,但不可以求最短路径(现比bfs迷宫问题,但也可以借助全局变量实现,不推荐)。dfs除了可以运用在连通性的判断上,还可以用来穷举方案。即在所有的方案空间中寻找相应的答案,关键在于剪枝的操作。时间复杂度指数级别,不同的剪枝操作可能运行时间差异巨大。
连通性模型
可行性方案搜索
最小步数方案模型
全局变量法
迭代加深法
DFS优化
剪枝优化
双向DFS
IDA*
学算法就上acwing 学习愉快