题目大意
给定一个01串(S)。每次操作可将一个1
移到它左边的任意一个0
的左边。求进行不多于给定整数(k)此操作所得的不同串个数(对(998244353)取模)。
数据范围:(1leq |S|leq 300, 0leq kleq 10^9)。
题解
记(n=|S|)。易知至多进行(n)次操作就可以达到所有能达到的状态,于是(kgeq n)时的答案和(k=n)是的答案是一样的,那么就可以把(k)的范围降到(300)了。
接着考虑用另一种方式表示一个01串:记(cnt_i)为有多少个1
的左边恰好有(i)个0
。容易证明,相同的cnt数组对应相同的01串,不同的cnt数组对应不同的01串。
然后考虑每一次操作对(cnt)数组的影响:选择两个位置(i,j(i< j)),使(cnt_ileftarrow cnt_i+1, cnt_jleftarrow cnt_j-1)(记此操作为(j ightarrow i))。
于是就可以愉快地dp了:设(f_{i,j,x})为总变化数为(x)(一次操作产生两次变化),共进行(j)次([i+1,n] ightarrow [0,i])的操作所产生的的不同方案数。那么转移方程为:
[f_{i,j,x}=sum_{0leq yleq x}f_{i-1,j-y,x-y}+sum_{1leq yleq min{n-j,x,cnt_i}}f_{i-1,j+y,x-y}
]
其中前一部分为进行(y)次([i+1,n] ightarrow i)操作所产生的贡献,后一部分为进行(y)次(i ightarrow [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);
}