搜索学习笔记+杂题 (进阶一 dfs/bfs的进阶)

前言:

没啥好说的了。

所以只能来写博客了。

搜索杂题:

相关题单:戳我

三、进阶 dfs/bfs

1、dfs进阶——折半搜索(meet in the middle)

由于深搜的时间复杂度在每种状态有两个分支的情况下是\(O(2^n)\)。所以一般暴力深搜的数据范围就在\(20-25\)之间。而对于有一大类的题,它的搜索思路非常的清晰,可以很快的打出深搜的代码,但是出题人像是故意卡深搜一样,将数据范围扩大一倍,使搜索的数据范围在\(40-50\)左右。那么在这种情况下,我们就可以使用折半搜索(meet in the middle)。(本篇博文中说的折半搜索就是meet in the middle哒)。

但需要注意的是,折半搜索应用的时候需要满足以下条件:

  • 搜索各项不能相互干扰。

  • 需要满足搜索状态可逆,就是说一个状态可以从两个方向都得到。

折半搜索顾名思义,将需要搜索的数据整体二分进行DFS ,最后再进行合并,所以它的时间复杂度就为\(O(2*2^{n/2}+q(n))\)。其中\(2*2^{n/2}\)为搜索的复杂度,\(q(n)\) 为合并的时间复杂度。因为合并在大多数情况下并不会占用过多的时间,所以基本上是可以过比暴力多一倍的数据。

既然前面说到meet in the middle的特点,搜索状态是非常容易就想出来的,所以一般种类题目的难点就是将前一半搜索出来的答案与后一半搜索出来的答案进行合并,得到最终的答案,而且这个合并的时间复杂度不能过高,一般\(40-50\)的数据每部分可以搜出\(2^{20}\)种情况,如何将前后\(2^{20}\)种状态用巧妙地思路进行合并就需要一些技巧。

P1466 [USACO2.2] 集合 Subset Sums

折半搜索的板子题,很明显,数据范围只有\(39\),超出了深搜的数据范围,但可以很快的想出暴力深搜的方法,只需要从前往后搜,判断当前这个数取不取,并记录当前已经取了数的和,如果和是全部的一半那么就ans++,如果和超过了全部的一半,那么就返回。但是这样的剪枝思路还是太单薄了,明显没法跑过极限数据。

所以考虑折半搜索,折半搜索的思路其实很简单,就是将原序列从中间分成两半,搜前一半的数各种选法所可以组成的各种和以及后一半的数各种选法所可以组成的和,明显剪枝思路都是一样的,超过了一半就返回,然后将所有小于等于所有数的一半的答案放入vector中记录下来。

然后我们就要从两个vector中各选出一个数相加,这样组成了整个序列选与不选各种方案所取得数的和,现在的时间复杂度是\(O(2*2^{n/2})\),但是我们可以像这样合并嘛,显然不行。因为前面与后面的方案数也可以达到\(2^{n/2}\)极端情况下就是前后都有将近\(2^{20}\)种方案,要是一一枚举那么时间复杂度就是\(O(n^2)\)的了,这和暴力深搜就是一样的了。

那怎么合并,由于我们从两个序列中各取一个数,要求这两个数的和是一个定值,那如果我们枚举一个数,那么另外一个符合题意的数也就固定下来了,在一个序列中找一个定值的个数时间复杂度总低了吧,直接固定后一半的方案,将前一半的方案从小到大排个序,二分查找定值就可以了,这样的合并的时间复杂度就是\(O(mlogm)\)的,其中\(m=2^{n/2}\)。(STL还是相当好用的)。

介绍一下二分的STL的一些用法,主要就是lower_bound与upper_bound。lower_bound返回第一个大于等于找的值下标,upper_bound返回第一个大于查找的值的下标,所以你把lower_bound返回的下标减一就是序列中小于找的值的最大数,把upper_bound返回的下标减一就是序列中小于等于找的值的最大数。

给出模板题的代码

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=45;
int n,sum;
int a[M];
vector<int> k1,k2;//用vector存方案比较简单,而且空间可以自由变化。

