题解 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}\)。
考虑一次操作,可以分为两种:
- 选择\(s'\)中的一个字母,并删去。
- 选择\(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;
}