Codeforces Round 876 (Div. 2)

Preface

DP腐乳闪总出列!

(本来以为大掉分的一把,但这个号因为挺新的所以竟然还能上挺多分的,压线完成了5场上紫)

早知道去做E题了,感觉CF真得要看题目相性,有些题目就是一眼感觉不适合自己的说


A. The Good Array

一个要动点脑子的签到题,因为\(a_1,a_n\)必须等于\(1\),然后中间的\(n-1\)个元素不能出现连续的\(k\)\(0\)

小推一下式子就是\(1+\lceil\frac{n-1}{k}\rceil\)

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int t,n,k;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	scanf("%d%d",&n,&k),printf("%d\n",1+(n-1+k-1)/k);
	return 0;
}

B. Lamps

这场上来给我的一个感觉就是题目好难读,难受捏

其实看清题目就很简单,因为一个灯泡爆了就不会被算到点亮的里面去

因此不难发现对于\(a_i=1\)的所有灯泡最多只能选其中一个点亮,而\(a_i=2\)的就只能选两个,依此类推

vector存下所有的灯泡然后排序选大的那些即可

#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int t,n,k,x,y; vector <int> v[N]; long long ans;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		RI i,j; for (scanf("%d",&n),i=1;i<=n;++i) v[i].clear();
		for (i=1;i<=n;++i) scanf("%d%d",&x,&y),v[x].push_back(y);
		for (ans=0,i=1;i<=n;++i)
		{
			sort(v[i].begin(),v[i].end()); int sz=v[i].size();
			for (j=1;j<=min(i,sz);++j) ans+=v[i][sz-j];
		}
		printf("%lld\n",ans);
	}
	return 0;
}

C. Insert Zero and Invert Prefix

刚开始想歪了走到等价转化那条路上去了,然后浪费好多时间,后面发现直接倒着构造即可

从无解的情况出发,不难发现当\(a_n=1\)是一定无解的,否则一定有解,接着考虑如何构造

考虑从后往前把序列中的一段\(1\)开头,后面跟着一段\(0\)的序列进行划分

比如对于样例1 0 0 1 1 0,可以划分成一段1 1 0和一段1 0 0

而这样一段序列的构造是很显然的,设这一段的长度为\(k\),且其中\(1\)的个数为\(c\)

那我们只要进行\(k-1\)\(0\)操作,然后用一次\(c\)操作把前面的\(c\)\(0\)反转成\(1\)即可

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int t,n,a[N],cnt,ans[N];
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		RI i,j,k,cnt; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]);
		if (a[n]) { puts("NO"); continue; }
		for (i=n,cnt=0;i>=1;i=k)
		{
			for (j=i;j>=1&&a[i]==a[j];--j);
			for (k=j;k>=1&&a[j]==a[k];--k);
			for (RI t=1;t<i-k;++t) ans[++cnt]=0;
			ans[++cnt]=j-k;
		}
		for (puts("YES"),i=1;i<=n;++i) printf("%d%c",ans[i]," \n"[i==n]);
	}
	return 0;
}

D. Ball Sorting

唉开始的基础转化就错了,导致后面一直深陷里面走不出来,本来挺简单的一个DP的说

比赛的时候思路大概是就是如果不考虑\(0\)球的个数限制,那么找出序列的LIS长度\(x\),则答案就是\(n-x\)

然后就在想如果有\(k\)\(0\)球的限制,就会划分成\(k+1\)个段,然后贡献就是每段里面的LIS还要保证能首尾相接起来,但怎么搞都对不上样例

好家伙后面仔细一想,其实不能动的根本不是LIS,还要满足连续性,即一个极长的单调上升区间

其实也很好理解,考虑如果现在有一个\(0\)放在序列的开头,那么它至少要操作\(t\)次,满足序列剩下的\(n-t\)个数是单调升的,这样把前\(t\)个数插入进去才是对的

同理如果\(0\)在末尾的话就是找极长的单调上升前缀,而如果前后都有\(0\)的段就是找一段极长的单调上升区间

因此现在问题就转化为,把序列分为\(k+1\)段,要求一种方案使得每段内的极长单调上升区间总长度和最长,并且相邻的区间的收尾也满足单调升的条件

此时我们就很容易设计DP方程了,令\(f_{i,j}\)表示处理了前\(i\)个数,放了\(j\)\(0\)球时最少要移动的球的数目

转移的话若\(a_i>a_{i-1}\)就直接从\(i-1\)处转移,否则枚举上一段内和它接在一起的位置\(k\)转移即可

