AtCoder panasonic2020_e - Three Substrings
洛谷题目页面传送门 & AtCoder题目页面传送门
给定\(3\)个字符串\(a,b,c,|a|=n,|b|=m,|c|=s\),求长度最小的字符串\(ans\),满足\(a,b,c\)都可以由\(ans\)的某个子串将其中某些字符替换成\(\texttt?\)得到。输出最小的长度。
\(n,m,s\in[1,2000]\)。
(以下认为\(n,m,s\)同阶,在复杂度中用\(n\)表示)
首先,思路很显然:可以枚举\(a,b\)在\(ans\)的相对距离,如果无冲突就考虑\(c\)在\(ans\)中与\(a,b\)相对的位置。\(a,b\)的相对距离的数量是\(\mathrm O(n)\),因为若\(a\)首\(b\)尾或\(a\)尾\(b\)首的距离超过\(s+1\),那便没了意义。
接下来考虑确定了\(a,b\)的相对距离之后,如何“考虑\(c\)在\(ans\)中与\(a,b\)相对的位置”。显然,一次最多能承受\(\mathrm O(n)\)。
找到所有\(c\)的无冲突位置,然后以最小的能包含\(a,b,c\)的\(|ans|\)更新答案。位置数显然是\(\mathrm O(n)\)的。关键问题在于如何“找到所有\(c\)的无冲突位置”。看似很简单?哈希?Z?KMP?抱歉,都不行。该死的通配符\(\texttt?\)!显然,有了通配符之后,可匹配函数是这样定义的:\(eq:eq(x,y)=[x=\texttt?\lor y=\texttt?\lor x=y]\),它并不满足传递性。而Z和KMP都利用了可匹配函数的传递性,所以跑不了。至于哈希,显然更跑不了了……bg,据说FFT可以解决带通配符的字符串模式匹配,而蒟蒻还没学过,而且那是\(\mathrm O(n\log n)\)的,你觉得能过么?
那怎么办呢?容易发现,字符串长度都很小,但是因为有\(\mathrm O(n)\)种相对位置,使得复杂度变高。我们可以充分利用“字符串长度都很小”这个特点,预处理一些东西。不难发现,\(c\)当前的位置无冲突,当且仅当它跟\(a\)无冲突且跟\(b\)无冲突。
以判断跟\(a\)有无冲突为例。显然,冲突只可能发生在\(a,c\)所覆盖的位置集合的交集里。于是我们考虑\(a,c\)覆盖的区间的关系,有以下几种:
- \(c\)左离\(a\),此时肯定无冲突;
- \(c\)右离\(a\),此时肯定无冲突;
- \(c\)左交\(a\),此时交集是\(a\)的前缀,共\(\mathrm O(n)\)种可能;
- \(c\)右交\(a\),此时交集是\(a\)的后缀,共\(\mathrm O(n)\)种可能;
- \(c\)包含\(a\),此时交集是\(c\)的长度为\(|a|\)的子区间,共\(\mathrm O(n)\)种可能;
- \(c\)包含于\(a\),此时交集是\(a\)的长度为\(|c|\)的子区间,共\(\mathrm O(n)\)种可能。
综上,共\(\mathrm O(n)\)种交集,对于每个交集都\(\mathrm O(n)\)暴力匹配预处理一下即可!
对\(b\)与\(c\)的冲突判断与\(a\)与\(c\)的冲突判断类似。
最后还有一个小调整:我们可以强行令\(c\)为长度最小的那个字符串,这样就可以排除掉第\(5\)种——\(c\)包含\(a\)啦!
最终时间复杂度为\(\mathrm O\!\left(n^2\right)\)
下面是AC代码:
#include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int N=2000;
int n,m,s;//3个字符串的长度
char a[N+5],b[N+5],c[N+5];//3个字符串
bool eq(char x,char y){return x=='?'||y=='?'||x==y;}//可匹配函数
bool pre_a[N+1]/*交集是a的前缀*/,rg_a[N+1]/*交集是a的子区间*/,suf_a[N+1]/*交集是a的后缀*/,pre_b[N+1],rg_b[N+1],suf_b[N+1]/*类似a*/;
void pre_rg_suf_init(int lenx,char x[],bool pre[],bool rg[],bool suf[]){//预处理
memset(pre,1,N+1);memset(rg,1,N+1);memset(suf,1,N+1);
for(int i=1;i<=s;i++)for(int j=1;j<=i;j++)/*暴力匹配*/pre[i]&=eq(x[j],c[j-i+s]);
for(int i=2;i+s-1<lenx;i++)for(int j=i;j<=i+s-1;j++)/*暴力匹配*/rg[i]&=eq(x[j],c[j-i+1]);
for(int i=lenx-s+1;i<=lenx;i++)for(int j=i;j<=lenx;j++)/*暴力匹配*/suf[i]&=eq(x[j],c[j-i+1]);
}
bool ok(int dif){//a与b的位置差为dif时是否无冲突
for(int i=1;i<=n;i++)if(1<=i-dif&&i-dif<=m&&!eq(a[i],b[i-dif]))/*冲突*/return false;
return true;
}
int sol(int dif){//a与b的位置差为dif时最小的|ans|
//设a的位置为1~n
int l=min(1,1+dif),r=max(n,m+dif);//能包含a,b的最小区间
int ans=inf;
for(int i=l-s;i<=r+1;i++)//枚举c开头的位置
if((i+s-1<1?true:i<=1?pre_a[i+s-1]:i+s-1<n?rg_a[i]:i<=n?suf_a[i]:true)&&//a,c无冲突
(i+s-1<1+dif?true:i<=1+dif?pre_b[i+s-1-dif]:i+s-1<m+dif?rg_b[i-dif]:i<=m+dif?suf_b[i-dif]:true)/*b,c无冲突*/)
ans=min(ans,max(r,i+s-1)-min(l,i)+1/*能包含a,b,c的最小区间长度*/);//更新答案
// cout<<dif<<" "<<ans<<"\n";
return ans;
}
int main(){
cin>>a+1>>b+1>>c+1;
n=strlen(a+1);m=strlen(b+1);s=strlen(c+1);
if(s>n){
for(int i=1;i<=s;i++)swap(a[i],c[i]);
swap(n,s);
}
if(s>m){
for(int i=1;i<=s;i++)swap(b[i],c[i]);
swap(m,s);
}//强行令c为最短的字符串
pre_rg_suf_init(n,a,pre_a,rg_a,suf_a);pre_rg_suf_init(m,b,pre_b,rg_b,suf_b);//预处理
int ans=inf;
for(int i=-(m+s);i<=n+s;i++)if(ok(i))ans=min(ans,sol(i));//求答案
cout<<ans;
return 0;
}