前缀函数(KMP 中的 Pi 数组)

我们用 \(s[l\dots r]\) 表示 \(s\) 中由下标 \(l \sim r\) 的字符组成的子串。

前缀函数,即 \(\pi_i\)。其含义是 \(s[1\dots i]\)最长 border 长度。若不存在则为 \(0\)

其中一个字符串的 border 表示它的某个(不等于原串的)子串,使得这个子串在原串的前缀和后缀都出现过。

显然一个字符串的 border 可能有多个,我们要求解的 \(\pi_i\) 是最长的一个(的长度)。


在这求解 \(\pi\) 数组之前,先考虑这样一个问题:如果已经求出了 \(\pi_1 \dots \pi_n\),如何用这些数表示出 \(s\) 的所有 border(的长度)?

首先 \(\pi_n\) 是一个,且是最长的一个。我们考虑顺次求解第二长,第三长等的 border。

如图。根据 \(\pi\) 数组的定义,我们可知图中 \(2\) 段红色所代表的子串是相同的,\(4\) 段蓝色所代表的子串也是相同的。

观察第一段蓝色和最后一段蓝色,发现这又是一个 \(s\) 的 border,且长度仅次于 \(\pi_n\)。因此 \(s\) 的第二长的 border(的长度)为 \(\pi_{\pi_n}\)

同理不难得出第三长的是 \(\pi_{\pi_{\pi_n}}\),第四长的是 \(\pi_{\pi_{\pi_{\pi_n}}}\),以此类推。当递归到 \(0\) 时就可以结束了。


考虑顺次求解 \(\pi_i\)。即在计算 \(\pi_i\) 前已经求解了 \(\pi_1,\pi_2\dots \pi_{i-1}\),我们希望用这些值计算 \(\pi_i\)

对于一个 \(s[1\dots i-1]\) 的 border 而言,不妨令其长度为 \(x\),其前缀和后缀的位置分别为 \([1,x]\)\([i-x,i-1]\)

如果能将这个 border 的长度增加 \(1\),即增加到 \([1,x+1]\)\([i-x,i]\),那么必须要满足 \(s_{x+1}=s_i\)。如果这个条件满足,就意味着 \(s[1,i]\) 存在一个 border 长度为 \(x + 1\)

我们可以用上面讲的方式枚举 \(x\),即枚举 \(s[1\dots i - 1]\) 的 border(的长度)。因为我们从大到小枚举,所以第一个满足 \(s_{x+1} = s_i\)\(x + 1\) 就是 \(s[1\dots i]\) 的最长 border,即 \(\pi_i\) 的值。否则,若一个这样的 \(x\) 也不存在,则 \(\pi_i = 0\)

求解 \(\pi_1,\pi_2\dots \pi_n\) 的代码:

int ne[N];		// ne[i] 就是 pi[i],即 s[1...i] 的最长 border(的长度)

ne[0] = ne[1] = 0;
for (int i = 2; i <= n; ++ i ) {
	int j = ne[i - 1];
	while (j && s[j + 1] != s[i]) j = ne[j];
	if (s[j + 1] == s[i]) j ++ ;
	ne[i] = j;
}
posted @ 2024-08-06 06:41  2huk  阅读(22)  评论(0编辑  收藏  举报