//分别代表现在是第几个数,搜索的边界,当前的和,以及用哪个vector来存答案
inline void dfs(int l,int r,int res,vector<int> &op)//你要修改当前vector的值当然要用&符号
{
	if(l>r)//到达边界
	{
		op.push_back(res);//存答案
		return ;
	}
	dfs(l+1,r,res+a[l],op);//要选这个数
	dfs(l+1,r,res,op); //不选
}//原来我没有写剪枝

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++) a[i]=i,sum+=a[i];
	if(sum%2==1)
	{
		cout<<"0\n";return 0;
	}
	sum/=2;//目标值
	int mid=(n>>1);
	dfs(1,mid,0,k1);
	dfs(mid+1,n,0,k2);//分成两半搜,拿不同的vector存
	sort(k1.begin(),k1.end());//二分查找的数组必须是有序的,这里固定k2,在k1里面找,所以要将k1排序
	int ans=0;
	
	for(int i=0;i<k2.size();i++)
	{
		if(sum-k2[i]<0) continue;
		int posl=lower_bound(k1.begin(),k1.end(),sum-k2[i])-k1.begin();
		int posr=upper_bound(k1.begin(),k1.end(),sum-k2[i])-k1.begin();
		ans+=posr-posl;//二分的STL相当好用
	}
	cout<<ans/2<<"\n"; //记得除2,打个比方,对于1 2 3 4 ,你搜了1和4,也搜了2和3,但实际上这是同一种
	return 0;
}

P5194 [USACO05DEC] Scales S

思路和上一道题很想吧,同样是二分搜索的板子题,但是我们观察题目给的范围$ N \ ( 1 \leq N \leq 1000 ) $,呵呵,做Nim呢。但是很显然这只是一个迷惑人的数据范围,毕竟要真是这个范围就不是黄题了。

题面中说每个砝码的质量至少等于前面两个砝码(也就是质量比它小的砝码中质量最大的两个)的质量的和,那么就可以得出$ N \ ( 1 \leq N \leq 30) $这个小清新数据范围,但这个数据范围用普通的暴力深搜也是不行的,所以考虑折半搜索。

首先很容易想出暴力的深搜,判断每一个物品是选还是不选,然后将要选的物品总和加上,如果超出了限制就返回,否则就判断是否比当前答案大,大就更新答案。折半搜索的搜索方法一样的,于是我们将前一半各数选与不选得到的质量与后一半各数选与不选得到的质量一样的用两个vector存起来。最后合并时固定其中一个vector,枚举其中的每个数,去找在另外一个vector中不大于两个之后不大于要求的最大值,更新答案。

所以搜索与合并的时间复杂度与上一题一模一样。

关键代码:

inline void dfs(int l,int r,int sum,vector<int> &op)//同上一道题
{
	if(sum>m) return ;
	if(l>r)
	{
		op.push_back(sum);//存答案
		return ;
	}
	dfs(l+1,r,sum,op);
	dfs(l+1,r,sum+a[l],op);//选与不选
}

主函数内:

	int mid=n>>1;
	dfs(1,mid,0,k1);
	dfs(mid+1,n,0,k2);//折半搜
	sort(k2.begin(),k2.end());//将其中一个固定,将另一个排序
	int maxx=0;
	for(int i=0;i<k1.size();i++)
	{
		int posl=upper_bound(k2.begin(),k2.end(),c-k1[i])-k2.begin()-1;//找到小于等于给定值的最大值下标
		maxx=max(maxx,k2[posl]+k1[i]);//更新哒
	}
	cout<<maxx<<"\n";

[ABC184F] Programming Contest

上一题的双倍经验,甚至比上一题都还要简单,已经告诉你正确的取值范围(上一题还有可能被神奇的数据范围给诈骗)。

代码基本同上。

P4799 [CEOI2015 Day2] 世界冰球锦标赛

严格意义上这道才是真正的折半搜索模板题(但谁让我找了两道更无脑的)。这一道你不觉得和上一道也很类似嘛(对的,折半搜索都很巧妙,所以好的trick较少,都比较相似)。同样的,数据范围是$ N \ ( 1 \leq N \leq 40) $,那多半就是折半搜索了。

暴力深搜和上两道一样的,判断当前这一场比赛看不看,然后记录下所有的方案所需的钱,当然你也可以剪枝,就是和已经超过限制了就返回。

考虑合并,两部分相加不就是总共需要的钱嘛,那么一样的,考虑固定一个数组,将另外一个数组进行排序,然后二分查找,找到最大的和自己相加后不会超过限制的下标(这简直就是了上一道的双倍经验),但这到要求的是方案数,由于我们已经将数组从小到大排好序了,所以我们找到的下标之前的所有数都满足要求,所以我们直接加上找到的下标就行了。

