失配树
给定一个字符串\(s\),定义它的\(k\)前缀\(pre_k\)为字符串\(s_{1\dots k}\),\(k\)后缀\(suf_k\)为字符串\(s_{|s|-k+1\dots |s|}\),其中\(1\le k\le |s|\).
定义\(Border(s)\)为对于\(i\in [1, |s|)\),满足\(pre_i=suf_i\)的字符串\(pre_i\)的集合。\(Border(s)\)中的每个元素都称之为字符串\(s\)的\(border\)。
有\(m\)组询问,每组询问给定\(p,q\),求\(s\)的\(\boldsymbol{p}\)前缀和\(\boldsymbol{q}\)前缀的最长公共\(border\)的长度.
失配树
一看数据范围,这道题用简单普通的哈希或\(KMP\)肯定是写不了的,所以引入失配树.
我们先来看一组样例:
假设这是我们找出的一个字符串的两个前缀。我们可以发现它们的最长公共\(border\)长度是\(3\).
说到求\(border\),就需要用\(KMP\)了,既然两个字符串都是同一个串的前缀,那么其中一个(较短的)必定也是另一个(较长的)字符串的前缀,然后我们将其中较长的一个字符串的字符下标及其\(nxt\)数组列出来看看.
显然什么也看不出来,那把它建成一棵树:我们连边\((nxt[i],i),i\in[1,6]\).
长度为\(4\)和长度为\(6\)的后缀的最长公共\(border\)长为\(2\),在树上的关系是\(2\)是\(4\)和\(6\)除了自己之外的\(LCA\).
原理:
如果\(C\)是\(B\)的\(border\),\(B\)是\(A\)的\(border\),那么\(C\)是\(A\)的\(border\). 也就是说处理出\(nxt\)数组之后,\(A\)可以不断跳\(nxt\)到\(B\),\(B\)也可以不断跳\(nxt\)到\(C\). 所以说,如果两个前缀能通过跳\(nxt\)跳到同一个位置去,那么第一个跳到的相同的位置就是它们的最长公共\(border\)长度. 这个过程和我们树上找\(LCA\)的方式很像,所以我们可以将其建成一棵树,这棵树也就是所谓的失配树.
通过对于一个字符串建出的失配树,我们可以快速找出其多组长度不同的前缀的最长公共\(border\)长度.
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn=1e6+5;
ll n,m,p,q,nxt[maxn],dep[maxn],f[maxn][21];
char s[maxn];
vector<ll> e[maxn];
void dfs(ll x,ll fa){
dep[x]=dep[fa]+1;
f[x][0]=fa;
for(ll i=1;i<=20;i++){
f[x][i]=f[f[x][i-1]][i-1];
}
for(auto y:e[x]){
dfs(y,x);
}
}
ll lca(ll u,ll v){
if(dep[u]<dep[v]){
swap(u,v);
}
for(ll i=20;i>=0;i--){
if(dep[f[u][i]]>=dep[v]){
u=f[u][i];
}
}
// if(u==v){
// return u;
// }
// 因为要查除了u、v自己之外的LCA,所以上面三行不能要
for(ll i=20;i>=0;i--){
if(f[u][i]!=f[v][i]){
u=f[u][i];
v=f[v][i];
}
}
return f[u][0];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>(s+1)>>m;
n=strlen(s+1);
for(ll i=2,j=0;i<=n;i++){
while(j&&s[i]!=s[j+1]){
j=nxt[j];
}
if(s[i]==s[j+1]){
j++;
}
nxt[i]=j;
}
for(ll i=1;i<=n;i++){
e[nxt[i]+1].push_back(i+1);//为了方便,建树时所有节点编号+1,这样就没有0号节点了
}
dfs(1,0);//dfs时从1号节点开始
while(m--){
cin>>p>>q;
cout<<lca(p+1,q+1)-1<<"\n";//因为建树是每个节点都+1,所以查询p+1、q+1,查完后再-1
}
return 0;
}

浙公网安备 33010602011771号