前言
又是一场过去的\(AtCoder\)。
一边希望着题目不会太难,一边却又希望题目能有足够的难度,让我从中学到更多。
\(A\):Darker and Darker(点此看题面)
大致题意: 给定一个\(n\times m\)的矩形,上面有一些黑格子。每个时刻所有与黑格子有边相邻的白格子会被染黑,问多久以后所有格子被染黑。
显然的\(BFS\),甚至可以算作\(BFS\)的板子,不加赘述了。
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 1000
using namespace std;
const int dx[4]={0,0,1,-1},dy[4]={1,-1,0,0};//四个方向
int n,m,vis[N+5][N+5];char s[N+5][N+5];struct Data {int x,y,t;}q[N*N+5];
int main()
{
RI i,j,H=1,T=0;for(scanf("%d%d",&n,&m),i=1;i<=n;++i)
for(scanf("%s",s[i]+1),j=1;j<=m;++j) s[i][j]=='#'&&(q[++T]=(Data){i,j,0},vis[i][j]=1);//初始化队列
Data k;RI nx,ny;W(H<=T) for(k=q[H++],i=0;i^4;++i) (nx=k.x+dx[i])&&nx<=n&&//BFS
(ny=k.y+dy[i])&&ny<=m&&!vis[nx][ny]&&(q[++T]=(Data){nx,ny,k.t+1},vis[nx][ny]=1);//扩展
return printf("%d\n",q[T].t),0;//输出最后一个格子被染黑的时间
}
\(B\):LRUD Game(点此看题面)
大致题意: 给定一个\(n\times m\)的棋盘,一开始在\((x,y)\)处有一个棋子。博弈二人各有一个长度为\(n\)的操作串(由"L""R""U""D"构成),第\(i\)个时刻可以选择按操作串第\(i\)位的方向移动或保持不动。先手目标是将棋子移出棋盘,后手目标是使棋子留在棋盘上。问后手是否必胜。
首先考虑到正着做肯定非常麻烦,说不定两个人还会耍下心机什么的,因此正难则反,我们倒着做。
由于先手的最优策略必然是始终向一个方向走,因此一开始想分别考虑四种方向,结果自己造出一组很有趣的数据:
11 5 6
6 3
ULLRRR
LUUUUU
发现此时后手无论第一步走不走,先手都有对应的方案,应输出"No"。
可如果你认为后手使用最优方案=能预知先手的选择,那么就会输出"Yes"。我一开始用这样一个小数据成功卡掉了自己的无数乱搞。
最终发现,需要同时考虑四个方向(或者分别考虑上下和左右),且一旦某一刻满足任意位置都符合条件就要立刻结束循环。
具体地,就是维护\(u,d,l,r\)分别表示当前在前\(u\)行、后\(d\)行、前\(l\)列、后\(r\)列先手都有必胜策略。每次根据操作串进行更新,一旦出现\(d-u\le1\)或\(r-l\le1\)时就说明先手必胜。
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 200000
using namespace std;
int n,sx,sy,h,w;char s1[N+5],s2[N+5];
int main()
{
scanf("%d%d%d%d%d%s%s",&h,&w,&n,&sx,&sy,s1+1,s2+1);
RI u=0,d=h+1,l=0,r=w+1;for(RI i=n;i&&d-u>1&&r-l>1;--i)//从后往前,一旦某一刻满足任意位置都符合条件就立刻结束
s2[i]=='U'?d<=h&&++d:(s2[i]=='D'?u^1&&--u:(s2[i]=='L'?r<=w&&++r:l^1&&--l)),//后手策略
s1[i]=='U'?++u:(s1[i]=='D'?--d:(s1[i]=='L'?++l:--r));//先手策略
return puts(u<sx&&sx<d&&l<sy&&sy<r?"YES":"NO"),0;//判断
}
\(C\):Removing Coins(点此看题面)
大致题意: 给定一棵树,每次可以选定一个节点,删去除它以外的所有叶节点,博弈双方谁先不能操作谁输。 问谁有必胜策略。
考虑博弈论的一般套路,把树上问题转化为链的问题。
首先当只剩两个点时,此时的决策者显然必输。
否则,我们发现对于这道题,实际上只要考虑树的直径的长度即可:
- 当选择直径的两个端点时,直径的长度只会减少\(1\)。
- 而当选择其他点时,直径的长度必然减少\(2\)。
由于这两种操作在每一个时刻必然都有对应的实现方式,因此只要假设初始直径长度为\(len\),题目就被转化成了:
有\(len-2\)个石子(最后剩下的两个石子比较特殊),每次可以取\(1\)或\(2\)个石子,谁先不能操作谁输。
我记得这好像可以算是一道小学数学题吧。。。
只要判断\(len-2\)是否为\(3\)的倍数,是则后手必胜,不是则先手必然可以把它转化成\(3\)的倍数的情况,先手必胜。
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 200000
#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
using namespace std;
int n,ee,lnk[N+5];struct edge {int to,nxt;}e[N<<1];
class FastIO
{
private:
#define FS 100000
#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
#define D isdigit(c=tc())
char c,*A,*B,FI[FS];
public:
I FastIO() {A=B=FI;}Tp I void read(Ty& x) {x=0;W(!D);W(x=(x<<3)+(x<<1)+(c&15),D);}
}F;
int q[N+5],d[N+5];I int BFS(CI x)//求树的直径的BFS
{
RI i,k,f=0,H=1,T=1;for(i=1;i<=n;++i) d[i]=0;d[q[1]=x]=1;
W(H<=T) for(i=lnk[k=q[H++]];i;i=e[i].nxt)
!d[e[i].to]&&(d[q[++T]=e[i].to]=d[k]+1)>d[f]&&(f=e[i].to);return f;
}
int main()
{
RI i,x,y;for(F.read(n),i=1;i^n;++i) F.read(x),F.read(y),add(x,y),add(y,x);//读入
return x=BFS(1),puts((d[BFS(x)]-2)%3?"First":"Second"),0;//判断len-2是否是3的倍数
}
\(D\):Complexity(点此看题面)
大致题意: 给定一个\(01\)矩形,定义一个子矩形的复杂度:若整块区域同色则复杂度为\(0\);否则,按某一行或某一列将矩形分为两部分,复杂度是两部分复杂度较大值的最小值\(+1\)。求整个矩形的复杂度。
只会状态数都要\(O(n^4)\)的大暴力\(DP\)。。。
实际上这题有一个显然的性质,就是答案肯定不超过\(O(\log nm)\)。(这个性质我也想到了,但用不来)
考虑我们枚举答案,那么原先\(O(n^4)\)的大暴力就变成了\(O(n^4\log n)\)的更大暴力,但它的状态类型为\(bool\)。
\(bool\)显然很浪费,因此我们可以把原先状态中的某一维改为状态的值。
即,设\(f_{p,i,j,k}\)表示复杂度为\(p\)时,以第\(i\)行为上边界、第\(j\)行为下边界、第\(k\)列为左边界,右边界的最大值。
然后这就很好转移了:
- 按列分:\(f_{p,i,j,k}=f_{p-1,i,j,f_{p-1,i,j,k}+1}\)。
- 按行分:\(f_{p,i,j,k}=\max_{x=i}^{j-1}\{\min\{f_{p,i,x,k},f_{p,x+1,j,k}\}\}\)。由于\(f\)数组显然具有单调性,因此可以二分。
最终复杂度\(O(n^3\log^2n)\)。
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 185
#define Gmax(x,y) (x<(y)&&(x=(y)),x)
using namespace std;
int n,m,s[N+5][N+5],f[2][N+5][N+5][N+5];char st[N+5][N+5];
I int Find(CI p,CI i,CI j,CI k)//二分
{
RI l=i,r=j-1,mid,t=0;W(l<=r) mid=l+r>>1,t=max(t,min(f[p][i][mid][k],//t统计答案
f[p][mid+1][j][k])),f[p][i][mid][k]>f[p][mid+1][j][k]?l=mid+1:r=mid-1;return t;//利用单调性
}
int main()
{
RI i,j,k,p=1,t,w;for(scanf("%d%d",&n,&m),i=1;i<=n;++i)
for(scanf("%s",st[i]+1),j=1;j<=m;++j) s[i][j]=s[i-1][j]+(st[i][j]=='#');//s记录每列前缀和
for(i=1;i<=n;++i) for(j=i;j<=n;++j) for(k=1;k<=m;++k)//计算初始状态
{
if(f[0][i][j][k]=k-1,(t=s[j][k]-s[i-1][k])&&t^(j-i+1)) continue;//一列都没有
W(f[0][i][j][k]^m&&s[j][f[0][i][j][k]+1]-s[i-1][f[0][i][j][k]+1]==t) ++f[0][i][j][k];//扩展
W(k+1<=f[0][i][j][k]) f[0][i][j][k+1]=f[0][i][j][k],++k;//给访问到的列一同赋值,保证复杂度
}
for(t=0;f[p^1][1][n][1]^m;p^=1,++t) for(i=1;i<=n;++i)//动态规划,枚举答案,直至有合法方案
for(j=i;j<=n;++j) for(k=1;k<=m;++k) (w=f[p^1][i][j][k])^m?//判断右边界是否已经达到m
(f[p][i][j][k]=f[p^1][i][j][w+1])^m&&Gmax(f[p][i][j][k],Find(p^1,i,j,k)):(f[p][i][j][k]=m);//两种转移
return printf("%d\n",t),0;//输出答案
}
\(E\):Go around a Circle(点此看题面)
大致题意: 给定一个圆,找出其上\(n\)个点,每两点之间可以染成红色或蓝色。给定一个"R"和"B"组成的字符串,询问有多少种染色方案,使得从圆上任意一点出发,都存在一条路径(可双向行走)满足经过颜色组成的字符串和给定字符串相同。
假设给定字符串中的第一个字符为"R",不是的话可以反转所有"R"和"B"。
显然,染色方案中不可能存在两段连续的蓝色,否则在两段蓝色中间的那个点根本无法出发。
假如整个字符串全是"R",那么就是要求不存在两段连续蓝色的方案数,这个\(DP\)应该是非常简单的。
否则,我们用\(a_{1\sim t}\)存下给定字符串中每一段极长的相同字符段的长度(显然下标为奇数表示"R",下标为偶数表示"B"),然后得到以下几个性质:
- 每一个红色段长肯定是奇数。 因为只有奇数才能影响奇偶性,一个偶数段中,从第一个点出发只能走出偶数个"R",从第二点出发只能走出奇数个"R",两点肯定无法同时满足限制。
- 每一个红色段长不可能超过\(a_1+[2|a_1]\)。 若\(a_1\)是奇数,你从第一个点出发必然要走过整个串,串长不能超过\(a_1\);若\(a_1\)为偶数,你从第二点出发必然要走过整个串,串长不能超过\(a_1+1\)。
- 给定串中最后一个"R"段可以忽略, 即\(k\)为奇数时将\(k\)减\(1\)。因为你无论何时都可以任意刷"R"。
- 每一个红色段长不可能超过长度为奇数的极长"R"段长。 因为对于奇数的情况必须要跨越整个段。
结合第二个性质和第四个性质,就可以得出每个红色段长的上界。
然后就可以设\(f_i\)表示填了前\(i\)段的方案数,可以用前缀和优化转移。
由于红色段长(奇数)+蓝色段长(\(1\))为偶数,因此其实只要\(DP\)偶数位即可。
注意由于这是一个环,环可以旋转,\(DP\)后还要枚举最后一段长\(i\)(包括红色段和蓝色段),答案应该是:
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 200000
#define X 1000000007
using namespace std;
int n,m,t,a[N+5],f[N+5],g[N+5];char s[N+5];
int w[N+5][2];I void Work()//只有一种字符
{
#define DP() for(RI i=2;i<=n;++i) w[i][0]=w[i-1][1],w[i][1]=(w[i-1][0]+w[i-1][1])%X;//DP
w[1][1]=1;DP();RI t=(w[n][0]+w[n][1])%X;//第一位填1
w[1][0]=1,w[1][1]=0;DP();printf("%d\n",(t+w[n][1])%X);//第一位填0,不能首尾都是0
}
int main()
{
RI i;for(scanf("%d%d%s",&n,&m,s+1),i=1;i<=m;++i) s[i]^s[i-1]&&++t,++a[t];//存下极长段长
if(t&1&&--t,!t) return Work(),0;if(n&1) return puts("0"),0;//特判
RI p=a[1]+(a[1]&1^1);for(i=3;i<=t;i+=2) a[i]&1&&(p=min(p,a[i]));//求出段长上界
for(f[0]=g[0]=1,i=2;i<=n;i+=2) f[i]=(g[i-2]+(i-p-3>=0?X-g[i-p-3]:0))%X,g[i]=(g[i-2]+f[i])%X;//只DP偶数位,前缀和优化
RI res=0;for(i=2;i<=p+1&&i<=n;i+=2) res=(1LL*i*f[n-i]+res)%X;return printf("%d\n",res),0;//枚举最后一段长统计答案
}
\(F\):Adding Edges(点此看题面)
大致题意: 给定一张图和一棵树。每次你可以选择三点\(a,b,c\),满足存在边\((a,b),(b,c)\)且不存在边\((a,c)\),若\(a,b,c\)在树中一条简单路径上,则连边\((a,c)\)。求最终整张图有多少条边。
一开始口胡了一个\(O(n^2log^2n)\)的做法,非常难写又不一定跑得过去,弃了。
题解中的做法,是对于一组边\((a,b),(a,c)\),若\(a,b,c\)在一条简单路径上且顺序依次是\(a,b,c\),则删去\((a,c)\),加上\((b,c)\)。
显然这不会对答案造成影响,但却方便了我们后续的处理:
考虑如果一对点\((x,y)\),如果它们的树上路径可以由若干条题目中给出的边对应的树上路径拼接而成,那么它们之间的边就能够存在。
因此,现在的核心问题就是如何完成对边的处理。
我们可以维护一个\(p_{a,b}\)表示在\(a\rightarrow b\)这条路径上,从\(a\)出发,经过若干条边之后能到达的离\(b\)最近的点。
考虑当添加一条边\((a,b)\)时,如果存在\(p(a,b)\),那么按照我们的处理方式,就应该改为添加边\((p(a,b),b)\)。
否则,我们\(BFS\)以\(a\)为根时\(b\)的子树,如果一个点\(t\)满足\(p_{a,t}=0\),则令\(p_{a,t}=b\)且继续\(BFS\);而若\(a,t\)之间存在边,按照我们的处理方式,就应该删去边\((a,t)\),连接边\((b,t)\)。
具体实现详见代码。
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 2000
#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
using namespace std;
int n,m,ans,ee,lnk[N+5],p[N+5][N+5],w[N+5][N+5];struct edge {int to,nxt;}e[N<<1];
class Tree//以x为根时的树
{
private:
int f[N+5];//每个点的父节点
public:
I int operator [] (CI x) Con {return f[x];}
I void dfs(CI x)//初始化
{
for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^f[x]&&(f[e[i].to]=x,dfs(e[i].to),0);
}
I void Calc(CI x,RI s)//统计答案
{
p[s][x]==x&&(s=x,++ans);for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^f[x]&&(Calc(e[i].to,s),0);
}
}F[N+5];
int c;vector<pair<int,int> > V[N+5];
int q[N+5];I void BFS(CI id,CI x,CI y)//BFS以x为根时y的子树
{
RI i,k,t,H=1,T=0;q[++T]=y,p[x][y]=y,w[x][y]=1;//更新y的信息
W(H<=T) for(i=lnk[k=q[H++]];i;i=e[i].nxt) (t=e[i].to)^F[x][k]&&
(!p[x][t]?(p[x][q[++T]=t]=y):w[x][t]&&(V[id].push_back(make_pair(y,t)),w[x][t]=w[t][x]=0));//分情况讨论
}
I void Link(CI x,CI y)//连边
{
if(p[x][y]==y||p[y][x]==x) return;if(p[x][y]) return Link(p[x][y],y);if(p[y][x]) return Link(p[y][x],x);//几类特殊情况
RI id=++c;V[id].clear(),BFS(id,x,y),BFS(id,y,x);//V[id]存储要加的边,两个BFS
for(RI i=0,s=V[id].size();i^s;++i) Link(V[id][i].first,V[id][i].second);--c;//把要加的边加上
}
int main()
{
RI i,x,y;for(scanf("%d%d",&n,&m),i=1;i^n;++i) scanf("%d%d",&x,&y),add(x,y),add(y,x);//建树
for(i=1;i<=n;++i) F[i].dfs(i);for(i=1;i<=m;++i) scanf("%d%d",&x,&y),Link(x,y);//建图
for(i=1;i<=n;++i) F[i].Calc(i,i);return printf("%d\n",ans>>1),0;//求答案,最后除以2
}
后记
这场难度要高上许多,但至少还是能够看明白题解的。
感觉这些题目都妙得很,多做做可以锻炼思维,也能从中有所启发。