这个题BST考场爆切然后教我的,%%%。
首先我们发现生成的数列一定是原序列的一个子序列,然后有要求本质不同的序列个数,这就让我们往子序列自动机上靠。我们先看一下对 (a_i,a_{i+1}) 操作的几种不同的后果:
[00 o 0\
01 o 1\
10 o 1\
11 o 1\
]
可以发现,只有有两个 (1) 的时候才有可能删掉一个 (1) ,所以我们构造如下的一个自动机:
- 字符集:(0,1)
- 状态集合:类似于子序列自动机,序列中每个状态代表一个位置
- 起始状态:第一个 (1) 的位置代表的状态
- 接受状态集合:所有 (1) 代表的状态均为接受状态,其余都不是
- 转移函数:设当前状态为 (zeta),则 ((zeta,1)) 为下一个 (1) 的位置,((zeta,0)) 为下一个 (len[x]=len[zeta]+1) 的状态 (x)(其中 (len[zeta]) 表示该状态代表的位置为结尾,连续的 (0) 的个数,这个转移函数是整个题目的精髓)。
比如一个序列 (1000110000),现在在位置 (4),想在这个后面加一个 (0),那么我们必须把位置 ([2,5]) 的数全部清掉,然后把位置 ([6,9]) 的数加进来才能达到目的(可以仔细想一想)。
我们现在对一个输入的序列,先把头尾的 (0) 全部删掉,最后累计进答案里(根据自动机的构造我们必须这样做),然后我们发现我们要计数的东西其实就是从初始节点开始,到任何一个接受状态的路径的个数,由于我们已经保证了任何一个最后可能生成的序列在自动机上跑的时候一定跑到它第一次出现的位置(类比子序列自动机),所以本质不同的要求我们也满足了。最后对自动机这个DAG跑一遍DP就可以了。
对这种要求本质不同的有条件的子序列个数的题,我们构建自动机时要注意:
- 自动机里的路径代表的子序列有且仅有题目要求的合法子序列
- 对于任意一个合法子序列,在自动机里跑一遍后必须是在它第一次出现的位置
- 与一般的子序列自动机不同,不一定每个状态都是接受状态
代码:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1000009,M=1000000007;
char s[N];
int n,ch[N][2],nxt_1[N],nxt_0[N],len_0[N],pos[N],f[N];
void add(int &x,int y)
{
x=x+y>=M?x+y-M:x+y;
}
void init()
{
scanf("%s",s+1);
n=strlen(s+1);
}
void work()
{
int L=0,R=0;
while(s[n]=='0')
R++,n--;
if(n==0)
{
printf("%d
",R);
return;
}
for (int i=1;i<=n;i++)
if(s[i]=='0') L++;
else break;
for (int i=1;i<=n-L;i++)
s[i]=s[i+L];
n-=L;
int last=n+1;
for (int i=n;i>=0;i--)
{
nxt_1[i]=last;
if(s[i]=='1') last=i;
}
for (int i=1;i<=n;i++)
len_0[i]=s[i]=='0'?len_0[i-1]+1:0;
for (int i=0;i<=n;i++)
pos[i]=n+1;
for (int i=n;i>=0;i--)
nxt_0[i]=pos[len_0[i]+1],
pos[len_0[i]]=i;
f[1]=1;
for (int i=1;i<=n;i++)
{
if(nxt_0[i]<=n) add(f[nxt_0[i]],f[i]);
if(nxt_1[i]<=n) add(f[nxt_1[i]],f[i]);
}
int ans=0;
for (int i=1;i<=n;i++)
if(s[i]=='1')
add(ans,f[i]);
printf("%lld
",1ll*ans*(1+L)%M*(1+R)%M);
}
int main()
{
init();
work();
return 0;
}