[AGC046C] Shift 题解
题目大意
给定一个01串\(S\)。每次操作可将一个1
移到它左边的任意一个0
的左边。求进行不多于给定整数\(k\)此操作所得的不同串个数(对\(998244353\)取模)。
数据范围:\(1\leq |S|\leq 300, 0\leq k\leq 10^9\)。
题解
记\(n=|S|\)。易知至多进行\(n\)次操作就可以达到所有能达到的状态,于是\(k\geq n\)时的答案和\(k=n\)是的答案是一样的,那么就可以把\(k\)的范围降到\(300\)了。
接着考虑用另一种方式表示一个01串:记\(cnt_i\)为有多少个1
的左边恰好有\(i\)个0
。容易证明,相同的cnt数组对应相同的01串,不同的cnt数组对应不同的01串。
然后考虑每一次操作对\(cnt\)数组的影响:选择两个位置\(i,j(i< j)\),使\(cnt_i\leftarrow cnt_i+1, cnt_j\leftarrow cnt_j-1\)(记此操作为\(j\rightarrow i\))。
于是就可以愉快地dp了:设\(f_{i,j,x}\)为总变化数为\(x\)(一次操作产生两次变化),共进行\(j\)次\([i+1,n]\rightarrow [0,i]\)的操作所产生的的不同方案数。那么转移方程为:
\[f_{i,j,x}=\sum_{0\leq y\leq x}f_{i-1,j-y,x-y}+\sum_{1\leq y\leq \min\{n-j,x,cnt_i\}}f_{i-1,j+y,x-y}
\]
其中前一部分为进行\(y\)次\([i+1,n]\rightarrow i\)操作所产生的贡献,后一部分为进行\(y\)次\(i\rightarrow [0,i-1]\)操作所产生的贡献。
注意用两个数组储存一下两个求和号,然后直接递推即可。时间复杂度\(O(n^3)\)。
代码(为实现方便,一些数组进行了平移处理):
#include<cstdio>
#include<cstring>
#include<algorithm>
#define P 998244353
#define N 305
char s[N];
int n,k;
int cnt[N];
int dp[N][N][N<<1],s1[N][N<<1],s2[N][N<<1],ans;
int main(){
scanf("%s%d",s+1,&k);
n=strlen(s+1);
k=std::min(k,n)<<1;
for(int i=1,tmp=0;i<=n;i++)
if(s[i]=='0')
tmp++;
else
cnt[tmp+1]++;
dp[0][0][0]=1;
for(int j=0;j<=n&&j<=k;j++)
s1[j+1][j+1]=1;
s2[1][1]=1;
for(int i=1;i<=n+1;i++){
for(int j=0;j<=n;j++)
for(int x=0;x<=k;x++){
dp[i][j][x]=s1[j+1][x+1];
int y=std::min(std::min(n-j,x),cnt[i]);
if(y)
dp[i][j][x]=((dp[i][j][x]+s2[j+2][x])%P+P-s2[j+2+y][x-y])%P;
}
for(int j=0;j<=n;j++)
for(int x=0;x<=k;x++)
s1[j+1][x+1]=(s1[j][x]+dp[i][j][x])%P;
for(int j=n;j>=0;j--)
for(int x=0;x<=k;x++)
s2[j+1][x+1]=(s2[j+2][x]+dp[i][j][x])%P;
}
for(int x=0;x<=k;x++)
ans=(ans+dp[n+1][0][x])%P;
printf("%d",ans);
}