题解 CF1329 A,B,C,D Codeforces Round #631 (Div. 1)

比赛链接

CF1329A Dreamoon Likes Coloring

涂到的格子数最少的构造方案是,让第\(i\)次涂色的位置从\(i\)开始。此时涂到的格子数为\(\max_{i=1}^{m}(i+l_i-1)\)

涂到的格子数最多的构造方案是,每种颜色都涂在上一种颜色结束后,即不同颜色互相没有重叠。此时涂到的格子数为\(\sum_{i=1}^ml_i\)

我们断言,当\(\max_{i=1}^{m}(i+l_i-1)\leq n\leq\sum_{i=1}^{m}l_i\)时,总能有一种涂色方法,恰好涂到\(n\)个格子。

这样的题目无非有两种构造方式:(1)先假设安排最小值,然后逐步增加;(2)先假设安排最大值,然后逐步减少。

以(1)为例。我们先令所有\(p_i=i\)从最后一个颜色向前考虑。当前的这一段,本来是接在上一段的开头位置的后面(即\(p_i=p_{i-1}+1\)),为了使涂到的格子数增多。我们把它改为接在上一段的结尾位置后面(即\(p_i=p_{i-1}+l_{i-1}\))。这样,从后向前依次考虑每一段,一定能找到某一个段,在它之前,全部都是\(p_i=p_{i-1}+1\)(缩在一起);在它之后,全部都是\(p_i=p_{i-1}+l_{i-1}\)(完全展开)。我们用它的开头位置来调整答案即可。

参考代码(片段):

sum=0;
for(int i=m;i>=1;--i){
	sum+=len[i];
	if(i-1+len[i-1]+sum-1>=n){
		assert(n-sum+1>i-1);
		for(int j=i;j<=m;++j)p[j]=n-sum+1,sum-=len[j];
		for(int j=1;j<i;++j)p[j]=j;
		break;
	}
}

当然,也可以按第(2)种方式构造。先令\(p_i=n-(\sum_{j=i}^{m}l_j)+1\)。这样可能会导致\(p_1\leq0\)。我们令\(p_1=1\)。如果此时\(p_2\)小于等于\(p_1\),则令\(p_2=2\)。以此类推。在有解的情况下,总能通过调整一个前缀来实现我们想要的效果。

纵观上述两种构造方法,其实殊途同归,都是让一个前缀是最小的形式,一个后缀是最大的形式,两段相连接的部分用来调整。如果记\(suf[i]=\sum_{j=i}^{m}l_j\),我们也可以把两种构造方法都总结为:\(p_i=\max(i,n-suf[i]+1)\)

时间复杂度\(O(n)\)

CF1329B Dreamoon Likes Sequences

考虑\(a\)序列每个数二进制下的最高位。可以发现每个数的最高位一定严格大于上一个数的最高位:如果小于,则\(a\)序列不递增;如果等于,则\(b\)序列不递增。因此,序列长度最多不超过\(\log_2 d\)

因为最高位严格递增了,所以其他位随便怎么填,都能保证\(a\),\(b\)序列分别递增。要保证当前数\(\leq d\)

\(dp[i]\)表示序列里最后一个数的最高位为\(i\)的方案数。则\(dp[i]=2^{i}+\sum_{j=0}^{i-1}dp[j]\cdot2^{i}\)。表示以当前数作为序列的第一个数,或者接在某个序列后面。

当然,如果\(i\)\(d\)的最高位,则转移式里的\(2^i\)应改为\(d-2^i+1\)。这是为了保证当前数\(\leq d\)

时间复杂度\(O(\log^2 d)\)

参考代码(片段):

int d,m,dp[32];
int main() {
	int T;read(T);while(T--){
		read(d);read(m);
		int sum=0;
		for(int i=0;(1<<i)<=d;++i){
			int x=(1<<i);
			if((1<<(i+1))>d)x=(d^(1<<i))+1;
			dp[i]=x;
			for(int j=0;j<i;++j){
				dp[i]=(dp[i]+(ll)dp[j]*x%m)%m;
			}
			sum=(sum+dp[i])%m;
		}
		printf("%d\n",sum);
	}
	return 0;
}

