【BZOJ2806】熟悉的文章(CTSC2012)-广义SAM+二分+DP+单调队列
测试地址:熟悉的文章
做法:本题需要用到广义SAM+二分+DP+单调队列。
首先,的性质显然是单调的,所以我们二分。接下来容易想到DP,令为以第个字符结尾的前缀最多能有多少个字符被符合条件的子串覆盖,容易得到状态转移方程:
其中的转移表示我们不选择用一个子串覆盖一个后缀,而的转移需要保证到这一段为个模式串的一个子串,并且。
要判别一个串是不是个模式串的一个子串,显然使用好写又快的广义SAM,这样我们就可以在DP时顺便将字符串在SAM中匹配,并求出它最长的满足条件的后缀长度。注意满足要求的后缀长度不一定是对应节点能表示的最长的一个子串,因为每次匹配如果匹配上了,最长长度只会加,不一定会加满。令前个字符构成的前缀的满足条件的最长长度为,显然长度在之内的后缀都满足条件。因此上面DP方程的条件可以简写为:
也即:
注意到合法的转移构成了一个连续的区间,而这个区间的左右端点都是单调不递减的(一定不递减,因为若,有,根据定义明显知道不可能出现这种情况),所以可以使用单调队列优化DP。于是我们就完成了这一题,时间复杂度为。
以下是本人代码:
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int n,m,rt=1,last,tot=1;
int ch[2200010][2]={0},pre[2200010]={0},len[2200010]={0};
int q[1100010],f[1100010];
char s[1100010];
void make_clone(int c,int p,int q,int nq)
{
len[nq]=len[p]+1;
ch[nq][0]=ch[q][0];
ch[nq][1]=ch[q][1];
while(ch[p][c]==q) ch[p][c]=nq,p=pre[p];
pre[nq]=pre[q];
pre[q]=nq;
}
void extend(int c)
{
int p,q,np;
p=last;
if (ch[p][c])
{
if (len[ch[p][c]]==len[p]+1) last=ch[p][c];
else make_clone(c,p,ch[p][c],++tot),last=tot;
return;
}
np=++tot;
len[np]=len[p]+1;
while(!ch[p][c]) ch[p][c]=np,p=pre[p];
if (!p) pre[np]=rt;
else
{
q=ch[p][c];
if (len[q]==len[p]+1) pre[np]=q;
else make_clone(c,p,q,++tot),pre[np]=tot;
}
last=np;
}
bool check(int l,int n)
{
f[0]=0;
int h=1,t=0,now=rt,nowlen=0;
for(int i=1;i<=n;i++)
{
while(now&&!ch[now][s[i]-'0']) now=pre[now],nowlen=len[now];
if (!now) now=rt;
else now=ch[now][s[i]-'0'],nowlen++;
if (i>=l)
{
while(h<=t&&f[q[t]]-q[t]<=f[i-l]-(i-l)) t--;
q[++t]=i-l;
}
while(h<=t&&q[h]<i-nowlen) h++;
if (h>t) f[i]=f[i-1];
else f[i]=max(f[i-1],f[q[h]]+i-q[h]);
}
return 10*f[n]>=9*n;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%s",s);
int len=strlen(s);
last=rt;
for(int j=0;j<len;j++)
extend(s[j]-'0');
}
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);
s[0]='#';
int len=strlen(s)-1;
int l=0,r=len;
while(l<r)
{
int mid=(l+r)>>1;
if (check(mid+1,len)) l=mid+1;
else r=mid;
}
printf("%d\n",l);
}
return 0;
}