搜索与合并的时间复杂度与第一题一模一样。

关键代码:

	//搜索部分和上面两道题是一样的
	int mid=n>>1;
	dfs(1,mid,0,k1);
	dfs(mid+1,n,0,k2);//折半
	sort(k1.begin(),k1.end());//固定一个
	int ans=0;
	for(int i=0;i<k2.size();i++)
	{
		ans+=upper_bound(k1.begin(),k1.end(),k-k2[i])-k1.begin();//值得注意的是,vector的下标是从0开始的
		//查找对于k2[i]有多少个满足要求。
	}
	cout<<ans<<"\n";

P5691 [NOI2001] 方程的解数

也是先看数据范围,\(1\le n \le 6\)\(1\le m \le 150\)所有的数都是整数,分析一下时间复杂度,对于每一个\(x_i\)都有\(150\)种情况,所以时间复杂度就是\(O(150^6)\),这样朴素的枚举搜索就过不去。

朴素的深搜就是搜索每一个可能的取值,然后将搜得的\(x_i\)带入原式,然后计算最后的答案是否等于\(0\),是\(0\)\(ans++\)

考虑优化,对于前三个数的取值与后三个数的取值进行折半搜索,分别代入原式计算出对应的值,然后也是固定其中一个vector,将另外一个vector排序,查找里面是否有自己的相反数,加起来就可以了,和上面几道是一样的做法(当然你也可以使用哈希判断,要快一点)。

我们计算一下时间复杂度(使用排序合并),极端情况下,时间复杂度就是\(O(2*150^3+150^3*log_{2}150^3)\)大概就是\(1e8\),而时间给了整整6s,非常的优秀。

关键代码:

inline void dfs(int l,int r,int sum,vector<int> &op)//同上
{
	if(l>r)
	{
		op.push_back(sum);
		return ;
	}
	for(int i=1;i<=m;i++)
	{
		dfs(l+1,r,sum+k[l]*pow(i,p[l]),op);//模拟它的运算法则
	}
}

主函数:

	dfs(1,mid,0,k1);
	dfs(mid+1,n,0,k2);//折半
	sort(k2.begin(),k2.end());//排序
	int ans=0;
	for(int i=0;i<k1.size();i++)
	{
		int posl=lower_bound(k2.begin(),k2.end(),-k1[i])-k2.begin();
		int posr=upper_bound(k2.begin(),k2.end(),-k1[i])-k2.begin();//由于两个相加的和为0,所以找的是-k1[i]
		ans+=posr-posl;
	}

[ABC271F] XOR on Grid Path

终于来了一个不一样的折半搜索,非常的有趣,它给定一个n行n列的矩阵,定义合法路径为只向右或向下的路径,且途径数字异或和为\(0\)。求合法路径条数。

观察范围,\(1\le n \le 20\),每一个点都有向下与向右两种情况,估算一下,直接从起点出发搜到终点好像时间复杂度有些没法接受所以我们考虑折半搜索,这样\(1\le n \le 10\),差不多时间复杂度是可以接受的。

首先判断是否可以折半(总的答案是否可以由前后两部分凭借而成)。由于异或具有很多的性质,这道题要用到它的交换律,也就是说,异或一个数,先异或与后异或得到的答案是一样的(a异或b=b异或a),所以我们计算出前半段的答案再异或出后半段的答案就是这条路径总的答案。

然后思考怎么折半,这道题不像前几道题是一个连续的序列,而是一张二维的地图。那么你可以在这个地图上割一刀,表示前一段与后一段的分界线(这道题由于地图是\(n*n\)的,所以我选择的分界线就是这个正方形的对角线,因为对角线上的点比较好判断)。那么前一半从起点搜到对角线,后一半从终点搜到对角线,这样就是折半了。

最后考虑如何合并,由于我们要求的是一段路径,所以前后合并的时候肯定是在对角线上的同一个点,那么我们可以在搜前半段的时候,将每个以对角线上某点为边界的所有路径的异或和拿一个东西(vector或数组都可以)存在那个点上,那么我们就可以知道从起点出发,到达每一个对角线上的点的路径异或和情况了,将每个点存在的异或和排个序,方便合并时的二分查找。

