题目
题目描述
给两个小写字母串
A
,
B
A,B
A,B ,请你计算这四类字符串的最小长度:
- 非 B B B 子串的 A A A 的子串
- 非 B B B 子序列的 A A A 的子串
- 非 B B B 子串的 A A A 的子序列
- 非 B B B 子序列 A A A 的子序列
数据范围与提示
n
=
m
a
x
(
∣
A
∣
,
∣
B
∣
)
≤
2000
n=max(|A|,|B|)\le 2000
n=max(∣A∣,∣B∣)≤2000 。这里
n
n
n 只是为了后面写复杂度好看一点。
思路
P A R T 1 \tt PART \;1 PART1
一种做法是,二分答案,把 ∣ A ∣ |A| ∣A∣ 个该长度的子串取出来(利用哈希),然后在 B B B 里查是否存在。复杂度 O ( n log 2 n ) \mathcal O(n\log^2n) O(nlog2n) 。(可行性源于,若长度为 L L L 的 A A A 的子串已经不存在,那么 L + 1 L+1 L+1 长度必然也不存在。)
还有另一种做法,就是利用
S
A
M
\tt SAM
SAM 去掉不可行的子串。或许好打一点。
忽然想到第三种,最简单的暴力做法:把 A A A 的所有子串拿去做匹配……
P A R T 2 \tt PART\;2 PART2
只要选择的是 A A A 的子串都很好办——数量很有限。考虑用类似 d p \tt dp dp 的方式优化枚举就行。
f ( l , r ) f(l,r) f(l,r) 表示 A A A 从 l l l 到 r r r 的子串,要匹配到 B B B 的哪个前缀的子序列才行。在 B B B 尾端加入无穷多个通配符。那么有转移
f ( l , r ) = min t > f ( l , r − 1 ) B t = A r t f(l,r)=\min_{t>f(l,r-1)}^{B_{t}=A_{r}}t f(l,r)=t>f(l,r−1)minBt=Art
显然 f ( l , r ) f(l,r) f(l,r) 在 l l l 这一维上单调不增。先枚举 r r r ,而后从小到大枚举 l l l ,就可实现 O ( 1 ) \mathcal O(1) O(1) 转移。
P A R T 3 \tt PART\;3 PART3
感觉更棘手了。因为序列我们维护不了。我们只会子串。咋整?将计就计,对 B B B 建立 S A M \tt SAM SAM 即可。
想一个暴力做法:把 A A A 的每个子序列拿去试着匹配 B B B 的子串。若是已经无法匹配了,那么最好就是以此处为结束点。所以,只有可匹配的才有价值。那么 S A M \tt SAM SAM 上每个点维护,匹配到此处的最短子序列。
转移的时候,对于一个 “可匹配” 的 y y y ,如果其 A x + 1 A_{x+1} Ax+1 儿子为空,那么 l e n y + 1 len_y+1 leny+1 为可行答案;否则打上可匹配 l e n y + 1 len_y+1 leny+1 标记。这里的 l e n len len 是指最短可匹配长度。
复杂度 O ( n 2 ) \mathcal O(n^2) O(n2) ,和 P A R T 2 \tt PART\;2 PART2 一样慢得很。
P A R T 4 \tt PART\;4 PART4
完了!我们根本不会处理子序列!我们只会 S A M \tt SAM SAM !天崩地裂,丘峦崩摧!
所以抛开自动机,直接 d p \tt dp dp 好了。用 f ( x , y ) f(x,y) f(x,y) 表示, A A A 的前 x x x 个字符中,有一个长度为 y y y 的子序列,使得 B B B 必须用前 f ( x , y ) f(x,y) f(x,y) 个字符才能完成匹配。(匹配的思想和 P A R T 3 \tt PART\;3 PART3 一致。)
转移就是,找到最小的 t t t 满足 t > f ( x , y ) t>f(x,y) t>f(x,y) 且 B t = A x + 1 B_{t}=A_{x+1} Bt=Ax+1 那么有
f ( x + 1 , y + 1 ) = max [ f ( x , y + 1 ) , t ] f(x+1,y+1)=\max[f(x,y+1),t] f(x+1,y+1)=max[f(x,y+1),t]
考虑到 f ( x , y ) f(x,y) f(x,y) 在两个维度上都递增,从小到大枚举 x x x 后,从大到小枚举 y y y 。这样 t t t 的取值范围就是越来越大的,就可以实现 O ( 1 ) \mathcal O(1) O(1) 转移了。
在 B B B 末尾加入无穷多通配符。答案就是让 f ( x , y ) f(x,y) f(x,y) 用到了通配符的 min y \min y miny 。
总结
其实四个部分并不是非常难,但是杂糅在一起容易把人心态搞崩。如果我不用写博客的方式来推导,可能也会花费不少时间。
容易受到定式思维误导。其实 S A M \tt SAM SAM 是配角;真正起决定性作用的是 d p \tt dp dp 。我们引入 S A M \tt SAM SAM 只是为了字符串匹配而已。用自动机节点表示匹配情况是常见的 d p \tt dp dp 操作了;学过 A C \mathcal{AC} AC 自动机的童鞋肯定深有体会。
代码
我的 s o l v e 1 ( ) \sf solve1() solve1() 写复杂了。建议读者自行更改。
#include <cstdio>
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(int x){
if(x > 9) writeint(x/10);
putchar((x-x/10*10)^48);
}
const int MaxN = 4005;
const int CharSiz = 26;
namespace SAM{
int ch[MaxN][CharSiz];
int fa[MaxN], len[MaxN];
int lst = 1, cntNode = 1;
void clear(){
memset(ch,0,MaxN*CharSiz<<2);
memset(fa,0,MaxN<<2);
memset(len,0,MaxN<<2);
lst = cntNode = 1;
}
void add(char c){
int p = lst; // last one
int now = lst = ++ cntNode;
len[now] = len[p]+1; // pre
for(; p && !ch[p][c]; p=fa[p])
ch[p][c] = now; // new string
if(!p) return void(fa[now] = 1);
int x = ch[p][c]; // maybe parent
if(len[x] == len[p]+1)
return void(fa[now] = x);
int y = ++ cntNode; // split x
memcpy(ch[y],ch[x],CharSiz<<2);
fa[y] = fa[x], fa[x] = y; // chain
len[y] = len[p]+1, fa[now] = y;
for(; ch[p][c]==x; p=fa[p])
ch[p][c] = y; // change son
}
void build(char ppl[],int llt){
clear(); // good habit
for(int i=0; i<llt; ++i)
add(ppl[i]);
}
} using namespace SAM;
char A[MaxN], B[MaxN]; // [0,26)
int lenA, lenB; // [0,len)
int f[2][MaxN]; // whatever
void solve1(){
build(A,lenA);
memset(f[0],0,MaxN<<2);
int x = 1, now = 0;
for(int i=0; i<lenB; ++i){
while(x && !ch[x][B[i]])
now = len[x = fa[x]];
x = ch[x][B[i]], ++ now;
if(!x) x = 1, now = 0;
for(int j=x; j; j=fa[j])
f[0][j] = max(f[0][j],now);
}
int ans = MaxN; // infty
for(int i=2; i<=cntNode; ++i){
int x = len[fa[i]]+1;
if(x < f[0][i]+1)
x = f[0][i]+1;
if(ans > x) ans = x;
}
if(ans != MaxN)
printf("%d\n",ans);
else puts("-1");
}
void solve2(){
int ans = MaxN;
memset(f,-1,2*MaxN<<2);
for(int r=0; r<lenA; ++r){
int fr = (r&1)^1;
int t = lenB, p = t-1;
for(int l=0; l<=r; ++l){
for(; f[fr][l]<p; --p)
if(B[p] == A[r])
t = p; // update
if((f[r&1][l] = t) == lenB)
ans = min(ans,r-l+1);
// printf("f[%d,%d] = %d\n",l,r,f[r&1][l]);
}
}
if(ans != MaxN)
printf("%d\n",ans);
else puts("-1");
}
void solve3(){
SAM::build(B,lenB);
memset(f,0x3f,2*MaxN<<2);
f[1][1] = 0; // initial
int ans = MaxN;
for(int i=0; i<lenA; ++i){
int fr = (i&1)^1;
for(int j=1,y; j<=cntNode; ++j)
if(!(y = ch[j][A[i]]))
ans = min(ans,f[fr][j]+1);
else if(f[i&1][y] > f[fr][j]+1)
f[i&1][y] = f[fr][j]+1;
for(int j=1; j<=cntNode; ++j){
f[i&1][j] = min(f[i&1][j],f[fr][j]);
f[fr][j] = 0x3f3f3f3f;
}
}
if(ans != MaxN)
printf("%d\n",ans);
else puts("-1");
}
void solve4(){
memset(f,-1,2*MaxN<<2); // no cost
for(int i=0; i<lenA; ++i){
int fr = (i&1)^1;
int t = lenB, p = t-1;
for(int j=i+1; j; --j){
for(; f[fr][j-1]<p; --p)
if(B[p] == A[i])
t = p; // update
f[i&1][j] = max(f[fr][j],t);
}
f[i&1][0] = -1; // of course
}
for(int i=1; i<=lenA; ++i)
if(f[(lenA-1)&1][i] == lenB){
printf("%d\n",i); return ;
}
puts("-1");
}
int main(){
scanf("%s %s",A,B);
lenA = strlen(A);
lenB = strlen(B);
for(int i=0; i<lenA; ++i)
A[i] -= 'a'; // hash
for(int i=0; i<lenB; ++i)
B[i] -= 'a'; // hash
solve1(); solve2();
solve3(); solve4();
return 0;
}