dfs 和 bfs
dfs
dfs,是英文名 deep-first-search 的缩写,它的思想是一搜到底,撞墙回头。这一种思想是基于递归和栈实现的,打个比方,你在迷宫里面,发现有岔路口,先向左走,走了一段时间发现是死路,退回去之后走右边。
基于 dfs,有一种术语叫做:回溯。回溯的思想是如果要走这一条路,那么就踩上去脚印,如果撞墙,回退的过程就把脚印擦掉。这一种思想避免了很多情况:比如重复走,不走可以走的路,记录答案错误等。
下面,给出 dfs 的伪代码:
void dfs(node dep){//node 为深度的类型
if(check(dep)){//答案撞墙了
change(ans);//更新答案
return;
}
for(auto i:plan[dep]){//枚举每一种扩展方法
if(check(i)){//下一个节点撞墙了
continue;
}
moveplan(GO_TO,i);//去下一个节点
dfs(i);//下一个节点
moveplan(COME_BACK,i);//从下一个节点回溯
}
}
dfs 的种类
- 全排列类型。即遍历全排列,为 O ( n × n ! ) \operatorname{O}(n\times n!) O(n×n!)。由于时间过大,所以只能够应对 1 ≤ n ≤ 10 1\le n\le 10 1≤n≤10。
int n,ans[MAXN];
bool vis[MAXN];
void dfs(int dep){
if(dep==n+1){
for(int i=1;i<=n;++i){
printf("%d ",ans[i]);//输出结果
}
putchar('\n');
return;
}
for(int i=1;i<=n;++i){
if(!vis[i]){//如果在此之前没有出现过
ans[dep]=i;
vis[i]=true;//标记 vis
dfs(dep+1);//继续搜索
vis[i]=false;//回溯
}
}
}
- 组合计数,从 n n n 个数里面选择 m m m 个数,发现枚举到 i i i 可以不用再回头枚举,所以设定 l a s t last last 表示上一个搜索的数,下一次直接从 l a s t + 1 last+1 last+1 开始枚举就可以了。这样子使得每一个集合只能够被遍历一次,时间复杂度 O ( m × C n m ) \operatorname{O}(m\times C_n^m) O(m×Cnm)。
int n,m,ans[MAXN];
void dfs(int dep,int last){
if(dep==m+1){
for(int i=1;i<=m;++i){
printf("%d ",ans[i]);//输出结果
}
putchar('\n');
return;
}
for(int i=last+1;i<=n;++i){
ans[dep]=i;//标记答案
dfs(dep+1,i);//继续搜索
}
}
- 枚举子集,考虑每一个点只能够有出现和没出现两种状态,因此时间复杂度为 O ( 2 n ) \operatorname{O}(2^n) O(2n)。
int n,top,ans[MAXN];
void dfs(int dep){
if(dep==n+1){
for(int i=1;i<=top;++i){
printf("%d ",ans[i]);//输出结果
}
putchar('\n');
return;
}
ans[++top]=dep;
dfs(dep+1);//选
--top;//回溯
dfs(dep+1);//不选
}
- 图上/树上 dfs,按照边来往外扩展即可。由于形式多样,也是给出模板代码。
struct node{
int to,next;
}edge[MAXM<<1];
int n,m,cnt,head[MAXN];
inline void addedge(int from,int to){
edge[++cnt].to=to;
edge[cnt].next=head[form];
head[from]=cnt;
}
void dfs(int from,int fa){
makeanswer(from);//处理答案,可以用于树形 dp
for(int i=head[from];i;i=edge[i].next){//遍历图或者树
int to=edge[i].to;
if(to!=fa){//判断回溯
dfs(to,from);//同样格式
}
}
}
- 网格 dfs,最经典的套题,通常是每一步可以向周围走一格,太模板了。
int n,m;
int dx[4]={0,-1,0,1};
int dy[4]={1,0,-1,0};
bool vis[MAXN][MAXN];
void dfs(int x,int y){
if(checkfinish(x,y)){//判断结束
moveanswer();//更新答案或者结束
return;
}
for(int i=0;i<4;++i){
int nx=x+dx[i];
int ny=y+dy[i];
if(1<=nx&&nx<=n&&1<=ny&&ny<=m&&vis[nx][ny]&&checkpoint(nx,ny){//下一个节点合法
move(COME_TO);
vis[nx][ny]=true;
dfs(nx,ny);//下一个节点
move(COME_BACK);
vis[nx][ny]=false;//回溯
}
}
}
例题1
板子题,图上 dfs,首先建边,要建无向图。然后枚举每一个点作为起点往后搜的最大答案,取
max
\max
max,为了保证第一个点的父节点不冲突,通常赋值
0
0
0。还需要标记 vis
数组,不然环会直接死递归爆栈。
#include<algorithm>
#include<cstdio>
#define MAXN 22
#define MAXM 55
using namespace std;
struct node{
int from,to,next,dis;
}edge[MAXM<<1];
int n,m,cnt,ans,head[MAXN];
bool vis[MAXN];
inline void addedge(int from,int to,int dis){
edge[++cnt].from=from;
edge[cnt].to=to;
edge[cnt].dis=dis;
edge[cnt].next=head[from];
head[from]=cnt;
}
void dfs(int from,int fa,int step){
vis[from]=true;
ans=max(ans,step);
for(int i=head[from];i;i=edge[i].next){
int to=edge[i].to;
if(to!=fa&&!vis[to]){
dfs(to,from,step+edge[i].dis);
}
}
vis[from]=false;
}
int main(){
scanf("%d %d",&n,&m);
while(m--){
int from,to,dis;
scanf("%d %d %d",&from,&to,&dis);
addedge(from,to,dis);
addedge(to,from,dis);
}
for(int i=1;i<=n;++i){
dfs(i,0,0);
}
printf("%d",ans);
return 0;
}
例题2
属于枚举从 n n n 个数里面选择 m m m 个数,再加上结尾判断质数的 1 0 8 = 1 0 4 \sqrt{10^8}=10^4 108=104,则最大是 C n k × 1 0 4 C_n^k\times 10^4 Cnk×104,不会超时。
#include<cstdio>
#define MAXN 22
using namespace std;
typedef long long ll;
int n,k,ans,a[MAXN];
inline bool prime(int n){
if(n<=1){
return false;
}
for(int i=2;i*i<=n;++i){
if(n%i==0){
return false;
}
}
return true;
}
void dfs(int dep,int sum,int last){
if(dep==k+1){
if(prime(sum)){
++ans;
}
}
for(int i=last+1;i<=n;++i){
dfs(dep+1,sum+a[i],i);
}
}
int main(){
scanf("%d %d",&n,&k);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
}
dfs(1,0,0);
printf("%d",ans);
return 0;
}
bfs
bfs 是英文名 breath-first-search 的缩写,它的思想是逐步扩展,搜到即停。也就是 bfs 是把每一个深度的所有节点扩展出来,一旦出现了答案,并且答案要最优且深度最浅,那么这个就是答案,可以停止搜索了。
这么来讲,你需要找到一棵树上最靠前的一个权值为 2 2 2 的节点,那么可以用 bfs 来实现。bfs 将每一个节点的下表压入队列,然后逐步弹出队头。如果看到了 2 2 2,那就是最靠前的。
下面,给出 bfs 的伪代码:
inline void bfs(node st,node en){//node 为扩展答案的类型
queue<node> q;
q.push(st);
moveplan(GO_TO,st);//去下一个节点
while(!q.empty()){
node front=q.front();
q.pop();
if(front==en){
ans=front;//找到答案
return;
}
moveplan(COME_BACK,front);//从下一个节点回溯
for(auto i:plan[front]){//枚举每一种扩展方法
if(check(i)){//下一个节点撞墙了
continue;
}
q.push(i);
moveplan(GO_TO,i);//去下一个节点
}
}
ans=unfound();//无解
}
此外,bfs 还有众多的优化方式。形如双端队列、优先队列、A-star 等算法。
例题
这一道题目可以看作最优性答案,所以用 bfs 实现。考虑加一个偏移数组 d d d,来表示向上走还是向下走。然后枚举向上走还是向下走,压入队列。
#include<bits/stdc++.h>
#define MAXN 202
using namespace std;
struct node{
int pos,step;
};
int n,ans,a[MAXN],vis[MAXN];
int d[2]={-1,1};
queue<node>q;
inline void bfs(int st,int en){
queue<node> q;
q.push((node){st,0});
vis[st]=true;
while(!q.empty()){
node front=q.front();
q.pop();
if(front.pos==en) {
ans=front.step;
return;
}
for(int i=0;i<2;++i){
int nxt=front.pos+d[i]*a[front.pos];
if(nxt>=1&&nxt<=n&&!vis[nxt]){
q.push((node){nxt,front.step+1});
vis[nxt]=true;
}
}
}
ans=-1;
}
int main(){
int st,en;
scanf("%d %d %d",&n,&st,&en);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
}
bfs(st,en);
printf("%d",ans);
return 0;
}
此外,所有 bfs 优化放在剪枝后面介绍。
总结
来分析一下,dfs 适用于统计答案个数的题目,优点是空间最大是
O
(
n
)
\operatorname{O}(n)
O(n),但是时间可能是指数级别的。bfs 适用于求最优性答案的题目,优点是较快,但是由于 queue
耗用空间很大,空间最坏能够达到指数级大小。
剪枝
剪枝的思想是把搜索树的一部分一定不满足答案的部分剪掉,如果剪枝精妙,再加上一些小优化或者玄学优化,可以把指数级别的搜索优化到多项式级别,甚至获得高分乃至 100 100 100 分。
剪枝分为以下几个:
正确性剪枝
这个通常来讲是优化正确性的,比如 dfs 例题,里面使用了 v i s vis vis 数组进行剪枝,减去了 { 1 , 1 , 1 } \{1,1,1\} {1,1,1} 的不合法情况。
最优性剪枝
这个是 bfs 能够很快的思想之一,就是如果当前答案没有已经求出的答案那么优,就不扩展。bfs 就是证明了搜到深度最小的答案,不可能有答案比它再优了才退出的。
记忆化搜索
有时候,dfs 会多次访问同一个节点。比如 d f s ( 3 , 3 ) dfs(3,3) dfs(3,3) 可以扩展成 d f s ( 2 , 2 ) dfs(2,2) dfs(2,2),而你又在此之前查询了 d f s ( 2 , 2 ) dfs(2,2) dfs(2,2),这明显可以用一个数组 f f f 来存储。在此之前,有 d f s ( 2 , 2 ) dfs(2,2) dfs(2,2) 的存在,那么设 c n t cnt cnt 为访问 d f s ( 2 , 2 ) dfs(2,2) dfs(2,2) 的次数, t t t 为 d f s ( 2 , 2 ) dfs(2,2) dfs(2,2) 的时间,那么可以优化 ( c n t − 1 ) × ( t − 1 ) (cnt-1)\times (t-1) (cnt−1)×(t−1) 的时间。
例题1
这一道题目在 dfs 的基础上需要有剪枝。很明显, d e p dep dep 曾的 i i i 必须从上一个 i i i 枚举到之前的答案加上 ( k − d e p ) × i (k-dep)\times i (k−dep)×i,这是一个剪枝。
#include<bits/stdc++.h>
using namespace std;
int n,k,ans;
void dfs(int dep,int sum,int last){
if(dep==k){
ans+=(sum==n);
return;
}
for(int i=last;sum+(k-dep)*i<=n;++i){
dfs(dep+1,sum+i,i);
}
}
int main(){
scanf("%d %d",&n,&k);
dfs(0,0,1);
printf("%d",ans);
return 0;
}
例题2
这一道题目需要使用最优性剪枝,即如果拼出的目标长度大于了目标长度,那就跳过,因为这样肯定有一个更优的答案小于目标长度。
#include<bits/stdc++.h>
#define MAXN 101
using namespace std;
int n,cnt,maxi=INT_MIN,mini=INT_MAX;
int a[MAXN],ans[MAXN];
void dfs(int dep,int now,int len,int pos){
if(!dep){
printf("%d",len);
exit(0);
}
if(now==len){
dfs(dep-1,0,len,maxi);
return;
}
for(int i=pos;i>=mini;--i){
if(ans[i]&&i+now<=len){
--ans[i];
dfs(dep,i+now,len,i);
++ans[i];
if(!now||now+i==len){
break;
}
}
}
}
int main(){
scanf("%d",&n);
int len,sum=0;
while(n--){
scanf("%d",&len);
if(len<=50){
a[++cnt]=len;
maxi=max(maxi,len);
mini=min(mini,len);
++ans[len];
sum+=len;
}
}
len=sum>>1;
for(int i=maxi;i<=len;++i){
if(sum%i==0){
dfs(sum/i,0,i,maxi);
}
}
printf("%d",sum);
return 0;
}
例题3
这一道题目是求 w ( x , y , z ) w(x,y,z) w(x,y,z),很明显满足 dfs。考虑记忆化搜索,这样,就可以避免大量的无意义运算。最多 2 0 3 20^3 203 次的不同的运算求出 a n s ans ans,那么之后就只需要求 w ( x , y , z ) = a n s x , y , z w(x,y,z)=ans_{x,y,z} w(x,y,z)=ansx,y,z 了。
#include<bits/stdc++.h>
#define MAXN 22
using namespace std;
typedef long long ll;
ll ans[22][22][22];
inline ll dfs(int x,int y,int z){
if(x<=0||y<=0||z<=0){
return ans[0][0][0]=1;
}
if(x>20||y>20||z>20){
return dfs(20,20,20);
}
if(ans[x][y][z]){
return ans[x][y][z];
}
if(x<y&&y<z){
return ans[x][y][z]=dfs(x,y,z-1)+dfs(x,y-1,z-1)-dfs(x,y-1,z);
}
return ans[x][y][z]=dfs(x-1,y,z)+dfs(x-1,y-1,z)+dfs(x-1,y,z-1)-dfs(x-1,y-1,z-1);
}
int main(){
while(true){
ll x,y,z;
scanf("%lld %lld %lld",&x,&y,&z);
if(x==-1&&y==-1&&z==-1){
return 0;
}
printf("w(%lld, %lld, %lld) = %lld\n",x,y,z,dfs(x,y,z));
}
return 0;
}
bfs 优化
双向 bfs
双向 bfs 的思想是每一次同时从起点和终点同时搜索,搜索到重合就可以得出答案。
例如,根节点可以衍生出深度为 n n n 的搜索树,时间复杂度为 O ( 2 n ) \operatorname{O}(2^n) O(2n),但是如果用折半搜索,开头可以衍生出深度为 ⌊ n 2 ⌋ \lfloor\frac{n}{2}\rfloor ⌊2n⌋ 的搜索树,结尾可以衍生出深度为 ⌈ n 2 ⌉ \lceil\frac{n}{2}\rceil ⌈2n⌉ 的搜索树,时间复杂度为 2 ⌈ n 2 ⌉ + 1 2^{\lceil\frac{n}{2}\rceil+1} 2⌈2n⌉+1。
例题
可以从开头状态和结尾状态同时搜索,用的步数用 map
记录下来。套用双向 bfs 板子即可。
由于双向 bfs 例题太难找了,所以只找了正常 bfs 也能过的题目。
#include<iostream>
#include<string>
#include<queue>
#include<map>
using namespace std;
string s,t,a[10],b[10];
int n=1;
inline int bfs(){
map<string,int> mp1,mp2;
queue<string> q1,q2;
int step=0;
q1.push(s);
mp1[s]=0;
q2.push(t);
mp2[t]=0;
string s,s2;
while(++step<=5){
while(mp1[q1.front()]==step-1){
s=q1.front();
q1.pop();
for(int i=1;i<=n;++i){
size_t pos=0;
while(pos<s.size()){
if(s.find(a[i],pos)==s.npos)break;
s2=s;
s2.replace(s2.find(a[i],pos),a[i].size(),b[i]);
if(mp1.find(s2)!=mp1.end()){
++pos;
continue;
}
if(mp2.find(s2)!=mp2.end()){
return step*2-1;
}
q1.push(s2);
mp1[s2]=step;
++pos;
}
}
}
while(mp2[q2.front()]==step-1){
s=q2.front();
q2.pop();
for(int i=1;i<=n;++i){
size_t pos=0;
while(pos<s.size()){
if(s.find(b[i],pos)==s.npos)break;
s2=s;
s2.replace(s2.find(b[i],pos),b[i].size(),a[i]);
if(mp2.find(s2)!=mp2.end()){
++pos;
continue;
}
if(mp1.find(s2)!=mp1.end()){
return step*2;
}
q2.push(s2);
mp2[s2]=step;
++pos;
}
}
}
}
return -1;
}
int main(){
cin>>s>>t;
while(cin>>a[n]>>b[n]){
n++;
}
int ans=bfs();
if(ans==-1){
cout<<"NO ANSWER!";
}else{
cout<<ans;
}
return 0;
}
双端队列优化
双端队列优化用于 01bfs,由于每一个点的权值一定是 0 0 0 或者 1 1 1,虽然不同,但是把 0 0 0 从队头入队, 1 1 1 从队尾入队仍然满足第一个就是答案。
例题
很明显,每一个权值扩展只可能是 0 0 0 或者 1 1 1。如果为 0 0 0,那么放在队首,反之,放在队尾。用 d i s x , y dis_{x,y} disx,y 记录答案,并进行松弛即可。
#include<cstring>
#include<cstdio>
#include<deque>
#define MAXN 505
using namespace std;
struct node{
int x,y,step;
};
int n,m,sx,sy,tx,ty;
char mp[MAXN][MAXN];
bool vis[MAXN][MAXN];
int dis[MAXN][MAXN];
int dx[4]={0,0,1,-1};
int dy[4]={1,-1,0,0};
inline int bfs(int sx,int sy){
memset(vis,0,sizeof(vis));
memset(dis,0x3f,sizeof(dis));
deque<node> q;
q.push_back((node){sx,sy,0});
vis[sx][sy]=true;
dis[sx][sy]=0;
while(!q.empty()){
int x=q.front().x;
int y=q.front().y;
int step=q.front().step;
q.pop_front();
if(x==tx&&y==ty){
return step;
}
for(int i=0;i<4;++i){
int nx=x+dx[i];
int ny=y+dy[i];
if(!(1<=nx&&nx<=n&&1<=ny&&ny<=m)){
continue;
}
if(dis[nx][ny]>step+(mp[x][y]==mp[nx][ny])){
dis[nx][ny]=step+(mp[x][y]==mp[nx][ny]);
if(!vis[nx][ny]){
vis[nx][ny]=true;
if(mp[x][y]==mp[nx][ny]){
q.push_front((node){nx,ny,step});
}else{
q.push_back((node){nx,ny,step+1});
}
}
}
}
}
return -1;
}
inline void work(){
for(int i=1;i<=n;++i){
scanf("%s",mp[i]+1);
}
scanf("%d %d %d %d",&sx,&sy,&tx,&ty);
++sx;
++sy;
++tx;
++ty;
printf("%d\n",bfs(sx,sy));
}
int main(){
while(~scanf("%d %d",&n,&m)&&n&&m){
work();
}
return 0;
}
优先队列优化
优先队列优化和双端队列优化的形式差不多,都是为了维持对内元素的单调性而产生的。思想是维护 bfs 的“搜到即停”的性质。在每次扩展结点深度不止加 1 1 1 的时候,对内元素可能不是有序的,需要使用数据结构维护单调性。(盲猜线段树或者平衡树也可以)
类似于 Dijiestra,每次把最小的出队,如果这就是答案,那么其余对内元素即使有答案也不会比这个更优。如果不是,那就继续扩展出来新的节点。
例题
是 Dijiestra 的经典板子。思想同上,还可以用线段树优化和平衡树优化,平衡树和堆效果相同,而且常数和空间更大,因此不推荐使用。
#include<cstring>
#include<cstdio>
#include<queue>
#define MAXN 100001
#define MAXM 200002
using namespace std;
typedef long long ll;
struct node{
int next,to;
ll dis;
}edge[MAXM];
int cnt,head[MAXN];
ll dis[MAXN];
bool vis[MAXN];
inline void addedge(int from,int to,ll dis){
edge[++cnt].to=to;
edge[cnt].dis=dis;
edge[cnt].next=head[from];
head[from]=cnt;
}
inline void Dijiestra(int s){
memset(dis,0x7f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[s]=0ll;
priority_queue<pair<ll,int>,vector<pair<ll,int> >,greater<pair<ll,int> > > q;
q.push(make_pair(0ll,s));
while(!q.empty()){
int from=q.top().second;
q.pop();
vis[from]=false;
for(int i=head[from];i;i=edge[i].next){
int to=edge[i].to;
if(dis[to]>dis[from]+edge[i].dis){
dis[to]=dis[from]+edge[i].dis;
if(!vis[to]){
q.push(make_pair(dis[to],to));
vis[to]=true;
}
}
}
}
}
int main(){
int n,m,s;
scanf("%d %d %d",&n,&m,&s);
while(m--){
int from,to;
ll dist;
scanf("%d %d %lld",&from,&to,&dist);
addedge(from,to,dist);
}
Dijiestra(s);
for(int i=1;i<=n;++i){
printf("%lld ",dis[i]);
}
return 0;
}
折半搜索
折半搜索是可以将指数折半的一种搜索,它的思想就是确定某一种意义下的 h e a d 1 head1 head1 和 h e a d 2 head2 head2,然后往后搜索。有些情况,搜索出来会使得 t a i l 1 = t a i l 2 tail1=tail2 tail1=tail2,而有些情况,会使得 t a i l 1 + 1 = h e a d 2 tail1+1=head2 tail1+1=head2。不管怎么样,如果最开始是 k n k^n kn 会爆,那么折半搜索能优化到 k ⌊ n 2 ⌋ + 1 k^{\lfloor\frac{n}{2}\rfloor+1} k⌊2n⌋+1 的复杂度。
例题
这一道题目可以枚举每一种状态选或者不选,复杂度 2 n 2^n 2n。但是 n n n 可以达到 40 40 40,所以考虑折半。折半搜索的结果是可以拼凑出多少种不同的方案,用 dfs 实现。一半枚举 2 ( 1 , ⌊ n 2 ⌋ ) 2^{(1,\lfloor\frac{n}{2}\rfloor)} 2(1,⌊2n⌋),一半枚举 2 ( ⌊ n 2 ⌋ + 1 , n ) 2^{(\lfloor\frac{n}{2}\rfloor+1,n)} 2(⌊2n⌋+1,n),然后二分查找出耗费了前半段那么多钱,后半段能够耗费多少钱,然后统计即可。
#include<bits/stdc++.h>
#define MAXN 41
#define MAXM 1<<20|1
using namespace std;
typedef long long ll;
int n,cnta,cntb;
ll m,w[MAXN],suma[MAXM],sumb[MAXM],ans;
void dfs(int l,int r,ll sum,ll a[],int &cnt){
if(sum>m){
return;
}
if(l>r){
a[++cnt]=sum;
return;
}
dfs(l+1,r,sum+w[l],a,cnt);
dfs(l+1,r,sum,a,cnt);
}
int main(){
scanf("%d %lld",&n,&m);
for(int i=1;i<=n;++i){
scanf("%lld",&w[i]);
}
int mid=n>>1;
dfs(1,mid,0,suma,cnta);
dfs(mid+1,n,0,sumb,cntb);
sort(suma+1,suma+1+cnta);
for(int i=1;i<=cntb;++i){
ans+=upper_bound(suma+1,suma+1+cnta,m-sumb[i])-suma-1;
}
printf("%lld",ans);
return 0;
}
A-star
A-star 属于玄学时间算法。它的思想是对于每一个点 p p p,都有从 h e a d head head 到 p p p 的函数 p r e ( p ) pre(p) pre(p),也有从 p p p 到 t a i l tail tail 的函数 n x t ( p ) nxt(p) nxt(p),还有估价函数 g o a l ( p ) = p r e ( p ) + n x t ( p ) goal(p)=pre(p)+nxt(p) goal(p)=pre(p)+nxt(p),然后以估价函数的值来搜索。在此之前,我们还需要引入一个定理:优先队列优化 bfs。
通用的估价函数 g o a l goal goal 有以下几个形式:
- 哈曼顿距离,即两个点在平面上无障碍在平行的线上需要走的距离。
inline int goal(int a,int b){
return abs(x[a]-x[b])+abs(y[a]-y[b]);//哈曼顿距离
}
- 对角线,这个是应用于 8 8 8 个方向的网格图。
inline int goal(int a,int b){
return max(abs(x[a]-x[b]),abs(y[a]-y[b]));//对角线距离
}
- 直线距离,即两个点走直线。
inline int goal(int a,int b){
return sqrt((x[a]-x[b])*(x[a]-x[b])+(y[a]-y[b])*(y[a]-y[b]));//直线距离
}
优先队列优化 bfs 在一次性扩展深度不一定加一的 bfs 中用于规划最小值,每一次取出的是最优的,那就满足了 bfs 的自带的最优性剪枝。优先队列优化本质上也是贪心加上最优性剪枝。
这些函数满足三角形不等式,所以 bfs 的证明是正确的。事实上,A-star 还可以变式成堆优化的 Dijiestra。
例题
这道题目就是求 K 短路径。可以考虑设置 p r e pre pre 函数为当前距离,这个可以用 Dijiestra 预处理。之后的 n x t nxt nxt 可以考虑用当前路径的长度,这样还是满足三角形不等式的,所以,之后用 A-star 跑。
#include<bits/stdc++.h>
#define MAXN 1001
#define MAXM 10001
using namespace std;
typedef long long ll;
typedef pair<ll,int> pli;
struct node{
int next,to;
ll dis;
}edge[MAXM][2];
struct cmp{
int pos;
ll dis;
};
int n,m,k,cnt[2],head[MAXN][2];
ll dis[MAXN];
bool vis[MAXN];
inline bool operator<(const cmp &x,const cmp &y){
return x.dis+dis[x.pos]>y.dis+dis[y.pos];
}
inline void addedge(int x,int from,int to,ll dis){
edge[++cnt[x]][x].to=to;
edge[cnt[x]][x].dis=dis;
edge[cnt[x]][x].next=head[from][x];
head[from][x]=cnt[x];
}
inline void dijiestra(){
priority_queue<pli,vector<pli>,greater<pli> > q;
q.push(make_pair(0ll,1));
memset(dis,0x7f,sizeof(dis));
dis[1]=0;
vis[1]=true;
while(!q.empty()){
int front=q.top().second;
q.pop();
vis[front]=false;
for(int i=head[front][1];i;i=edge[i][1].next){
int to=edge[i][1].to;
if(dis[to]>dis[front]+edge[i][1].dis){
dis[to]=dis[front]+edge[i][1].dis;
if(!vis[to]){
q.push(make_pair(dis[to],to));
vis[to]=true;
}
}
}
}
}
inline void A_star(){
priority_queue<cmp> q;
q.push((cmp){n,0ll});
while(!q.empty()){
cmp front=q.top();
q.pop();
if(front.pos==1){
printf("%lld\n",front.dis);
if((--k)==0){
return;
}
continue;
}
int from=front.pos;
for(int i=head[from][0];i;i=edge[i][0].next){
int to=edge[i][0].to;
q.push((cmp){to,front.dis+edge[i][0].dis});
}
}
}
int main(){
scanf("%d %d %d",&n,&m,&k);
if(!k){
return 0;
}
while(m--){
int from,to;
ll dis;
scanf("%d %d %lld",&from,&to,&dis);
addedge(0,from,to,dis);
addedge(1,to,from,dis);
}
dijiestra();
A_star();
while(k--){
puts("-1");
}
return 0;
}
为什么说玄学时间算法?因为有时候 A-star 并不能够优化时间,在考场上能造出数据点卡 A-star,K 短路问题还需要通过其他的方式算出来,比如可持久化可并堆,详见【模板】k 短路 / [SDOI2010] 魔法猪学院。
ID
ID 是 iterate-deepning 的缩写,中翻迭代加深搜索。主要思想是结合了 dfs 和 bfs 的思想,每一次设置一个深度 d e p dep dep,如果深度超过 d e p dep dep,那就再把 d e p + 1 dep+1 dep+1,如果搜到了最佳答案,那就结束。ID 优化了 bfs 的空间不足和 dfs 的时间不足。
例题
这一道题目可以考虑枚举个数,用 ID 来搜。然后要搜分母,统计答案。此外,还需要正确性剪枝。每一次如果 x × n x t ≥ y × ( m a x d e p − d e p + 1 ) x\times nxt\ge y\times(maxdep-dep+1) x×nxt≥y×(maxdep−dep+1),那么一定不合法。
#include<bits/stdc++.h>
#define MAXN 202
#define MAXM 1001
using namespace std;
typedef long long ll;
int maxdep;
ll n,m,zip[MAXN],ans[MAXN];
bool f=true;
inline bool check(){
for(int i=maxdep;i>=1;--i){
if(!ans[i]){
return true;
}else if(ans[i]!=zip[i]){
return zip[i]<ans[i];
}
}
return false;
}
void iddfs(int dep,ll x,ll y,ll last){
if(dep==maxdep){
if(y%x){
return;
}
zip[maxdep]=y/x;
if(check()){
for(int i=1;i<=maxdep;++i){
ans[i]=zip[i];
}
}
f=false;
return;
}
for(ll i=max(last,y/x+1);x*i<y*(maxdep-dep+1);++i){
ll nxtx=x*i-y;
ll nxty=y*i;
zip[dep]=i;
ll g=__gcd(nxtx,nxty);
iddfs(dep+1,nxtx/g,nxty/g,i+1);
}
}
int main(){
scanf("%lld %lld",&n,&m);
f=true;
for(maxdep=2;f;++maxdep){
memset(zip,0,sizeof(zip));
iddfs(1,n,m,n/m+1);
}
--maxdep;
for(int i=1;i<=maxdep;++i){
printf("%lld ",ans[i]);
}
}
IDA-star
IDA-star 是结合了 A-star 和 ID 的算法,它的具体实现是 dfs,只不过是每次需要 g o a l goal goal 进行扩展。A-star 优化的是 bfs,ID 优化的是所有广搜,那么正好 ID 可以优化 A-star。相较于 A-star,IDA-star 的优势是空间。
例题
这一道题目,需要在 15 15 15 步内走完,很像 ID 的 m a x d e p maxdep maxdep 限制条件,然后可以考虑 g o a l goal goal 函数定义成至少要多少步,典型的 A-star,之后,再加上一些剪枝优化即可。
#include<bits/stdc++.h>
#define MAXN 6
using namespace std;
char a[MAXN][MAXN];
char mp[MAXN][MAXN]={
{'0','0','0','0','0','0'},
{'0','1','1','1','1','1'},
{'0','0','1','1','1','1'},
{'0','0','0','*','1','1'},
{'0','0','0','0','0','1'},
{'0','0','0','0','0','0'}
};
int dx[9]={0,-2,-2,-1,-1,1,1,2,2};
int dy[9]={0,-1,1,-2,2,-2,2,-1,1},ans;
inline int goal(){
int val=0;
for(int i=1;i<=5;++i){
for(int j=1;j<=5;++j){
if(a[i][j]!=mp[i][j]){
++val;
}
}
}
return val;
}
void dfs(int x,int y,int dep,int last){
int val=goal();
if(dep+val>16||dep>=ans){
return;
}
if(val==0){
ans=dep;
return;
}
for(int i=1;i<=8;++i){
if(x+dx[i]<1||x+dx[i]>5||y+dy[i]<1||y+dy[i]>5){
continue;
}
if(last+i!=9){
swap(a[x][y],a[x+dx[i]][y+dy[i]]);
dfs(x+dx[i],y+dy[i],dep+1,i);
swap(a[x][y],a[x+dx[i]][y+dy[i]]);
}
}
return;
}
inline void work(void){
for(int i=1;i<=5;++i){
scanf("%s",a[i]+1);
}
int sx,sy;
for(int i=1;i<=5;++i){
for(int j=1;j<=5;++j){
if(a[i][j]=='*'){
sx=i;
sy=j;
}
}
}
ans=INT_MAX;
dfs(sx,sy,0,0);
printf("%d\n",ans==INT_MAX?-1:ans);
}
int main(){
int t;
scanf("%d",&t);
while(t--){
work();
}
return 0;
}