接着搜后半段,记录路径异或和,在到达边界线(对角线)时,借助异或的性质(\(a^a=0\)),就可以使用二分查找找到在这个点上有多少个与自己从终点出发到达对角线的异或和的相同的值,统计答案即可(好像有点乱,建议自己理一理)。

这种类型的题无论是搜索还是合并都相当巧妙。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<vector>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=21;
int n,ans;
int mapp[M][M];
vector<int> t[M];

inline void dfs(int x,int y,int sum,int flag)//flag为0时代表从起点出发,为1时代表从终点出发。
{
	if(x<1||x>n||y<1||y>n) return ;//超出边界了
	if(x+y==n+1)//对角线上的点 x+y==n+1
	{
		if(!flag)
		{
			t[x].push_back(sum^mapp[x][y]);//对角线上的点明显一行就只有一个,所以用x的值代表每一行的对角线的点
			//到达边界的时候边界上的值还没有异或,所以要将边界上的值异或后放入vector中
		}
		else//两个相同数的异或起来为0 
		{
			int posl=lower_bound(t[x].begin(),t[x].end(),sum)-t[x].begin();
			int posr=upper_bound(t[x].begin(),t[x].end(),sum)-t[x].begin();//查找相同的值
			if(posl<posr)
			{
				ans+=posr-posl;//统计答案
			}
		}
		return ;
	}
	if(!flag)//从起点出发就是朝右下走 
	{
		dfs(x,y+1,sum^mapp[x][y],flag);
		dfs(x+1,y,sum^mapp[x][y],flag);
	}
	else//从终点出发就是朝左上走 
	{
		dfs(x,y-1,sum^mapp[x][y],flag);
		dfs(x-1,y,sum^mapp[x][y],flag);
	}
}

signed main()
{
	//ios::sync_with_stdio(false);
	//cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			cin>>mapp[i][j];
		}
	}
	dfs(1,1,0,0);//搜前半段
	for(int i=1;i<=n;i++) sort(t[i].begin(),t[i].end());//以每个每个对角线为终点的路径异或和
	dfs(n,n,0,1);//搜后半段
	cout<<ans<<"\n";
	return 0;
}

CF1006F Xor-Paths

很明显,这是上一道题的双倍经验,只有两个不同的地方。

一是这道题的图不是正方形而是一个矩形,所以它的对角线就不是整点,所以你需要另外找一个合适的边界(我找的其实也是对角线,从右上方45°向下的一条直线,这天线就有一个性质\(x+y==m+1\),比较好判断边界)。

二是这道题目求得路径异或和不是0了,变成了一个给定的值,但这都是小问题,同样利用异或的性质,异或运算是可逆的,如果a异或b=c,那么b异或c=a。所以你在搜后半段的时候,到达边界时,查找的就应该时后半段的路径和异或上要求找的值。

关键代码:

inline void dfs(int x,int y,int res,int flag)//同上
{
	if(x<1||x>n||y<1||y>m) return;
	if(x+y==1+m)//注意前后两段的边界条件
	{
		if(!flag)
		{
			v[x].push_back(res^=mapp[x][y]);//同上
		}
		else
		{
			int fx=k^res;//要异或上要求的值再去查找
			int posl=lower_bound(v[x].begin(),v[x].end(),fx)-v[x].begin();
			int posr=upper_bound(v[x].begin(),v[x].end(),fx)-v[x].begin();
			ans+=posr-posl;
		}
		return ;
	}
	if(!flag)
	{
		dfs(x+1,y,res^mapp[x][y],flag);
		dfs(x,y+1,res^mapp[x][y],flag);
	}
	else
	{
		dfs(x-1,y,res^mapp[x][y],flag);
		dfs(x,y-1,res^mapp[x][y],flag);
	}//同上
}

CF888E Maximum Subsequence

非常吉利的题号呐,这也是一道折半搜索的板子题,同时它也拥有一个非常美妙的trick。给一个数列和m,在数列任选若干个数,使得他们的和对\(m\)取模后最大。翻译的非常简练。先观察数据范围\(1\le n \le 35\),差不多就可以知道是一道折半了。

首先考虑暴力搜索应该怎么做,显然,还是对任意一个数都有选与不选两种情况,时间复杂度过不去,那就折半的暴力搜索。