具体实现看代码,想通了就非常简单的说,总复杂度\(O(n^3)\)

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=505,INF=1e9;
int t,n,a[N],f[N][N];
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		RI i,j,k,t; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]);
		for (a[n+1]=INF,i=1;i<=n+1;++i) for (j=0;j<=n;++j) f[i][j]=INF;
		for (i=1;i<=n+1;++i) for (j=0;j<=n;++j)
		{
			if (a[i-1]<a[i]) f[i][j]=f[i-1][j];
			if (j) for (k=0;k<i;++k) if (a[k]<a[i]) f[i][j]=min(f[i][j],f[k][j-1]+i-k-1);
		}
		for (i=1;i<=n;++i) printf("%d%c",f[n+1][i]," \n"[i==n]);
	}
	return 0;
}

E. Decreasing Game

趣题,看似是博弈其实和博弈没太大关系,是个思博题

首先考虑如果我们存在一种划分方案,可以把\(n\)个数划分为两个集合\(A,B\),满足两个集合内元素的和相同,则此时一定后手必胜

具体的策略也很简单,若先手选择操作集合\(A\)中的数,我们就任意在集合\(B\)中取一个数即可,反之如果先手取\(B\)集合我们就取\(A\)集合

不难发现此时两个集合减去的数相同,因此一轮过后依然满足之前的性质,那么在若干轮后一定会变成两个集合中的数都是\(0\)的情况,因此是种必胜策略

那么有意思的地方就是不能划分的情况怎么处理,这里就考验人类智慧了,答案选择先手然后随便操作都能获胜(你没有看错就是随便做)

证明的话其实也很简单,假设在一轮随便操作后,使得当前局面变为了可划分成两个集合的局面,那么说明我们的策略有问题,因为此时对手作为后手可以用上面的策略击败我们

但考虑真的会出现这种局面吗,不妨设原来不存在这样一种划分方案,在一轮操作选出了数\(x,y\)(不妨设\(x\le y\))后,剩下的局面存在了可划分方案

\(x=y\)的情况显然是矛盾的,不用考虑,因此现在只考虑\(x<y\)的情形

此时操作完会剩下一个数\(y-x\),不妨设\(y-x\)在后面的划分中被归到了集合\(A\)

设后面的划分中集合\(A\)的元素和是\(S\),那么\(A\)中其它元素的和就是\(S-(y-x)\),而\(B\)的元素和就是\(S\)

然后我们会发现如果剩下的局面有解,那么我们如果撤销对\(x,y\)的操作,把\(y\)加入集合\(A\),把\(x\)加入集合\(B\)

此时集合\(A\)的元素和就是\(S-(y-x)+y=S+x\)\(B\)的元素和就是\(S+x\),这说明原来就存在着一种划分方案,与我们的前提矛盾,因此结论成立

然后这题就做完了,确实是很有意思的说

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=305,INF=1e9;
int t,n,a[N],sum,f[N][N*N],g[N][N*N],tp[N];
int main()
{
	RI i,j; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]),sum+=a[i];
	for (f[0][0]=1,i=0;i<n;++i) for (j=0;j<=sum;++j)
	if (f[i][j]) f[i+1][j]=f[i+1][j+a[i+1]]=g[i+1][j+a[i+1]]=1;
	if (sum%2||!f[n][sum/2])
	{
		puts("First"); fflush(stdout);
		while (1)
		{
			for (i=1;i<=n;++i) if (a[i]>0)
			{
				printf("%d\n",i); fflush(stdout);
				if (scanf("%d",&j),!j) return 0;
				int k=min(a[i],a[j]); a[i]-=k; a[j]-=k; break;
			}
		}
	} else
	{
		for (sum>>=1,i=n;i>=1;--i)
		if (g[i][sum]) tp[i]=1,sum-=a[i];
		puts("Second"); fflush(stdout);
		while (1)
		{
			if (scanf("%d",&j),!j) return 0;
			for (i=1;i<=n;++i) if (a[i]>0&&tp[i]!=tp[j])
			{
				printf("%d\n",i); fflush(stdout);
				int k=min(a[i],a[j]); a[i]-=k; a[j]-=k; break;
			}
		}
	}
	return 0;
}

Postscript

现在再想是不是要再搞一个小号接着打Div2,还是去打Div1遭受折磨

因为感觉以现在的水平时而连Div2D都做不出来,去打Div1也是纯被虐

不过如果不打Div1的话不知道多久才有机会突破到橙名,也算是有点两难的说

不过话说回来,立个flag,暑假结束前打到浅橙/深橙,这样感觉明年去打区域赛心里才稍微有点底的说

posted @ 2023-06-04 17:43  空気力学の詩  阅读(49)  评论(0编辑  收藏  举报