搜索指南 QwQ

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 1n10
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) (cnt1)×(t1) 的时间。

例题1

这一道题目在 dfs 的基础上需要有剪枝。很明显, d e p dep dep 曾的 i i i 必须从上一个 i i i 枚举到之前的答案加上 ( k − d e p ) × i (k-dep)\times i (kdep)×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} 22n+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} k2n+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×nxty×(maxdepdep+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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值