可不可以使用折半?显然答案就是前一段的和+后一段的和再去模上给定的数,所以我们还是用两个vector将所有的情况存起来,深搜的时候记录总和,顺便取模。这道题的搜索部分还是相当板子的。

重点在合并的部分,现在两个vector中都存了所有可能的答案,按照惯例我们应该二分搜索,但是好像这题有一点不好二分查找,因为两个数加起来有可能就大于了给定的模数,一模和就变小了,破坏了单调性,所以肯定是不可以使用二分搜索的。

于是我们考虑贪心的合并,对于两个都小于模数的数,相加起来要么小于模数,要么小于两倍模数。那么问题就简单了,对于两个相加大于1倍模数但是小于两倍模数的方案,明显两个数的和应该越大越好,所以我们将两个vector都从小到大拍好序,取两个的最后一个数相加起来减去模数就是这种情况的最大值,而另外一种情况两个之和小于模数,我们就可以使用二分查找(当然你也可以使用双指针)的方法解决。

关键代码:

	dfs(1,mid,0,k1);
	dfs(mid+1,n,0,k2);//折半
	sort(k1.begin(),k1.end());
	sort(k2.begin(),k2.end());
	int ans=(k1.back()+k2.back())%mod;//>m时的最大情况 
	int cnt1=k1.size(),cnt2=k2.size();
	int l=cnt1-1,r=0;
	for(r=0;r<=cnt2;r++)//双指针维护 
	{
		while(k1[l]+k2[r]>=mod) --l;
		ans=max(ans,k1[l]+k2[r]); 
	}
	cout<<ans<<"\n";

CF585D Lizard Era: Beginning

这题就比较麻烦了,数据范围\(1\le n \le 25\),感觉可以过,但是这道题有三个人,每次选出来两个人,所以每一个任务就有3种组合方案,很显然\(3^25\)就超时了,所以理所当然的考虑折半。

搜索还是暴搜,看我们选择那两个人,最后我们要输出的是派人去做任务的方案,所以对于方案我们可以用数组存(我不确定会不会爆内存),还有一种较为优秀的做法就是使用进制存贮,这里有三个人,所以我们就可以使用三进制数来存,然后每一个人物暴力搜索派哪两个人去做,将每一种可能的答案存下来。

这题挺烦的是合并,一开始我想着可不可以考虑考虑二分或者双指针等一些比较好写的方法,但是没救,因为这题没有啥单调性,它甚至没有给出让三个人得到的值相同的那个值,只是让我们保证求得的值最大。那我们就只能使用map,你可以把map看作一个极大的桶,可以自行调整空间,但是有点费时间。那么这道题就没啥了。

代码:

map<int,pair<int,int> > mp;//使用map将各种方案存起来
pair<int,int> temp;
int n,l[M],m[M],w[M];
int t1,t2,ans;
string ansx[3]={"LM","LW","MW"};

inline void dfs(int x,int y,int flag,int a,int b,int c,int t)
{
	if(x>y)
	{
		int val=((a-b)<<30)+(b-c);
		if(flag==2)
		{
			val=((b-a)<<30)+(c-b);
			if(!mp.count(val)) return;
			temp=mp[val];
			temp.first+=a;
			if(temp.first>ans)
			{
				ans=temp.first;
				t1=temp.second;
				t2=t;
			}
			return;
		}
		if(!mp.count(val)) mp[val]=make_pair(a,t);
		else mp[val] = max(mp[val],make_pair(a,t));
		return;
	}
	dfs(x,y-1,flag,a+l[y],b+m[y],c, t<<2|0);
	dfs(x,y-1,flag,a+l[y],b,c+w[y], t<<2|1);
	dfs(x,y-1,flag,a,b+m[y],c+w[y], t<<2|2);
}

P9234 [蓝桥杯 2023 省 A] 买瓜

这题还是有点毒瘤的,因为虽然给的数据范围是\(1\le n \le 30\),但是与上一道题一样,对于每一个瓜它都有3种状态,全部都要,切一半,不要,求一半的时候代价增加1,最后看最少的代价。但是由于这道题的数据较水,除了折半搜索之外,我们还可以使用疯狂剪枝使暴力深搜莽过去。

给出优化的方法:

1.当前答案都已经大于已经搜索出来的答案,明显这种方案对于答案已经没有任何贡献,直接返回。

