[BZOJ4032][HEOI2015]最短不公共子串(后缀自动机+序列自动机+DP)
题面
给两个小写字母串A,B,请你计算:
(1) A的一个最短的子串,它不是B的子串
(2) A的一个最短的子串,它不是B的子序列
(3) A的一个最短的子序列,它不是B的子串
(4) A的一个最短的子序列,它不是B的子序列
分析
先考虑(1)(3),对B建SAM
对于询问(1).直接枚举子串左端点。对于每个左端点,向右扫的同时在SAM上匹配,当第一次失配的时候更新答案并break。
对于询问(3).设(f_{i,j})为SAM节点(j)在A的前(i)位中匹配到的最长子序列长度。
那么显然有(f_{i,delta(j,A_i)}=max(f_{i-1,j}+1)(delta(j,A_i)
eq ext{NULL})),其中(delta)为自动机的转移函数。
最终答案为(f_{n,q_0}),其中(q_0)为SAM的起始状态,代表空串
考虑(2)(4)如何做。如果我们能构造出一个自动机,它能把询问串在某个串的所有子序列中匹配,那么就可以套用(1)(3)的方法。那么它的转移函数是根据子序列的,即(delta(x,c))表示(x)代表串后加一个字符(c)能匹配到的子序列。
那么就可以用到序列自动机。其实很多人都在不知不觉中用过这个方法。其实很简单,(delta(i,c))为:匹配到以(i)结尾的子序列的串,后加一个字符c能够匹配到的子序列位置。我们直接令 (delta(i,c))为位置(i)之后第一次出现字符(c)的位置即可。因为贪心来讲,跳到最先出现的位置会更优,相当于给后面更多可能选项。
如我们要在( exttt{aabacba})找到一个子序列与( ext{abc})匹配,那么跳的位置是1,3,5.
构建算法很简单,直接从后往前扫描一遍,维护每个字符上一次出现的位置即可。注意实现上为了区分根节点和空状态,我们要把点的编号增加一位,如状态3对应的是结尾在第二位的子序列,1表示空串,0表示NULL。
struct seqt {
//注意序列自动机的编号为实际位置+1
//1代表空串,2代表第1位,3代表第2位...
int ch[maxn+5][maxc+5];
int last[maxc+5];//存储每个字符上一次出现的位置
const int root=1;
inline int trans(int x,char c) {
return ch[x][c-'a'];
}
void build(char *s) {//无log做法
int len=strlen(s+1);
for(int i=len;i>=1;i--){
//现在更新位置i对应的节点i+1
for(int j=0;j<maxc;j++) ch[i+1][j]=last[j];
last[s[i]-'a']=i+1;
}
for(int j=0;j<maxc;j++) ch[1][j]=last[j];
}
} S;
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#define INF 0x3f3f3f3f
#define maxn 2000
#define maxc 26
using namespace std;
struct SAM {
#define len(x) (t[x].len)
#define link(x) (t[x].link)
struct node {
int ch[maxc];
int link;
int len;
} t[maxn*2+5];
const int root=1;
int last=root;
int ptr=1;
void extend(int c) {
int p=last,cur=++ptr;
len(cur)=len(p)+1;
while(t[p].ch[c]==0) {
t[p].ch[c]=cur;
p=link(p);
}
if(p==0) link(cur)=root;
else {
int q=t[p].ch[c];
if(len(p)+1==len(q)) link(cur)=q;
else {
int clo=++ptr;
link(clo)=link(q);
for(int i=0; i<maxc; i++) t[clo].ch[i]=t[q].ch[i];
len(clo)=len(p)+1;
link(q)=link(cur)=clo;
while(t[p].ch[c]==q) {
t[p].ch[c]=clo;
p=link(p);
}
}
}
last=cur;
}
void build(char *s) {
int len=strlen(s+1);
for(int i=1; i<=len; i++) extend(s[i]-'a');
}
inline int trans(int x,char c) {
return t[x].ch[c-'a'];
}
} T;
struct seqt {
//注意序列自动机的编号为实际位置+1
//1代表空串,2代表第1位,3代表第2位...
int ch[maxn+5][maxc+5];
int last[maxc+5];//存储每个字符上一次出现的位置
const int root=1;
inline int trans(int x,char c) {
return ch[x][c-'a'];
}
void build(char *s) {//无log做法
int len=strlen(s+1);
for(int i=len;i>=1;i--){
//现在更新位置i对应的节点i+1
for(int j=0;j<maxc;j++) ch[i+1][j]=last[j];
last[s[i]-'a']=i+1;
}
for(int j=0;j<maxc;j++) ch[1][j]=last[j];
}
} S;
int n,m;
char a[maxn+5],b[maxn+5];
int solve1() {
int ans=INF;
for(int i=1; i<=n; i++) {
int x=T.root;
for(int j=i; j<=n; j++) {
x=T.trans(x,a[j]);
if(x==0) {
ans=min(ans,j-i+1);
break;
}
}
}
if(ans==INF) ans=-1;
return ans;
}
int solve2() {
int ans=INF;
for(int i=1; i<=n; i++) {
int x=S.root;
for(int j=i; j<=n; j++) {
x=S.trans(x,a[j]);
if(x==0) {
ans=min(ans,j-i+1);
break;
}
}
}
if(ans==INF) ans=-1;
return ans;
}
int solve3() {
static int f[2][maxn*2+5];
//f[i][j]表示A串的前i位的子串中,能走到自动机上j节点的最短子串
//那么答案就是f[i][0](到空节点就是不匹配)
//第一维可以滚动数组
memset(f,0x3f,sizeof(f));
int now=0;
f[now][T.root]=0;
for(int i=1; i<=n; i++) {
now^=1;
f[now][T.root]=0;
for(int j=1; j<=T.ptr; j++) f[now][j]=f[now^1][j];
for(int j=1; j<=T.ptr; j++) {
int to=T.trans(j,a[i]);
f[now][to]=min(f[now][to],f[now^1][j]+1);
}
}
if(f[now][0]==INF) return -1;
else return f[now][0];
}
int solve4() {
static int f[2][maxn*2+5];
memset(f,0x3f,sizeof(f));
int now=0;
f[now][1]=0;
for(int i=1; i<=n; i++) {
now^=1;
f[now][1]=0;
for(int j=1; j<=m+1; j++) f[now][j]=f[now^1][j];
for(int j=1; j<=m+1; j++) {
int to=S.trans(j,a[i]);
f[now][to]=min(f[now][to],f[now^1][j]+1);
}
}
if(f[now][0]==INF) return -1;
else return f[now][0];
}
int main() {
scanf("%s",a+1);
scanf("%s",b+1);
n=strlen(a+1);
m=strlen(b+1);
T.build(b);
S.build(b);
printf("%d
",solve1());
printf("%d
",solve2());
printf("%d
",solve3());
printf("%d
",solve4());
}
/*
hack:
abaaa
aabaa
*/