CF1329C Drazil Likes Heap

对节点\(x\)操作,相当于从\(x\)出发,每次向大儿子走,直到走到叶子节点,删掉叶子节点,把路径上(除\(x\)外)每个值都向上移一步,最后把\(x\)原本的值覆盖掉。

考虑整个过程,消失的只有节点\(x\)上的值。我们希望消失的值越大越好。在任意时刻,任何一个子树根节点的值都是子树里最大的。可以想到贪心:不断对根节点操作,直到操作无法进行,然后递归左、右儿子,继续操作。操作无法进行,指的是如果继续操作,将要被删除的叶子节点的深度\(\leq g\)

为什么这样做是最优的?我们从几个方面来考虑。

第一,前面已经论述过,就本次操作而言(暂时不考虑全局情况),能对根节点操作时,我们对根节点操作,一定是最优的,因为消失掉的值最大。

第二,根节点无法操作时,我们如果要强行进行操作,考虑从根节点到被删除的叶子节点的这条路径,路径上的点(除根节点外)一定都是它父亲的大儿子。考虑路径上某个点的小儿子。如果要对这个小儿子操作,则操作后小儿子上新的值只会比原来小,不会比原来大。也就是说,小儿子永远还是小儿子,不会因为之后的操作而变成大儿子。这证明了,绝对不会出现:“在当前根节点无法操作了,但过几次操作后,当前根节点又变得可以操作”的这种情况。因此,我们直接递归考虑左、右儿子,不用回头。

第三,左、右儿子子树里情况是相互独立的

综合以上三点,我们就可以归纳证明,我们的贪心策略是最优的。

时间复杂度\(O(n\log n)\)

参考代码(片段):

const int MAXN=1<<20;
int h,g,a[MAXN*2+5],dep[MAXN*2+5],ans[MAXN*2+5],cnt_ans;
void clr(){
	for(int i=1;i<(1<<h);++i)a[i]=0;
	cnt_ans=0;
}
bool del(int x){
	if(!a[x<<1]&&!a[x<<1|1]){
		if(dep[x]<=g)return 0;
		else{
			//cout<<"del "<<x<<" "<<a[x]<<endl;
			a[x]=0;
			return 1;
		}
	}
	if(a[x<<1]>a[x<<1|1]){
		int val=a[x<<1];
		bool res=del(x<<1);
		if(res)a[x]=val;
		return res;
	}
	else{
		int val=a[x<<1|1];
		bool res=del(x<<1|1);
		if(res)a[x]=val;
		return res;
	}
}
void dfs(int x){
	if(!a[x])return;
	//cout<<"at "<<x<<endl;
	while(del(x))ans[++cnt_ans]=x;
	dfs(x<<1);
	dfs(x<<1|1);
}
int main() {
	for(int i=1;i<MAXN;++i)dep[i]=dep[i>>1]+1;
	int T;cin>>T;while(T--){
		cin>>h>>g;
		for(int i=1;i<(1<<h);++i)cin>>a[i];
		dfs(1);
		assert(cnt_ans==(1<<h)-(1<<g));
		ll sum=0;
		for(int i=1;i<(1<<g);++i)sum+=a[i];
		cout<<sum<<endl;
		for(int i=1;i<=cnt_ans;++i)cout<<ans[i]<<(" \n"[i==cnt_ans]);
		clr();
	}
	return 0;
}

CF1329D Dreamoon Likes Strings

\(s\)中所有相邻的两个相同的字符缩起来,依次放在一起,得到一个新串,记为\(s'\)。例如,若\(s=\text{aabbbcdaab}\),则\(s'=\text{abba}\)

