前言:
由于搜索的题还是做的太少了,所以以后有可能会不定期更新。
四、还是进阶的dfs/bfs
相关题单:戳我
1、dfs
(1)meet in the middle
P2962 [USACO09NOV] Lights G
颠覆了我对折半搜索的认知,果然,只要满足了折半搜索的几个性质,基本上都可以使用折半搜索来处理。
首先我们拿到的是一张无向图,其中点与边的数据范围是\(1\le n\le35,1\le m\le595, 1\le a,b\le n\),一开始个点的状态都是0,我们可以操作任意一个点,使得这个点与和它相连的所有点的状态变化,问我们最少需要多少次来使所有点的状态变为1。数据范围是超过了暴力深搜的处理能力,而使用bfs也不好处理。那我们就考虑折半搜索。
对于这道题,唯一可以这般搜索的就只有各个点了,那么我们将点分为两组,分别搜索这一组内各个点任意操作所能使无向图变成的状态,很明显每一个点都只有操作与不操作两种情况,因为如果要操作两次,那就相当于没有操作,所以搜索时就暴力枚举给个点的使用情况,同时将与其相连的点使用二进制状态压缩记录每个点的01情况。
那么我们就得到了操作不同的点所可以使图变成的状态,接着就是考虑合并,合并这道题如果你已经想到了使用二进制状态压缩,那么就比较简单了,同样固定一个数组,将另外一个数组按照二进制的状态进行排序(以状态为第一关键字,操作数为第二关键字),
然后一一枚举固定的数组,使用目标状态(也就是所有的点都为1的状态)与当前枚举到的状态异或,这样就得到我们需要查找的状态,然后直接在已经排好序的数组中二分查找第一个出现的状态(因为第二关键字是操作次数),判断是否可以更新答案。
特别坑爹的是这道题由于点数超过了31,所以二进制存的时候所有的位运算要使用\((1ll<<x)\)
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<vector>
#define int long long
#define inf 1e18
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=36;
int n,m;
int f[5000005];
vector<int> v[41];
vector<pair<int,int> > k1,k2;//前面状态,后面存操作状态(操作了哪些灯,我为啥要用二进制压缩谔谔)
//二进制维护开关灯的情况
//当前,左边界,开关情况,使用情况,vector记录
inline void dfs(int l,int r,int sum,int res,vector<pair<int,int> > &op)
{
if(l>r)
{
op.push_back(make_pair(sum,res));//存答案
return ;
}
int t=sum;
t^=(1ll<<(l-1));//改变自己的状态
for(int i=0;i<v[l].size();i++)
{
t^=(1ll<<(v[l][i]-1));
}//改变相连的状态
dfs(l+1,r,t,res|(1ll<<(l-1ll)),op);//使用这盏灯
dfs(l+1,r,sum,res,op);//不使用这盏灯
}
signed main()
{
//ios::sync_with_stdio(false);
//cin.tie(0);cout.tie(0);
cin>>n>>m;
int a,b;
for(int i=1ll;i<=m;i++)
{
cin>>a>>b;
v[a].push_back(b);
v[b].push_back(a);
}
//只修改前一半的灯泡
int mid=(n>>1ll)+1ll;
dfs(1,mid,0,0,k1);
dfs(mid+1,n,0,0,k2);//后一半
for(int i=0;i<k1.size();i++)
{
int res=k1[i].second,num=0;
while(res)
{
if(res%2) num++;
res/=2;
}
k1[i].second=num;//将操作状态改为操作数(有点多此一举)
}
for(int i=0;i<k2.size();i++)
{
int res=k2[i].second,num=0;
//cout<<k2[i].second<<" ";
while(res)
{
if(res%2) num++;
res/=2;
}
k2[i].second=num;//同上
}
int ans=(1ll<<n)-1,minn=inf;
sort(k1.begin(),k1.end());//vector内pair排序
int len=k1.size();
for(int i=0;i<len;i++) f[i]=k1[i].first;
for(int i=0;i<len;i++)
{
if(k1[i].first==ans)
{
minn=min(minn,k1[i].second);
}
}
for(int i=0;i<k2.size();i++)
{
if(k2[i].first==ans)
{
minn=min(minn,k2[i].second);
}
}//两种特殊情况,即只用前一半或后一半就可以达到目标状态(也有点多此一举)
for(int i=0;i<k2.size();i++)
{
int posl=lower_bound(f,f+len,ans^k2[i].first)-f;//找到第一个符合的,此时其所需要的操作数一定最小
if((f[posl]^k2[i].first)==ans)
{
minn=min(minn,k2[i].second+k1[posl].second);//更新答案
}
}
cout<<minn<<"\n";
return 0;
}
//操作的先后顺序好像没有太大的关系
P8187 [USACO22FEB] Robot Instructions S
\(1\le n\le40\)的数据范围,有\(n\)条指令,问你在使用\(k\)条指令的限制下,有多少种方案可以到达给定的终点,时间开了4s。
是一道非常友善的题(可是我自己没有想出正解),先是折半,按照上一道题给我们的思路,还是考虑对于每一个操作是否使用,最后将操作次数与到达的状态存入一个vector中。
但是如何去将两个vector向合并,这里并不给出正解,而是给出一种暴力的做法引导一下思路。显然的,我们已知了各个操作状态下的操作数与到达的位置,那么还是固定其中一个数组,将另外一个数组按照x坐标,y坐标,操作数分别为第一二三关键字进行排序,然后暴力的一一枚举固定了的数组,按照终点的x值与当前数组的x值二分查找出一个区间,然后这个区间的x已经固定,然后我们再在这个确定的区间内二分查找所需的y,最后暴力统计答案,可以获得84pts的好成绩!
至于正解,是会用到哈希或者map记录一下各个状态下的情况,搜索的速度会快得多。
84pts的代码:
#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=45;
int n,fx,fy;
int x[M],y[M],ans[M];
int cx[2000005],cy[2000005];
struct N{
int x,y,sum;
};
vector<N> k1,k2;
//使用一个玩意记录一下最后的坐标+操作次数
inline bool cmp(N a,N b)
{
if(a.x!=b.x) return a.x<b.x;
if(a.y!=b.y) return a.y<b.y;
return a.sum<b.sum;
}
inline void dfs(int l,int r,int xx,int yy,int last,vector<N> &op)
{
if(l>r)
{
op.push_back((N){xx,yy,last});
return ;
}
dfs(l+1,r,xx+x[l],yy+y[l],last+1,op);
dfs(l+1,r,xx,yy,last,op);
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
cin>>fx>>fy;
for(int i=1;i<=n;++i) cin>>x[i]>>y[i];
int mid=(n>>1);
dfs(1,mid,0,0,0,k1);
dfs(mid+1,n,0,0,0,k2);
sort(k1.begin(),k1.end(),cmp);
int len=k1.size();
for(int i=0;i<len;++i) cx[i]=k1[i].x,cy[i]=k1[i].y;
for(int i=0;i<k2.size();++i)
{
int posl=lower_bound(cx,cx+len,fx-k2[i].x)-cx;
int posr=upper_bound(cx,cx+len,fx-k2[i].x)-cx;//首先可以肯定地是这些x一定是相同的
if(posl==posr) continue;
int posyl=lower_bound(cy+posl,cy+posr,fy-k2[i].y)-cy;
int posyr=upper_bound(cy+posl,cy+posr,fy-k2[i].y)-cy;//找符合的y区间
for(int j=posyl;j<posyr;j++)
{
if(k2[i].y+cy[j]==fy)
{
ans[k2[i].sum+k1[j].sum]++;
}
}
}
for(int i=1;i<=n;++i) cout<<ans[i]<<"\n";
return 0;
}
[ABC300G] P-smooth number
这题其实也是用meet in the middle 也可以做,但是由于数据较为玄学,我是使用记忆化搜索莽过去的,读者自己思考哒。
(2)剪枝
剪枝的思路其实都比较简单,但是就看你有没有想到了,所以对于卡常一类的剪枝搜索题,还是要多练练,见识一下各种神奇的剪枝技巧,万一某一天你就撞上一道需要特殊剪枝技巧的题呢。
P1120 小木棍
终于讲到这一道题了,毒瘤剪枝搜索题的开端,一开始是几段长度一样的木棍,但是都分成了几个小段,给出每小段段的长度,问原始木棍的最小可能长度。
首先数据就不太友好\(1\le n\le 65\),单单是判断某一条小木棍选不选都会时间爆炸,而这道题的搜索状态也比较麻烦,不好进行什么特殊的处理,那就只能以最简单的方法进行模拟,同时加上很多的剪枝优化。
首先小木棍的数量和长度都都是已知的,那么我们可以知道所有小木棍的长度之和,于是对于整除不了长度和的长度,明显最后是不可以拼成几段长度一摸一样的木棍,接着,由于我们要使搜索的时间尽可能地少,所以我们可以按照长度将小木棍从大到小的排序,这样我们可以尽早的得知此方案是否合法,还有一开始木棍的长度应该从最大的小木棍开始判断。
接着在深搜的时候注意一下细节,对于某种情况下,所有方法都不可能拼接成一段木棍时,及时返回,避免搜索次数过多。
基本上思路还是比较简单的,代码:
#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=105;
int n,sum,maxx,len,cnt;
int a[M],v[M];
//正在拼u+1根,这一根已经拼了x这么长,上一个用的小木棍是last
inline int dfs(int u,int x,int last)
{
if(u>cnt) return 1;//所有的都拼完了
if(x==len) return dfs(u+1,0,1);//这一条拼完了
int fail=0;
for(int i=last;i<=n;i++)
{
//cout<<v[i]<<"\n";
if(!v[i]&&x+a[i]<=len&&fail!=a[i])//没用过+不超过长度+这根没失败
{
v[i]=1;//标记为用了
//cout<<len<<"\n";
if(dfs(u,x+a[i],i+1)) return 1;//如果有深搜的情况成功了
fail=a[i];//没有返回1就说明失败了
v[i]=0;//撤销操作
if(x==0||x+a[i]==len) return 0;//这一种已经是唯一可以成功的情况了,失败了就不可能了
}
}
return 0;//没有返回1那就返回0;
}
inline bool cmp(int a,int b){return a>b;}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i],sum+=a[i],maxx=max(maxx,a[i]);//记录最大值与和
sort(a+1,a+n+1,cmp);//从大到小排序
for(len=maxx;len<=sum;len++)
{
if(sum%len) continue;//没法整分
cnt=sum/len;//分几段
memset(v,0,sizeof(v));//清空数组
if(dfs(1,0,1)) break;
}
cout<<len<<"\n";
return 0;
}
P1380 T型骨牌
思路也比较巧妙
首先一个T形至少要占\(3*3\)的格子,所以一开始特判一下,如果给定的\(n,m\)有小于3的,那么答案肯定就是0了。借助这个性质,我们就可以将这道题的剪枝思路想出来了——我们只考虑T形第一行中间的那个点的放置位置,明显我们就可以将\(n,m\)都缩小2,这样朴素的深搜就可以完成了。
只不过深搜里面还是比较复杂的,注意模拟两个块之间是不能重叠的。
代码:
#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=11;
int n,m,ans=0;
int mapp[M][M],dp[M][M];
int fx[5]={0,1,-1,0,0};
int fy[5]={0,0,0,1,-1};
inline void dfs(int x,int y,int sum)//dfs里直接模拟就可以了,将每一个占了的点标记一下,记得撤销。
{
if(x==n)
{
ans=max(ans,sum);
return ;
}
if(!mapp[x-1][y-1]&&!mapp[x-1][y]&&!mapp[x-1][y+1]&&!mapp[x+1][y]&&!mapp[x][y])
{
mapp[x-1][y-1]=mapp[x-1][y]=mapp[x-1][y+1]=mapp[x+1][y]=mapp[x][y]=1;
if(sum+1>=dp[x][y])
{
dp[x][y]=max(dp[x][y],sum);
if(y==m-1) dfs(x+1,2,sum+1);
else dfs(x,y+1,sum+1);
}
mapp[x-1][y-1]=mapp[x-1][y]=mapp[x-1][y+1]=mapp[x+1][y]=mapp[x][y]=0;
}
if(!mapp[x+1][y-1]&&!mapp[x+1][y]&&!mapp[x+1][y+1]&&!mapp[x-1][y]&&!mapp[x][y])
{
mapp[x+1][y-1]=mapp[x+1][y]=mapp[x+1][y+1]=mapp[x-1][y]=mapp[x][y]=1;
if(sum+1>=dp[x][y])
{
dp[x][y]=max(dp[x][y],sum);
if(y==m-1) dfs(x+1,2,sum+1);
else dfs(x,y+1,sum+1);
}
mapp[x+1][y-1]=mapp[x+1][y]=mapp[x+1][y+1]=mapp[x-1][y]=mapp[x][y]=0;
}
if(!mapp[x-1][y-1]&&!mapp[x][y-1]&&!mapp[x][y+1]&&!mapp[x+1][y-1]&&!mapp[x][y])
{
mapp[x-1][y-1]=mapp[x][y-1]=mapp[x][y+1]=mapp[x+1][y-1]=mapp[x][y]=1;
if(sum+1>=dp[x][y])
{
dp[x][y]=max(dp[x][y],sum);
if(y==m-1) dfs(x+1,2,sum+1);
else dfs(x,y+1,sum+1);
}
mapp[x-1][y-1]=mapp[x][y-1]=mapp[x][y+1]=mapp[x+1][y-1]=mapp[x][y]=0;
}
if(!mapp[x-1][y+1]&&!mapp[x][y-1]&&!mapp[x][y+1]&&!mapp[x+1][y+1]&&!mapp[x][y])
{
mapp[x-1][y+1]=mapp[x][y-1]=mapp[x][y+1]=mapp[x+1][y+1]=mapp[x][y]=1;
if(sum+1>=dp[x][y])
{
if(y==m-1) dfs(x+1,2,sum+1);
else dfs(x,y+1,sum+1);
dp[x][y]=max(dp[x][y],sum);
}
mapp[x-1][y+1]=mapp[x][y-1]=mapp[x][y+1]=mapp[x+1][y+1]=mapp[x][y]=0;
}
if(y==m-1) dfs(x+1,2,sum);
else dfs(x,y+1,sum);
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
if(n<3||m<3)
{
cout<<0<<"\n";return 0;
}
if(n==3&&m==3)
{
cout<<1<<"\n";return 0;
}//特判一下
dfs(2,2,0);//缩小了搜索的地图
cout<<ans<<"\n";
return 0;
}
P1985 [USACO07OPEN] 翻转棋 Fliptile S
这道题不是剪枝,但是它的贪心性质甚至比剪枝强多了。
首先感觉和前面的双向搜索长得非常相像,数据范围\(1 \leq M,N \leq 15\)也是在双向搜索的处理范围之内的。我觉得应该是可以做的(我没有写过谔谔,但是可能会很不好处理)。
但是这道题有一个很特殊的性质,你可以发现,你的目的肯定是让白色面朝上的数量越变越多,那么为了控制全部都是白色的,而不让一些操作打乱这个状态,应该是要按某一种顺序一一判断是否进行操作。那么观察到每一列的下一列肯定可以将这一列全部变成白色的,只需要判断当前位置是否是白色,如果是那么就操作下一列的这个位置。
而且对于某一个点,和上面的题一样,明显最多只有两种可能,要么操作要么不操作,因为在同一个点操作两次那还不如不操作。
按照前面的两个已知思路,正解就已经很清晰了。我们的答案是按照第一列的颜色来决定的,只要确定了第一行的颜色,那么往下的操作的是递推的,所以我们可以对第一列的所有操作方案进行深搜,模拟出第一列的颜色排序,然后计算出整个序列所需要的操作数,进行更新(题目要求输出操作最少且字典序最小的操作方案),最后无解的情况也比较好判断,如果最后一排出现了黑色的点,那么因为没有其他的点可以改变最后一个排的点颜色,于是就可以判定为无解,直接退出就好。
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
#define inf 1e18
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,m;
int mapp[M][M],c[M][M],f[M];
int sum,ans[M][M],a[M][M];
int fx[5]={0,1,-1,0,0};
int fy[5]={0,0,0,1,-1};
inline void update(int x,int y)//操作函数
{
c[x][y]^=1;//自己也要改变
for(int i=1;i<=4;i++)
{
int sx=x+fx[i],sy=y+fy[i];
c[sx][sy]^=1;
}
}
inline void check()
{
memset(a,0,sizeof(a));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
c[i][j]=mapp[i][j];//操作数组
}
}
for(int i=1;i<=m;i++)
{
if(f[i])
{
a[1][i]=1;
update(1,i);//按照f数组模拟第一排的情况
}
}
for(int i=2;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(c[i-1][j])//自己头顶上是黑色,那么自己就要进行操作
{
update(i,j);
a[i][j]=1;
}
}
}
for(int i=1;i<=m;i++)
{
if(c[n][i]) return ;//无解,最后一排谁都救不了
}
int res=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
res+=a[i][j];//统计操作数
}
}
if(res<sum)//如果可以更新
{
sum=res;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
ans[i][j]=a[i][j];
}
}
}
else if(res==sum)//观察字典序是否可以更新
{
int flag=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(a[i][j]!=ans[i][j])
{
if(a[i][j]<ans[i][j]) flag=1;
else flag=2;
break;
}
}
if(flag) break;
}
if(flag==1)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
ans[i][j]=a[i][j];
}
}
}
}
}
inline void dfs(int u)
{
if(u>m)
{
check();
return ;
}
f[u]=1;//f数组记录的是第一排第u个是否操作
dfs(u+1);
f[u]=0;
dfs(u+1);
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
memset(ans,0x3f,sizeof(ans));
sum=inf;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>mapp[i][j];
}
}
dfs(1);//搜索第一排的操作方案
if(sum==inf)//没有被更新那就是没有解
{
cout<<"IMPOSSIBLE\n";
return 0;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cout<<ans[i][j]<<" ";
}
cout<<"\n";
}
return 0;
}
/*
4 4
1 0 1 0
0 0 0 1
0 0 0 0
1 1 0 1
*/
P1514 [NOIP2010 提高组] 引水入城
深搜与其他很多思想都可以结合。
其实是一道DP,但是有深搜+贪心也可以过掉,大致的思路是搜索出第一排每一个点建水库可以影响到最后一排的哪段区间,然后就转化成为了一道最少区间覆盖问题,可以使用双指针维护,就当贪心+dfs的板子题了。
2、bfs
(1)大模拟BFS
对于大模拟类型的BFS,最重要的不是能力,而是耐心,不要调了一两个小时就润啦,一般来是这种大模拟的思路还是相当简单的,只不过有许多的细节需要一一的调试。
P3956 [NOIP2017 普及组] 棋盘
算是一道操作较多的BFS了,但还只是四联通的地图,所以很多东西维护起来还是相当的方便,主要就是这一道题每一个格子上的颜色需要进行一些判断和处理。
细节标在代码注释中:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
#define inf 1e9
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=105;
int m,n;
int mapp[M][M],vis[M][M][3],d[M][M][3];//vis dis数组有三维的原因是可能我们染色的方案不同,不好判断哪一种染色的方案会好一点,那就全部存进来
int fx[5]={0,1,-1,0,0};
int fy[5]={0,0,0,1,-1};
queue<pair<pair<int,int>,int>> q;//有点极端了,我应该写一个结构体的,分别代表了x,y+当前节点的颜色(操作后的颜色)
inline void bfs()
{
q.push(make_pair(make_pair(1,1),mapp[1][1]));
vis[1][1][mapp[1][1]]=1;
d[1][1][mapp[1][1]]=0;//起点入队
while(!q.empty())
{
int x=q.front().first.first,y=q.front().first.second;
int col=q.front().second;
q.pop();
vis[x][y][col]=0;
//cout<<x<<" "<<y<<" "<<col<<"\n";
for(int i=1;i<=4;i++)
{
int sx=x+fx[i],sy=y+fy[i];
if(sx<1||sx>m||sy<1||sy>m) continue;
if(!mapp[sx][sy])//去的地方没有颜色的话
{
if(!mapp[x][y]) continue;//自己本身也没有颜色,那就无路可走了,因为魔法只能连续使用1次
else
{
if(mapp[x][y]==1)//当前颜色为1
{
if(d[sx][sy][2]>d[x][y][1]+3)//将无色的格子染成和自己不同的颜色2,这样总共的代价就是2+1=3
{
d[sx][sy][2]=d[x][y][1]+3;
if(!vis[sx][sy][2])
{
vis[sx][sy][2]=1;
q.push(make_pair(make_pair(sx,sy),2));
}
}
if(d[sx][sy][1]>d[x][y][1]+2)//将无色的格子染成和自己同一个颜色1,代价就只有2
{
d[sx][sy][1]=d[x][y][1]+2;
if(!vis[sx][sy][1])
{
vis[sx][sy][1]=1;
q.push(make_pair(make_pair(sx,sy),1));
}
}
}
else//当前颜色为2 同理可得
{
if(d[sx][sy][1]>d[x][y][2]+3)//染成颜色1
{
d[sx][sy][1]=d[x][y][2]+3;
if(!vis[sx][sy][1])
{
vis[sx][sy][1]=1;
q.push(make_pair(make_pair(sx,sy),1));
}
}
if(d[sx][sy][2]>d[x][y][2]+2)//染成颜色2
{
d[sx][sy][2]=d[x][y][2]+2;
if(!vis[sx][sy][2])
{
vis[sx][sy][2]=1;
q.push(make_pair(make_pair(sx,sy),2));
}
}
}
}
}//按照题目中给定的要求模拟就是了
else//去的地方有颜色
{
if(mapp[sx][sy]==col)//颜色相同
{
if(d[sx][sy][col]>d[x][y][col])
{
d[sx][sy][col]=d[x][y][col];
if(!vis[sx][sy][col])
{
vis[sx][sy][col]=1;
q.push(make_pair(make_pair(sx,sy),col));
}
}
}
else//颜色不同 u->v u是col
{
if(d[sx][sy][3-col]>d[x][y][col]+1)
{
d[sx][sy][3-col]=d[x][y][col]+1;
if(!vis[sx][sy][3-col])
{
vis[sx][sy][3-col]=1;
q.push(make_pair(make_pair(sx,sy),3-col));
}
}
}
}
}
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
memset(d,0x3f,sizeof(d));
cin>>m>>n;
int x,y,c;
for(int i=1;i<=n;i++)
{
cin>>x>>y>>c;
if(c==0) mapp[x][y]=1;
else mapp[x][y]=2;
}
bfs();
int ans=min(d[m][m][1],d[m][m][2]);//最后在两种情况中取min
if(ans>inf) cout<<"-1\n";//要是两种情况都没有被更新,那就是没有解
else cout<<ans<<"\n";
return 0;
}
P6474 [NOI Online #2 入门组] 荆轲刺秦王
荆轲刺秦王的故事真的要为万人传唱了。
朴素的题目背景+极长的题目描述,以及奇奇怪怪的操作,很显然这就是一道大模拟BFS。
地图上标出给出卫兵,起点与终点,给出卫兵的视线范围,荆轲不能出现在卫兵的视线范围之内,荆轲还有两个技能,都有次数的限制。隐身之后卫兵观察不到荆轲,但是荆轲不能通过卫兵所在的点;瞬移之后,可以到达上下左右一个方向的d距离开外。求出荆轲是否可以刺秦王,并按时间,隐身操作次数,瞬移操作次数为一二三关键字进行排序,输出最优秀的答案。
那么因为每一次的BFS转移的是荆轲的状态,所以面对如此多的变量,我们需要将时间,坐标,两种操作次数放入一个结构体中维护。然后正常的模拟操作,将可能的情况全部入队就可以了。
但是在跑图之前还有一个问题,就是卫兵的视线范围,数据范围\(2\le n\), \(m\le 350\),\(1\le a_{i,j}\le 350\)。很明显如果直接模拟的话,最高的时间复杂度明显是没法接受的,那对与区间加的操作,一个简单的处理方法就是差分(尤其注意你的差分的边界,因为很容易写错),将每一个卫兵对地图进行差分,然后在做一遍前缀和,然后我们就可以知道哪里在卫兵的视线范围之内了,地图就预处理好了。
然后按照题意模拟就好哒,注意一下隐身的时候是不可以经过卫兵的,还要两种操作是由次数限制的。
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
#define inf 1000000000000000
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=355;
int n,m,c1,c2,d;
int ex,ey,bx,by;
int mapp[M][M];
struct N{
int x,y;//坐标
int c1,c2;//两个操作
int s;
};queue<N> q;
inline N minn(N a,N b)
{
if(a.s!=b.s)return a.s<b.s?a:b;
if(a.c1+a.c2!=b.c1+b.c2)return a.c1+a.c2<b.c1+b.c2?a:b;
return a.c1<b.c1?a:b;
}//按照一二三关键字更新答案
bool vis[M][M][21][21],look[M][M];
int c[M][M];
int fx[9]={0,1,-1,0,0,1,1,-1,-1};
int fy[9]={0,0,0,1,-1,1,-1,1,-1};
inline void pre(int x,int y,int k)//预处理看到的范围
{
for(int i=0;i<=k;i++)
{
c[max(x-i,1)][max(y-(k-i),1)]++;
c[max(x-i,1)][min(y+(k-i),m)+1]--;
c[min(x+i,n)][max(y-(k-i),1)]++;
c[min(x+i,n)][min(y+(k-i),m)+1]--;//注意边界,不能小于0也不能大于n或m
}
}//差分
N ans=(N){0,0,inf,inf,inf};
inline void bfs()
{
while(!q.empty())
{
N x=q.front();
q.pop();
if(x.s>ans.s) continue;
if(x.x==ex&&x.y==ey)
{
ans=minn(ans,x);continue;//在终点了,判断是否可以更新答案
}
for(int i=1;i<=8;i++)
{
int sx=x.x+fx[i],sy=x.y+fy[i];
if(sx<1||sx>n||sy<1||sy>m||mapp[sx][sy]>0)continue;
if(look[sx][sy])//可以被看见
{
if(vis[sx][sy][x.c1+1][x.c2]||x.c1+1>c1) continue;//如果这里有卫兵或隐身次数超过限制,那就只能返回了
vis[sx][sy][x.c1+1][x.c2]=1;
q.push((N){sx,sy,x.c1+1,x.c2,x.s+1});
}
else//正常的走
{
if(vis[sx][sy][x.c1][x.c2]) continue;
vis[sx][sy][x.c1][x.c2]=1;
q.push((N){sx,sy,x.c1,x.c2,x.s+1});
}
}
if(x.c2+1>c2) continue;//瞬移次数是否超过限制
for(int i=1;i<=4;i++)
{
int sx=x.x+fx[i]*d,sy=x.y+fy[i]*d;
if(sx<1||sx>n||sy<1||sy>m||mapp[sx][sy]>0) continue;
if(look[sx][sy])//可以被看见 那么都还要隐身
{
if(vis[sx][sy][x.c1+1][x.c2+1]||x.c1>c1) continue;//同上
vis[sx][sy][x.c1+1][x.c2+1]=1;
q.push((N){sx,sy,x.c1+1,x.c2+1,x.s+1});//两种操作都要用
}
else//可以放心的瞬移
{
if(vis[sx][sy][x.c1][x.c2+1]) continue;
vis[sx][sy][x.c1][x.c2+1]=1;
q.push((N){sx,sy,x.c1,x.c2+1,x.s+1});
}
}
}
}
string s;
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
//freopen("P6474_16.in","r",stdin);
//freopen("xxx.out","w",stdout);
cin>>n>>m>>c1>>c2>>d;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>s;
if(s[0]=='S')
{
bx=i,by=j,mapp[i][j]=-1;
q.push((N){bx,by,0,0,0});
vis[i][j][0][0]=1;
}
else if(s[0]=='T')
{
ex=i,ey=j;
mapp[i][j]=-2;
}//记录起点终点
else if(s[0]!='.')
{
int x=0;
for(int k=0;k<s.size();k++)
{
x=(x<<1)+(x<<3)+(s[k]^'0');
}
mapp[i][j]=x;
pre(i,j,x-1);
}//这个点是卫兵,进行差分处理
}
}
for(int i=1;i<=n;i++)
{
int sum=0;
for(int j=1;j<=m;j++)
{
sum+=c[i][j];//前缀和
if(sum>0)
{
look[i][j]=1;//如果大于1,那么这个位置卫兵是监视的到的
}
}
}
bfs();
if(ans.s==inf) cout<<"-1\n";//没有更新就是没有解
else
{
cout<<ans.s<<" "<<ans.c1<<" "<<ans.c2<<"\n";//输出最优方案
}
return 0;
}
(2)双向搜索
还是和上一节一样的。重点是在于题目是否可以运用双向搜索,以及答案是否可以进行合并。
P4289 [HAOI2008] 移动玩具
其实和上一节的8数码问题还是比较一样的,只不过地图变成\(4*4\)的了,然后起点和终点状态都是给出的,并且每一个点都只有1和0两种状态。思考和上一节8数码问题一样的状态处理方法,8数码是十进制所以需要一个9位数,而本题只用两种状态,所以使用二进制的状态压缩就绰绰有余了。
首先先将状态压缩成2进制存起来,方便queue的存储,将终点和起点入队并染上不同的颜色,每一次都可以在地图值为1处进行操作,将1放置周围为值为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 s,goal;
int mapp[5][5];
int vis[100005],ans[100005];
int fx[5]={1,-1,0,0};
int fy[5]={0,0,1,-1};
inline void bfs()
{
queue<int> q;
q.push(s),q.push(goal);
vis[s]=1,vis[goal]=2;
ans[s]=0,ans[goal]=0;//起点终点都入队,并染上不同的颜色
int kx,ky,t;
while(!q.empty())
{
int x=q.front();q.pop();
t=x;
for(int i=4;i;i--)
{
for(int j=4;j;j--)
{
mapp[i][j]=t%2;//重新恢复成地图
t/=2;
}
}
for(int i=1;i<=4;i++)
{
for(int j=1;j<=4;j++)
{
if(mapp[i][j])//这个地方有玩具
{
for(int k=0;k<4;k++)
{
int sx=i+fx[k],sy=j+fy[k];
if(sx<1||sx>4||sy<1||sy>4||mapp[sx][sy]) continue;//不能超出边界以及目标点不能有玩具
swap(mapp[sx][sy],mapp[i][j]);
t=0;
for(int xx=1;xx<=4;xx++)
{
for(int yy=1;yy<=4;yy++) t=t*2+mapp[xx][yy];
}
swap(mapp[sx][sy],mapp[i][j]);//注意你需要将操作撤回,方便下一个的操作
if(vis[x]+vis[t]==3)//相遇了
{
cout<<ans[x]+ans[t]+1<<"\n";//两个答案相加,由于x状态到达t状态还需要1的代价,所以要加1
return ;
}
if(vis[t]) continue;
vis[t]=vis[x];
ans[t]=ans[x]+1;
q.push(t);//更新答案,入队
}
}
}
}
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
char opt;
for(int i=1;i<=4;i++)
{
for(int j=1;j<=4;j++)
{
cin>>opt;
s=s*2+(opt=='1');//二进制的状态压缩
}
}
for(int i=1;i<=4;i++)
{
for(int j=1;j<=4;j++)
{
cin>>opt;
goal=goal*2+(opt=='1');//同理
}
}
if(s==goal)
{
cout<<"0\n";return 0;//特判一下是否相同
}
bfs();
return 0;
}
(2)01BFS
同样也是模板题,只不过比前面的例题稍稍麻烦了一点,但是知道了01BFS的本质与操作的方法,其实挺简单的。
CF590C Three States
小清醒01BFS题,总共也就只有三个国家,问你需要将多少荒地变成路可以使各个国家联通。
由于到达荒地的代价是1,数字与数字(国家)之间移动的代价是0,符合01BFS的要求。而完成题目中的限制明显就只有2种可能,要么就是三个国家互相连,要么就是三个国家同时达到一个点,这两种情况都可以保证三个国家是联通了的,所以我们可以求出每一个国家到达其他点的最小代价,这样将两种情况的最小值进行比较我们就可以知道总的最小代价了。(其中要注意的是对于第二种情况如果选择的点是皇荒地,那么答案要减2,因为每个国家都会在那个点修路,总共就加了3,多加了2,那就减回去)。
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
#define inf 1e18
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=1005;
int n,m;
char mapp[M][M];
int dis[M][M][4];//各个国家到达其他点的最小代价
int fx[5]={0,1,-1,0,0};
int fy[5]={0,0,0,1,-1};
struct N{
int x,y;
};
inline void bfs(int x)
{
deque<N> q;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(mapp[i][j]-'0'==x)
{
q.push_front((N){i,j});//将本国的点放入队列
dis[i][j][x]=0;//距离设为0
}
}
}
while(!q.empty())
{
N u=q.front();
int xx=u.x,yy=u.y;
q.pop_front();
for(int i=1;i<=4;i++)
{
int sx=xx+fx[i],sy=yy+fy[i];
if(sx<1||sx>n||sy<1||sy>m||mapp[sx][sy]=='#') continue;
if(dis[sx][sy][x]>=inf)
{
int w=(mapp[sx][sy]=='.');
dis[sx][sy][x]=dis[xx][yy][x]+w;
if(w) q.push_back((N){sx,sy});//边权为1就放队尾
else q.push_front((N){sx,sy});//边权为0就放队首
}
}
}
return ;
}//普通的01BFS求最短代价
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
memset(dis,0x3f,sizeof(dis));
cin>>n>>m;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>mapp[i][j];
}
}
bfs(1),bfs(2),bfs(3);//每个国家都要把整张图遍历一遍,得到到其他所有点最小距离
int ans=inf;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(mapp[i][j]!='#')
{
int res=0;
for(int k=1;k<=3;k++)
{
res+=dis[i][j][k];
}
if(res<0) continue;
if(mapp[i][j]=='.') res-=2;//上文的注意情况
ans=min(ans,res);//得出最小的答案
}
}
}
if(ans>=inf) cout<<"-1\n";
else cout<<ans<<"\n";
return 0;
}
标签:sy,sx,进阶,int,dfs,bfs,mapp,include
From: https://www.cnblogs.com/keepofsilence/p/17971303