失配树模板题 题解

给定一个字符串 \(s\),定义它的 \(k\) 前缀 \(pre_k\) 为字符串 \(s_{1\dots k}\),\(k\) 后缀 \(suf_k\) 为字符串 \(s_{|s|-k+1\dots |s|}\),其中 \(1\le k\le |s|\)。

定义 \(Border(s)\) 为对于 \(1\leq i\lt |s|\),满足 \(pre_i=suf_i\) 的字符串 \(pre_i\) 的集合。\(Border(s)\) 中的每个元素都称之为字符串 \(s\) 的 \(border\)。

有 \(m\) 组询问,每组询问给定 \(p,q\),求 \(s\) 的 \(p\) 前缀和 \(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-11-11 20:42  beiciqi  阅读(8)  评论(0)    收藏  举报