考虑一次操作,可以分为两种:

  1. 选择\(s'\)中的一个字母,并删去。
  2. 选择\(s'\)相邻的两个不同的字母,同时删去。

发现,操作后,\(s'\)中不会新增字符。而当\(s'\)为空时,原串中剩下的字符一定没有相邻且相同的,所以此时我们只需要再额外进行一次操作,就能把原串清空了。因此,总操作次数就是清空\(s'\)所需要的操作次数再加一。

考虑如何用最少的操作次数清空\(s'\)

因为操作2一次可以使\(s'\)的长度减小\(2\),所以我们要尽可能多地使用操作2。也就是说,我们每次尽量找两个不同的字母,把它们同时消掉。这是经典问题。设每个字母\(i\)的出现次数为\(c_i\),设\(sum=\sum c_i\)。考虑出现次数最多的字母\(x\)

  • \(c_x\geq sum-c_x\),我们让所有其他字母都去消\(c_x\),再把最后剩下的\(c_x\)用操作1处理。
  • 否则,我们每次找两个相邻的、不同的字母相消。直到存在某个\(x\)使\(c_x\geq sum-c_x\),问题转化为上一种情况。

在具体实现时,我们不需要每做一次操作就暴力\(\texttt{for}\)一遍来找到下一对相邻的、不同的字母。我们可以从左向右扫描整个\(s'\)序列,同时维护一个栈。如果栈为空,或者栈顶字母等于当前字母,就直接把当前字母入栈。否则,把栈顶的字母弹出,把当前字母和栈顶字母同时消掉。

\(c_x\geq sum-c_x\)时,可以用同样的方法扫描序列。只不过新元素和栈顶元素同时消掉,当且仅当两者中恰有一个是\(x\)

最后,栈里面剩下的一定全是多出来的\(x\)了,只能一个一个删掉。

时间复杂度\(O(n)\)

参考代码(片段):

const int MAXN=2e5;
int n,m,a[MAXN+5],p[MAXN+5],cnt[26],top;
pii sta[MAXN+5];
char s[MAXN+5];
bool check(){
	int mx=0,sum=cnt[0];
	for(int i=1;i<26;++i){sum+=cnt[i];if(cnt[i]>cnt[mx])mx=i;}
	return 2*cnt[mx]<sum;
}
int main() {
	int T;cin>>T;while(T--){
		cin>>(s+1);n=strlen(s+1);
		m=0;
		for(int i=2;i<=n;++i)if(s[i]==s[i-1])a[++m]=s[i]-'a',p[m]=i;
		int sum=n;
		for(int i=0;i<26;++i)cnt[i]=0;
		for(int i=1;i<=m;++i)cnt[a[i]]++;
		vector<pii>ans;top=0;
		for(int i=1,lazy=0;i<=m;++i){
			if(!check()||!top||sta[top].fi==a[i])sta[++top]=mk(a[i],p[i]-lazy);
			else{
				ans.pb(mk(sta[top].se,p[i]-1-lazy));
				sum-=(p[i]-1-lazy)-sta[top].se+1;
				lazy+=(p[i]-1-lazy)-sta[top].se+1;
				cnt[sta[top].fi]--;
				cnt[a[i]]--;
				top--;
			}
		}
		if(sum){
			//for(int i=1;i<=top;++i)cout<<sta[i].fi<<" ";cout<<endl;
			m=top;top=0;
			for(int i=1;i<=m;++i)a[i]=sta[i].fi,p[i]=sta[i].se;
			int mx=0;
			for(int i=1;i<26;++i)if(cnt[i]>cnt[mx])mx=i;
			for(int i=1,lazy=0;i<=m;++i){
				if(top&&(a[i]==mx)+(sta[top].fi==mx)==1){
					ans.pb(mk(sta[top].se,p[i]-1-lazy));
					sum-=(p[i]-1-lazy)-sta[top].se+1;
					lazy+=(p[i]-1-lazy)-sta[top].se+1;
					top--;
				}
				else{
					sta[++top]=mk(a[i],p[i]-lazy);
				}
			}
			for(int i=1,lazy=0;i<=top;++i){
				assert(sta[i].fi==mx);
				ans.pb(mk(sta[i].se-lazy,sta[i].se-lazy));
				sum--;
				lazy++;
			}
			if(sum>0){
				ans.pb(mk(1,sum));
			}
		}
		cout<<SZ(ans)<<endl;
		for(int i=0;i<SZ(ans);++i)cout<<ans[i].fi<<" "<<ans[i].se<<endl;
	}
	return 0;
}
posted @ 2020-04-14 23:17  duyiblue  阅读(451)  评论(1编辑  收藏  举报