CodeForces 1383E - Strange Operation


就没啥好说的了……u1s1 使我做出来的题目组成的序列不是所有题目组成的序列的前缀的比赛都不是好比赛(暴论

比赛结束前40min一直在肝这个E……结果没肝出来……然而还是在赛后三天独立肝出来了……

洛谷题目页面传送门 & CF题目页面传送门

题意见洛谷/xyx(翻译有彩蛋)

首先把操作转化一下:若\(a_i\)\(a_{i+1}\)中有至少一个\(\texttt1\),则留下来一个\(\texttt1\)删掉另一个,否则任意删掉一个\(\texttt0\)。可以理解为,相邻两个字符打架,\(\texttt1\)是要比\(\texttt0\)厉害的,一定能干掉\(\texttt0\)

不难想到,可以将\(a\)分成若干个相等段,这样就是一段\(\texttt1\),一段\(\texttt0\)(设第\(i\)段是\(v_i\)\(c_i\))……然后就是一脸DP的样子。

考虑设\(dp_i\)表示第\(i\)个相等段一直到最后组成的01串,通过若干次操作,并且保持第一个字符不变的情况下,能产生多少个不同的01串。边界:\(dp_{m+1}=1\),其中\(m\)为相等段的个数;目标:\(\begin{cases}dp_1&m=1\lor c_1=\texttt1\\dp_1+dp_2&\text{otherwise}\end{cases}\)。然后倒着DP。这个状态对于老司机来说设计的还是挺自然的。此题的难点在于转移,会把人脑子烧坏。

分两种情况:

  1. \(i\)段为\(\texttt0\)。不难发现:若第\(i\)段后面有段的话,第\(i\)段与第\(i+2\)往后的段是不能够发生关系的(互相独立),因为,中间第\(i+1\)\(\texttt1\)是怎么也删不光的,于是\(dp_{i}=v_idp_{i+1}\);而后面没有段的话,稍微想一下就会发现也满足上面那个方程;

  2. \(i\)段为\(1\)。此时第\(i\)段和第\(i+2\)段往后是可能发生关系的,因为有可能把中间那段\(\texttt0\)全部干掉来实现前面和后面的合并。那么我们枚举的01串的开头的\(\texttt1\)的数量\(x\)。对于\(x\),贪心的思想告诉我们,尽可能地用前面的\(\texttt1\)凑是最好的,因为这样的话需要把从左往右数第\(x\)\(\texttt1\)所在的段及前面的\(\texttt1\)段(达到了要合并的段数的下限)全部合并起来,然后就只有后面是不确定的,加上后面的DP值,而后面的串越长,DP值肯定就越大(方案越全)。接下来仔细考虑这种情况如何转移。

    对于每个\(x\),设从\(i\)开始从左往右数第\(x\)\(\texttt1\)所在的段为\(j\),考虑从第\(2\)段开始后面的情况:

    1. 后面啥也没有了。那么只有\(1\)种情况;
    2. 后面只有一个\(\texttt0\)段,这个是在\(dp_{j+1}\)里没有考虑到的,因为在\(dp_{j+1}\)里是不可能把后面的\(\texttt1\)全删掉的,而考虑\(dp_i\)的时候可以,方案最全的方法是把所有的\(\texttt1\)段全部合并并删的只剩\(x\)个,中间的\(\texttt0\)干掉,只留最后一部分\(\texttt0\)。由于最后一部分也可以随意删,那么有\([c_m=\texttt0]v_m\)
    3. 后面有至少\(2\)段。那么若\(j+1<m\)直接套\(dp_{j+1}\)

    综上,\(dp_i=\sum\limits_{j=i}^m[c_j=\texttt1]v_j(1+[c_m=\texttt0]v_m+[j+1<m]dp_{j+1})\)。这个方程直接算会T,优化很容易,two-pointers即可。

然后就会发现样例4过不去。然后就发现,前面那个贪心的思想假掉了……一个反例是:\(a=\texttt{101001}\),在DP到\(dp_1\)时,\(x=1\)的情况下,考虑不到\(\texttt{1001}\),而实际上是有的。问题出现在哪儿?第\(2\)段的\(\texttt0\)的数量被上限\(v_{j+1}\)限住了。不过这个假是可以解决的,不像某些辛辛苦苦写了几个小时发现救不了……………………

考虑再枚举第\(2\)段的\(\texttt0\)的个数\(y\),由于\(\texttt0\)是不可合并的,我们要找的是\(j\)后面最前面(这个还是基于贪心的思想)的\(c_k=\texttt0,v_k\geq y\)的第\(k\)段(要把中间的\(\texttt1\)全部合并,\(\texttt0\)全部干掉),而不是\(k=j+1\)。对于每个\(y\),都有一个贡献值,于是维护这个基于\(y\)的贡献值序列,每次更完一个\(\texttt0\)段的DP值就修改这个序列,然后计算\(\texttt1\)段的DP值时就全体求和。一个暴力的想法是直接大力线段树区间赋值,\(\mathrm O(n\log n)\)。然而其实只需要维护一个栈即可,每个元素是一个(区间,值)的二元组,由于每个赋值操作最多会被弹出一次,\(\mathrm O(n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define X first
#define Y second
#define mp make_pair
const int mod=1000000007;
const int N=1000000;
int n;
char a[N+5]; 
vector<pair<int,bool> > v;//相等段序列 
int dp[N+1];
pair<pair<int,int>,int> stk[N];int top;//栈 
int main(){
	scanf("%s",a+1);
	n=strlen(a+1);
	for(int i=1,ie;i<=n;i=ie+1){//预处理v 
		ie=i;while(ie+1<=n&&a[ie+1]==a[i])ie++;
		v.pb(mp(ie-i+1,a[i]^48));
	}
//	reverse(v.begin(),v.end());
	int sum=0,now=0,las=!v.back().Y*v.back().X;
	dp[v.size()]=1;//边界 
	for(int i=v.size()-1;~i;i--){//转移 
		if(v[i].Y==0){
			dp[i]=1ll*v[i].X*dp[i+1]%mod;
			while(top&&stk[top-1].X.Y<=v[i].X)(now-=1ll*(stk[top-1].X.Y-stk[top-1].X.X+1)*stk[top-1].Y%mod)%=mod,top--; 
			if(top)(now-=1ll*(v[i].X+1-stk[top-1].X.X)*stk[top-1].Y%mod)%=mod,stk[top-1].X.X=v[i].X+1;//维护栈 
			stk[top++]=mp(mp(1,v[i].X),(i<v.size()-1)*dp[i+1]),(now+=(i<v.size()-1)*dp[i])%=mod;
//			for(int j=0;j<top;j++)printf("[%d,%d]:%d ",stk[j].X.X,stk[j].X.Y,stk[j].Y);puts("");
		}
		else
			dp[i]=(sum+=1ll*v[i].X*(1+las+now)%mod)%=mod;
//		cout<<dp[i]<<"\n";
	}
	if(v.size()==1)cout<<(dp[0]+mod)%mod;
	else if(v[0].Y==1)cout<<(dp[0]+mod)%mod;
	else cout<<((dp[0]+dp[1])%mod+mod)%mod;//目标 
	return 0;
} 
posted @ 2020-07-27 22:37  ycx060617  阅读(391)  评论(2编辑  收藏  举报