失配树

给定一个字符串\(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\)的长度.

\[1\leq p,q\leq |s|\leq10^6,1\leq m \leq 10^5,s_i\in [a,z] \]


失配树

  一看数据范围,这道题用简单普通的哈希或\(KMP\)肯定是写不了的,所以引入失配树.
  我们先来看一组样例:

\[\frac{a\space b\space a\space b\space a\space b}{a\space b\space a\space b\quad\space\space} \]

  假设这是我们找出的一个字符串的两个前缀。我们可以发现它们的最长公共\(border\)长度是\(3\).
  说到求\(border\),就需要用\(KMP\)了,既然两个字符串都是同一个串的前缀,那么其中一个(较短的)必定也是另一个(较长的)字符串的前缀,然后我们将其中较长的一个字符串的字符下标及其\(nxt\)数组列出来看看.

\[\begin{array}{c|lcr} 下标 & \text{nxt}\\ \hline 1 & 0\\ 2 & 0\\ 3 & 1\\ 4 & 2\\ 5 & 3\\ 6 & 4\\ \end{array} \]

  显然什么也看不出来,那把它建成一棵树:我们连边\((nxt[i],i),i\in[1,6]\).

graph TB 0((0))-->1((1)) 0-->2((2)) 1-->3((3)) 3-->5((5)) 2-->4((4)) 4-->6((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;
}
posted @ 2026-10-17 16:28  beiciqi  阅读(8)  评论(0)    收藏  举报