2.将a数组从大到小排序(大的明显需要的状态更少)。

3.预处理出前缀和,要是当前已经选了的西瓜重量之和+还没有判断的所有西瓜重量之和还没有题目的要求,那就直接返回,肯定没有解。

然后你就可以水过去了。

关键代码:

inline void dfs(int x,int k,double sum)
{
	if(k>ans||sum>m||sum+(s[n]-s[x])<m) return ;//答案已经超过ans,没有贡献;答案超过要求;加上剩下的所有还达不到要求。
	if(sum==m)
	{
		ans=min(ans,k);
		return ;
	}
	for(int i=x+1;i<=n;i++)
	{
		dfs(i,k,sum+a[i]);
		dfs(i,k+1,sum+a[i]/2);
	}
}

后面给六道练习题(敲不动了,强化一下深搜的各种类型)

P3067 [USACO12OPEN] Balanced Cow Subsets G

这题确定不是上面第一道的双倍经验,只是稍微有一点复杂,每个奶牛的产奶量有一点不同,但是还是很好用二分处理(好像要被卡?那就用哈希/map咯)。

CF285D Permutation Sum

这题的时间其实卡的很死,正解折半合并还是相当的复杂,所以我们可以退而求其次,使用较为低效的合并方法,将答案都在本机上预处理出来,打表做就可以了。

[AGC026C] String Coloring

也是要和字符串哈希结合,这样才可以做到\(O(1)\)时间判断两个字符串是否相等。

P2476 [SCOI2008] 着色方案

这个应该很明显了,就是一个暴力6维状态的记忆化,但是数据范围非常的友善,总方案数也很少,所以算是一道无脑题。

P1790 矩形分割

一道比较巧妙的题,需要转化一下题意,转化以后就比较简单了,想一想一根线怎么样才可以将一个矩形分割成两部分,并且两部分都要有露在外面,不可以包含(数据较弱,所以支持各种乱搞)。

P4537 [CQOI2007] 矩形

上面一道题的双倍经验,不说了。

1、bfs进阶——双向搜索&01BFS

(1)双向搜索

前一节已经说过了,在已知起点和终点的情况下,我们可以将起点和终点都放入队列中,在BFS的过程中观察什么时候从起点出发的状态与从终点出发的状态相遇,此时就是一个合法的解,基本上双向搜索可以优化很多的时间,因为BFS一层一层向下遍历的时候,节点数也会以指数级别的速度增长,而使用双向搜索,基本上是可以让搜索树的深度/2。

P1379 八数码难题

双向搜索的板子题,题目中已经给出了起点状态和终点状态,按照题目的输入,可以联想到对于任意的一张\(3*3\)的图我们可以使用一个9位数来表示,这样我们就将空间优化了下来,同时地图入队出队处理起来就会方便许多。

双向搜索的过程就是,将起点和终点两个状态都入队,然后标上不同的颜色,在每一次扩展结点的时候,将扩展的节点表上当前所在点的颜色,例如我们将起点染成\(1\),终点染成\(2\),那么从起点扩散出去的点就都是\(1\),从终点扩散出去的点就都是\(2\),在每一次扩展节点时,判断扩展结点是否被染过色,且当前点与扩展点上的颜色不同,那么我们的双向搜索就找到公共点了,这时将两边搜索到的答案进行合并,最后输出答案就行了。

对于这道题,由于我们将每一张图都转化成了一个九位数来表示,所以vis和ans数组就有点不好开,所以就直接使用map来存,这样就可以不用担心空间爆炸的问题了。每一次的交换明显都是数字为\(0\)的(代表这个格子上没有任何数)与其四周交换。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5;
int n,goal=123804765;//初始&目标状态
int mapp[M][M];
int fx[M]={1,-1,0,0};
int fy[M]={0,0,1,-1};
map<int,int> ans,vis;//使用map来存答案和是否入队&颜色

