• 题解 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 nleqsum_{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_1leq0)。我们令(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
    ",sum);
    	}
    	return 0;
    }
    

    CF1329C Drazil Likes Heap

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

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

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

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

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

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

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

    时间复杂度(O(nlog 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]<<(" 
    "[i==cnt_ans]);
    		clr();
    	}
    	return 0;
    }
    

    CF1329D Dreamoon Likes Strings

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

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

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

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

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

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

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

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

    (c_xgeq 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;
    }
    
  • 相关阅读:
    细说 webpack 之流程篇
    git 撤销commit
    Git远程操作详解
    git Could not read from remote repository 解决
    Mysql 关键字及保留字
    使用 Intellij Idea 导出JavaDoc
    【树莓派】盒子常见问题处理基础帮助
    【树莓派】crontab设置Linux设备定时重启
    【医疗行业】关于dcm4che DICOM Toolkit:C-Move与C-Get
    关于操作系统:eos、deepin
  • 原文地址:https://www.cnblogs.com/dysyn1314/p/12702160.html
Copyright © 2020-2023  润新知