【AtCoder】AtCoder Grand Contest 014 解题报告

点此进入比赛

A:Cookie Exchanges(点此看题面

  • 给定三个数\(A,B,C\),每次操作令每个数等于另两个数的平均值。
  • 求几次操作之后会出现奇数,或判断永远不会出现。
  • \(A,B,C\le10^9\)

签到题

稍微推一下就会发现如果有解一定能在\(O(logV)\)的次数内得到。

于是直接暴力模拟一下就好了。

代码:\(O(logV)\)

#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 LL long long
using namespace std;
LL A,B,C;
int main()
{
	RI T;LL a,b,c;for(scanf("%lld%lld%lld",&A,&B,&C),T=0;T<=100;++T,A=a,B=b,C=c)//暴力模拟log次
		if(A&1||B&1||C&1) return printf("%d\n",T),0;else a=(B+C)/2,b=(A+C)/2,c=(A+B)/2;//模拟
	return puts("-1"),0;//无解
}

B:Unplanned Queries(点此看题面

  • 有一棵\(n\)个点的树,初始边权全为\(0\)
  • 进行\(m\)次操作,每次把一条树上路径的所有边边权加\(1\)
  • 现在给你操作序列,问是否可能存在一棵树,使得执行完这些操作后所有边权都是偶数。
  • \(n,m\le10^5\)

结论题

显然先边权化点权,那么每条边的边权都变成了它所连两点中子节点的点权。

接着就考虑一次操作可以通过树上差分转化为单点修改,也就是给\(x,y\)点权加\(1\),给\(LCA(x,y)\)点权减\(2\)

其中,\(LCA(x,y)\)点权减\(2\)并不会改变奇偶性,可以直接忽略。

然后回想我们差分值转真实值的过程,是给每个点的点权加上所有子节点的点权。

假设所有子节点的点权都是偶数(如果有奇数就说明无解了),那么加上子节点的点权之后当前点点权的奇偶性并不变,所以最终这个差分值转真实值的过程也可以忽略。

得出结论,对于每次操作修改\(x,y\)两点的奇偶性,如果所有点点权都为偶数则有解,否则无解。

代码:\(O(n+m)\)

#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 100000
using namespace std;
int n,m,s[N+5];
int main()
{
	RI i,x,y;for(scanf("%d%d",&n,&m),i=1;i<=m;++i) scanf("%d%d",&x,&y),s[x]^=1,s[y]^=1;//修改两个端点点权奇偶性
	for(i=1;i<=n;++i) if(s[i]) return puts("NO"),0;return puts("YES"),0;//如果都为偶数则有解,否则无解
}

C:Closed Rooms(点此看题面

  • 给定一张\(n\times m\)的网格图,有一些格子被上了锁无法行走。
  • 每轮操作你可以先走\(k\)步,然后解锁\(k\)个格子。
  • 求至少几轮你才能从起点走到任一边界格子。
  • \(n,m\le800\)

\(BFS\)

考虑除了第一轮之外,每一轮行走之前都可以利用上一轮的解锁操作预先把要走的格子全解锁掉。

也就是说,除了第一轮外,每一轮都可以任意走。

那么只要\(BFS\)一遍求出第一轮可以到达的离边界最近的格子,然后算一下轮数就好了。

代码:\(O(nm)\)

#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 800
using namespace std;
const int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
int n,m,k,dis[N+5][N+5];char s[N+5][N+5];
int qx[N*N+5],qy[N*N+5];I void BFS()//BFS求出第一轮能走到的格子
{
	RI i,j,H=1,T=0;for(i=1;i<=n;++i) for(j=1;j<=m;++j)
		if(s[i][j]=='S') {qx[++T]=i,qy[T]=j,dis[i][j]=0;break;}//找到起点
	RI x,y,nx,ny;W(H<=T&&dis[qx[H]][qy[H]]^k)//行走步数不超过k
		for(x=qx[H],y=qy[H++],i=0;i^4;++i) (nx=x+dx[i])&&nx<=n&&(ny=y+dy[i])&&
			ny<=m&&!~dis[nx][ny]&&s[nx][ny]^'#'&&(qx[++T]=nx,qy[T]=ny,dis[nx][ny]=dis[x][y]+1);
}
int main()
{
	RI i,j;for(scanf("%d%d%d",&n,&m,&k),i=1;i<=n;++i) scanf("%s",s[i]+1);
	RI t=1e9;for(memset(dis,-1,sizeof(dis)),BFS(),i=1;i<=n;++i)
		for(j=1;j<=m;++j) ~dis[i][j]&&(t=min(t,min(min(i-1,n-i),min(j-1,m-j))));//找到离边界最近的距离
	return printf("%d\n",1+(t+k-1)/k),0;//计算轮数
}

D:Black and White Tree(点此看题面

  • 给定一棵\(n\)个点的树,先手可以将一个点染白,后手可以将一个点染黑。
  • 若最终存在一个白点周围没有黑点则先手胜,否则后手胜。
  • \(n\le10^5\)

博弈

考虑如果先手染了某个叶节点的父节点,那么后手就不得不去染那个叶节点,然后我们就可以把这两个点从原树中删去。

发现先手始终这样操作一定是最优的,因为他一直能掌控主动权。

而这样一来先手的胜利条件就是某个时刻存在一个点同时是两个及以上叶节点的父亲,后手反之。

那么我们就是要从叶节点开始往树的内部不断走,这可以通过拓扑来实现。

但实际上一条五个点的链就是一个\(Hack\)数据,此时中间的点不与任何叶节点(黑点)相邻,但也因此不是任何叶节点的父亲。

所以最后还要再扫一遍判一下是否存在一个白点周围没有黑点。

代码:\(O(n)\)

#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 100000
#define add(x,y) (e[++ee].nxt=lnk[x],++d[e[lnk[x]=ee].to=y])
using namespace std;
int n,d[N+5],q[N+5],nxt[N+5],col[N+5],IQ[N+5];
int ee,lnk[N+5];struct edge {int to,nxt;}e[2*N+5];
I int GetNxt(CI x) {RI i;for(i=lnk[x];IQ[e[i].to];i=e[i].nxt);return e[i].to;}//相邻的还未加入队列的节点,就是该叶节点的父节点
int main()
{
	RI i,k,x,y;for(scanf("%d",&n),i=1;i^n;++i) scanf("%d%d",&x,&y),add(x,y),add(y,x);
	#define First() (puts("First"),exit(0),0)//先手必胜
	#define Push(x) (IQ[q[++T]=x]=1,nxt[x]=GetNxt(x))//加入队列
	RI H=1,T=0;for(memset(col,-1,sizeof(col)),i=1;i<=n;++i) d[i]==1&&(Push(i),0);//初始把所有叶节点加入队列
	W(H<=T) if(!nxt[k=q[H++]]) col[k]=0;else --d[nxt[k]]==1&&
		(Push(nxt[k]),0),!~col[k]&&(col[k]=1,~col[nxt[k]]&&First(),col[nxt[k]]=0);//如果当前点是叶节点(黑点),父节点就是白点
	for(x=1;x<=n;++x) if(!col[x])//再扫一遍判断
		{for(i=lnk[x];i;i=e[i].nxt) if(col[e[i].to]) break;!i&&First();}//如果有一个白点周围没有黑点
	return puts("Second"),0;//后手必胜
}

E:Blue and Red Tree(点此看题面

  • 给定两棵树,一棵全是蓝边,一棵全是红边。
  • 对于第一棵树,每次你可以选择一条全是蓝边的路径,删去其中任意一条蓝边,然后用红边连接两个端点。
  • 问是否可能得到第二棵树。
  • \(n\le10^5\)

树链剖分

考虑一条红边\((x,y)\)等价于在第一棵树上路径\((x,y)\)中的边都存在的前提下删去其中某一条边。

因此,一条蓝边能被删去,就要满足需要它的红边只剩一条。

我们在每个点上记录一下它到其父节点的边被多少红边需要,每次拿出一条只被一条红边需要的蓝边,找到它是被哪条红边需要,然后就可以同时删去这条蓝边和这条红边。

具体实现中,我们只要树链剖分一下,维护点权最小值、最小值对应点、最小值对应点覆盖边编号异或和(由于我们只会在被一条边覆盖时使用,因此这就是覆盖边编号)。

然后就是模拟了。

代码:\(O(nlogn)\)

#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 100000
#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[2*N+5];
struct line {int x,y;}s[N+5];
namespace T//树链剖分
{
	class SegmentTree
	{
		private:
			#define PT CI l=2,CI r=n,CI rt=1
			#define LT l,mid,rt<<1
			#define RT mid+1,r,rt<<1|1
			#define PU(x) (Mn[x<<1]<Mn[x<<1|1]?(Mn[x]=Mn[x<<1],ID[x]=ID[x<<1],\
				P[x]=P[x<<1]):(Mn[x]=Mn[x<<1|1],ID[x]=ID[x<<1|1],P[x]=P[x<<1|1]))
			#define PD(x) (A[x]||X[x])&&(T(x<<1,A[x],X[x]),T(x<<1|1,A[x],X[x]),A[x]=X[x]=0)
			#define T(x,v,p) (Mn[x]+=v,A[x]+=v,P[x]^=p,X[x]^=p)
			int Mn[N<<2],ID[N<<2],P[N<<2],A[N<<2],X[N<<2];
		public:
			I void Build(PT)//建树
			{
				if(l==r) return (void)(ID[rt]=l);RI mid=l+r>>1;Build(LT),Build(RT),PU(rt);
			}
			I void U(CI L,CI R,CI v,CI p,PT)//区间修改,权值加上v,编号异或p
			{
				if(L>R) return;if(L<=l&&r<=R) return (void)T(rt,v,p);RI mid=l+r>>1;
				PD(rt),L<=mid&&(U(L,R,v,p,LT),0),R>mid&&(U(L,R,v,p,RT),0),PU(rt);
			}
			I void Get(int& mn,int& id,int& p) {mn=Mn[1],id=ID[1],p=P[1];}//取出最小点信息
	}S;
	int f[N+5],g[N+5],dep[N+5],sz[N+5];I void dfs1(CI x=1)//树剖第一遍dfs
	{
		sz[x]=1;for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^f[x]&&
		(
			dep[e[i].to]=dep[f[e[i].to]=x]+1,dfs1(e[i].to),
			sz[x]+=sz[e[i].to],sz[e[i].to]>sz[g[x]]&&(g[x]=e[i].to)
		);
	}
	int d,dfn[N+5],tp[N+5];I void dfs2(CI x=1,CI c=1)//树剖第二遍dfs
	{
		dfn[x]=++d,tp[x]=c,g[x]&&(dfs2(g[x],c),0);
		for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^f[x]&&e[i].to^g[x]&&(dfs2(e[i].to,e[i].to),0);
	}
	I void U(CI id,CI op)//树剖修改路径
	{
		RI x=s[id].x,y=s[id].y;W(tp[x]^tp[y])
			dep[tp[x]]<dep[tp[y]]&&(x^=y^=x^=y),S.U(dfn[tp[x]],dfn[x],op,id),x=f[tp[x]];
		dep[x]>dep[y]&&(x^=y^=x^=y),S.U(dfn[x]+1,dfn[y],op,id);//注意不包括LCA
	}
	I bool Check()//模拟
	{
		RI Mn,ID,P;if(S.Get(Mn,ID,P),Mn>1) return false;//如果没有覆盖次数为1的点
		return S.U(ID,ID,1e9,0),U(P,-1),true;//删去这条蓝边和这条红边
	}
}
int main()
{
	RI i,x,y;for(scanf("%d",&n),i=1;i^n;++i) scanf("%d%d",&x,&y),add(x,y),add(y,x);
	for(T::dfs1(),T::dfs2(),T::S.Build(),i=1;i^n;++i) scanf("%d%d",&s[i].x,&s[i].y),T::U(i,1);//把红边覆盖到第一棵树上
	for(i=1;i^n;++i) if(!T::Check()) return puts("NO"),0;return puts("YES"),0;//模拟
}

F:Strange Sorting(点此看题面

  • 给定一个长度为\(n\)的排列。
  • 每轮操作,会找出所有比前面元素都大的元素,保持相对顺序不变,把它们移到最后面。
  • 问几轮操作后排列会有序。
  • \(n\le2\times10^5\)

思维

首先,我们发现,比一个数小的所有数都不会影响到这个数的移动。

因此,考虑从大到小枚举每个数一个个加入。

假设原本的答案为\(t\),显然若现在加入的最小值\(i\)\(t\)轮操作后恰好在开头位置就无需再操作,否则就需要共计\(t+1\)轮才能完成排序。

那么现在问题就是如何判断现在加入的最小值\(i\)\(t\)轮操作后是否在开头位置了。

发现当前的次小值,也就是上次加入的最小值\(i+1\),既然能在第\(t\)轮操作后在开头位置,那么\(t-1\)轮操作后它的前面必然有数,并且这些数是递增的这才可能一轮拿完。

而现在的\(i\)如果要在开头,则\(t-1\)轮操作后它必须在\(i+1\)之前,同时它的前面也必然有数。

不妨设\(t-1\)轮操作后开头的数为\(k\),也就是说我们要判断\(i\)是否在\(k\)\(i+1\)之间。

可以证明\(k,i,i+1\)三者的位置关系满足循环不变,那么我们只要看看在原序列中三者位置关系是否为\(k,i,i+1\)\(i,i+1,k\)\(i+1,k,i\)即可。

如果需要增加一轮,那么就将\(t\)\(1\),而第\((t+1)-1\)轮操作后的开头元素正是\(i+1\)

代码:\(O(n)\)

#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,a[N+5],p[N+5];
int main()
{
	RI i;for(scanf("%d",&n),i=1;i<=n;++i) scanf("%d",a+i),p[a[i]]=i;//记录每个数的位置
	RI t=0,k=0;for(i=n-1;i;--i)//从大到小加入每个数
	{
		if(!t) {p[i]>p[i+1]&&(t=1,k=i+1);continue;}//特判t仍为0
		if(p[k]<p[i]&&p[i]<p[i+1]) continue;if(p[i]<p[i+1]&&p[i+1]<p[k]) continue;
		if(p[i+1]<p[k]&&p[k]<p[i]) continue;++t,k=i+1;//判断是否需要加一轮
	}return printf("%d\n",t),0;
}
posted @ 2020-12-04 12:03  TheLostWeak  阅读(52)  评论(0编辑  收藏