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