inline void bfs()
{
	queue<int> q;
	vis[n]=1,vis[goal]=2;
	ans[n]=0,ans[goal]=1;//将起点和终点都入队,标记上不同的颜色,此时由于起点状态和终点状态肯定不一样,我们就直接将终点的答案数组赋为1
	q.push(n),q.push(goal);
	int kx,ky,t;
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		t=x;
		for(int i=3;i;i--)
		{
			for(int j=3;j;j--)
			{
				mapp[i][j]=t%10;//将九位数转化成矩阵
				t/=10;
				if(!mapp[i][j]) kx=i,ky=j;//找到0所在的位置。
			}
		}
		for(int i=0;i<4;i++)
		{
			int sx=kx+fx[i],sy=ky+fy[i];
			if(sx<1||sx>3||sy<1||sy>3) continue;
			swap(mapp[kx][ky],mapp[sx][sy]);
			t=0;
			for(int i=1;i<4;i++)
			{
				for(int j=1;j<4;j++)
				{
					t=t*10+mapp[i][j];//将变化后的矩阵又表示成九位数
				}
			}
			if(vis[t]==vis[x])//已经遍历过的 和自己颜色一样 所以肯定已经入队了,直接continue
			{
				swap(mapp[sx][sy],mapp[kx][ky]);//恢复原状 !!!(每一次操作之后都要恢复原状,因为以这个矩阵为基础还有其他的变化方法)
				continue;
			}
			if(vis[x]+vis[t]==3)//起点状态与终点状态相遇了
			{
				cout<<ans[x]+ans[t]<<"\n";//直接输出前半段的答案+后半段的答案。
				return ;
			}
			vis[t]=vis[x];//新状态,向下广搜 
			ans[t]=ans[x]+1;
			q.push(t);//入队
			swap(mapp[sx][sy],mapp[kx][ky]);//恢复原状
		}
	}
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	if(n==goal)
	{
		cout<<"0\n";
		return 0;
	}
	bfs();
	return 0;
}

(2)01BFS

借助STL中的deque,将边权为\(0\)的插入队列的前方,将边权为1的插入到队列的后方,由于双端队列的性质,我们每一次都是取得队列前面存的状态,可以贪心的保证我们拿来更新每一个点值的方法一定是最优的,而入队和出队都是\(O(1)\)的,每一个点就进队出队一次,所以总共的时间复杂度近似于\(O(n)\)但是可能有一点常数(但是都\(O(n)\)了,谔谔)。

P4554 小明的游戏

01BFS的板子题,移动到相同的格子的费用就是0,不同的费用就是1,只有这两种边权。唯一需要一点注意的就只有对于多组询问记得清空一些东西。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=505;
int n,m;
int bx,by,ex,ey;
int mapp[M][M],vis[M][M],dis[M][M];
int fx[5]={0,0,0,1,-1};
int fy[5]={0,1,-1,0,0};

inline void bfs()
{
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	deque<pair<int,int> > q;
	q.push_front(make_pair(bx,by));
	dis[bx][by]=0;//起点入队并清空
	while(!q.empty())
	{
		int x=q.front().first,y=q.front().second; 
		q.pop_front();//每次贪心的取前面
		if(vis[x][y]) continue;
		vis[x][y]=1;
		for(int i=1;i<=4;i++)
		{
			int sx=x+fx[i],sy=y+fy[i];
			if(sx<1||sx>n||sy<1||sy>m) continue;
			int check=mapp[sx][sy]!=mapp[x][y];//一样边权就是0,否则为1
			if(dis[sx][sy]>dis[x][y]+check)
			{
				dis[sx][sy]=dis[x][y]+check;
				if(check)//边权是0是1
				{
					q.push_back(make_pair(sx,sy));//边权为1,放入队尾
				}
				else
				{
					q.push_front(make_pair(sx,sy));//边权为0,放入队首
				}
			}
		}
	}
}

signed main()
{
	//ios::sync_with_stdio(false);
	//cin.tie(0);cout.tie(0);
	while(1)
	{
		cin>>n>>m;
		if(n+m==0) break;
		char opt;
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=m;j++)
			{
				cin>>opt;
				if(opt=='#') mapp[i][j]=1;
				else mapp[i][j]=0;//预处理一下mapp数组
			}
		}
		cin>>bx>>by>>ex>>ey;
		bx++,by++,ex++,ey++;//不是很喜欢处理下标0,那就全部++
		bfs();
		cout<<dis[ex][ey]<<"\n";
	}
	return 0;
}

SP22393 KATHTHI - KATHTHI

和上面一道题一摸一样,只不过输入的字符变了。

代码参照上题。

posted @ 2024-01-16 21:14  keep_of_silence  阅读(86)  评论(0编辑  收藏  举报
/*
*/