[算法] 前缀函数与KMP算法

前缀函数

前缀函数 n x t [ i ] nxt[i] nxt[i]定义为 子串 s [ 1 ... i ] s[1\dots i] s[1...i]最长的相等的真前缀与真后缀的长度。

计算前缀函数

cpp 复制代码
	scanf("%s",b+1);
	lb=strlen(b+1);
	int j=0;nxt[1]=0;
	for(int i=2;i<=lb;i++){
		while(j&&b[j+1]!=b[i]) j=nxt[j];
		if(b[j+1]==b[i]) j++;
		nxt[i]=j;
	}

在线算法,时间复杂度 O ( n ) O(n) O(n)

应用

[KMP算法] 给定一个模式串和一个待匹配串,找出前者在后者中的所有位置

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000010;
char a[maxn],b[maxn];
int la,lb,next[maxn];
int main(){
	scanf("%s%s",a+1,b+1);
	la=strlen(a+1);
	lb=strlen(b+1);
	int j=0;
	for(int i=2;i<=lb;i++){
		while(j&&b[j+1]!=b[i]) j=next[j];
		if(b[j+1]==b[i]) j++;
		next[i]=j;
	}
	j=0;
	for(int i=1;i<=la;i++){
		while(j&&b[j+1]!=a[i]) j=next[j];
		if(b[j+1]==a[i]) j++;
		if(j==lb){
			printf("%d\n",i-lb+1);
			j=next[j];
		}
	}
	for(int i=1;i<=lb;i++){
		printf("%d ",next[i]);
	}
	return 0;
}
cpp 复制代码
//把模式串与待匹配串接在一起,一次解决
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000010;
char a[maxn],b[maxn],c[maxn<<1];
int la,lb,lc,nxt[maxn<<1];
int main(){
	scanf("%s%s",a+1,b+1);
	la=strlen(a+1);
	lb=strlen(b+1);
	lc=0;
	for(int i=1;i<=lb;i++) c[++lc]=b[i];//b是模式串 
	c[++lc]='#';
	for(int i=1;i<=la;i++) c[++lc]=a[i];//a是待匹配串 
	int j=0;nxt[1]=0;
	for(int i=2;i<=lc;i++){
		while(j&&c[j+1]!=c[i]) j=nxt[j];
		if(c[j+1]==c[i]) j++;
		nxt[i]=j;
		if(i>lb+1){
			if(nxt[i]==lb) printf("%d\n",(i-lb-1)-lb+1);
		} 
	}
	for(int i=1;i<=lb;i++){
		printf("%d ",nxt[i]);
	}
	return 0;
}

找字符串的所有周期

字符串的周期:对字符串 s s s 和 0 < p ≤ ∣ s ∣ 0 < p \le |s| 0<p≤∣s∣,若 s [ i ] = s [ i + p ] s[i] = s[i+p] s[i]=s[i+p] 对所有 i ∈ [ 1 , ∣ s ∣ − p ] i \in [1, |s| - p] i∈[1,∣s∣−p] 成立,则称 p p p 是 s s s 的周期。

字符串的 b o r d e r border border:对字符串 s s s 和 0 ≤ r < ∣ s ∣ 0 \le r < |s| 0≤r<∣s∣,若 s s s 长度为 r r r 的前缀和长度为 r r r 的后缀相等,就称 s s s 长度为 r r r 的前缀是 s s s 的 b o r d e r border border。

由 s s s 有长度为 r r r 的 b o r d e r border border 可以推导出 ∣ s ∣ − r |s|-r ∣s∣−r 是 s s s 的周期。

由 n x t [ i ] nxt[i] nxt[i] ,可以得到 s s s 所有的 b o r d e r border border 长度,即 n x t [ n ] , n x t [ n x t [ n ] ] , ... nxt[n],nxt[nxt[n]], \ldots nxt[n],nxt[nxt[n]],...,由此可以得出 s s s 所有的周期。

其中最小正周期为 ∣ s ∣ − n x t [ n ] |s|-nxt[n] ∣s∣−nxt[n]。

统计模式串每个前缀的出现次数

问题一: 统计每个前缀 s [ 1 ... i ] s[1 \dots i] s[1...i] 在同一个字符串 s s s 的出现次数

以位置 i i i 为右端点,有长度为 n x t [ i ] nxt[i] nxt[i] 的前缀,有长度为 n x t [ n x t [ i ] ] nxt[nxt[i]] nxt[nxt[i]] 的前缀,有长度为 n x t [ n x t [ n x t [ i ] ] ] nxt[nxt[nxt[i]]] nxt[nxt[nxt[i]]] 的前缀,等等,直到长度变为 0。

cpp 复制代码
for(int i=1;i<=n;i++) cnt[nxt[i]]++;
for(int i=n;i>=1;i--) cnt[nxt[i]]+=cnt[i];
for(int i=1;i<=n;i++) cnt[i]++;//加上前缀自身

问题二: 统计每个前缀 s [ 1 ... i ] s[1 \dots i] s[1...i] 在另一个字符串 t t t 的出现次数

构造 s [ 1 ... n ] s[1\dots n] s[1...n]# t [ 1 ... m ] t[1\dots m] t[1...m]

cpp 复制代码
	scanf("%s%s",a+1,b+1);
	la=strlen(a+1);
	lb=strlen(b+1);
	lc=0;
	for(int i=1;i<=lb;i++) c[++lc]=b[i];//b是模式串 
	c[++lc]='#';
	for(int i=1;i<=la;i++) c[++lc]=a[i];//a是待匹配串 
	int j=0;nxt[1]=0;
	for(int i=2;i<=lc;i++){
		while(j&&c[j+1]!=c[i]) j=nxt[j];
		if(c[j+1]==c[i]) j++;
		nxt[i]=j;
	}
	for(int i=lb+2;i<=lc;i++) cnt[nxt[i]]++;
	for(int i=lb;i>=1;i--) cnt[nxt[i]]+=cnt[i];
	for(int i=1;i<=lb;i++) 
		printf("%d ",cnt[i]);

一个字符串中本质不同子串的数目

令 k k k 为当前 s s s 的本质不同子串数量,当前 s s s 的长度为 n n n 。

我们添加一个新的字符 c c c 至 s s s 末尾。

显然,会有一些新的子串以字符 c c c 结尾。我们希望对这些以该字符结尾且我们之前未曾遇到的子串计数。

构造字符串 t = s [ 1 ... n ] c t = s[1\dots n] c t=s[1...n]c 并将其反转得到字符串 t ∼ t^{\sim} t∼。

现在我们的任务变为计算有多少 t ∼ t^{\sim} t∼ 的前缀未在 t ∼ t^{\sim} t∼ 的其余任何地方出现。

如果我们计算了 t ∼ t^{\sim} t∼ 的前缀函数最大值 n x t max ⁡ nxt_{\max} nxtmax,那么出现在 s ∼ s^{\sim} s∼ 中的 t ∼ t^{\sim} t∼ 前缀的最长长度为 n x t max ⁡ nxt_{\max} nxtmax。自然的,所有更短的前缀也出现了。

因此,当添加了一个新字符后新出现的子串数目为 ∣ s ∣ + 1 − n x t max ⁡ |s| + 1 - nxt_{\max} ∣s∣+1−nxtmax。

所以对于每个添加的字符,我们可以在 O ( n ) O(n) O(n) 的时间内计算新子串的数目,故最终复杂度为 O ( n 2 ) O(n^2) O(n2)。

值得注意的是,我们也可以重新计算在头部添加一个字符,或者从尾或者头移除一个字符时的本质不同子串数目。

字符串压缩

给定一个长度为 n n n 的字符串 s s s,我们希望找到其最短的「压缩」表示,也即我们希望寻找一个最短的字符串 t t t,使得 s s s 可以被 t t t 的一份或多份拷贝的拼接表示。

让我们计算 s s s 的前缀函数。通过使用该函数的最后一个值 n x t [ n − 1 ] nxt[n-1] nxt[n−1],我们定义值 k = n − n x t [ n − 1 ] k = n - nxt[n-1] k=n−nxt[n−1]。我们将证明,如果 k k k 整除 n n n,那么 k k k 就是答案,否则不存在一个有效的压缩,故答案为 n n n。

假定 n n n 可被 k k k 整除。那么字符串可被划分为长度为 k k k 的若干块。根据前缀函数的定义,该字符串长度为 n − k n - k n−k 的前缀等于其后缀。但是这意味着最后一个块同倒数第二个块相等,并且倒数第二个块同倒数第三个块相等,等等。作为其结果,所有块都是相等的,因此我们可以将字符串 s s s 压缩至长度 k k k。

诚然,我们仍需证明该值为最优解。实际上,如果有一个比 k k k 更小的压缩表示,那么前缀函数的最后一个值 n x t [ n − 1 ] nxt[n-1] nxt[n−1] 必定比 n − k n - k n−k 要大。因此 k k k 就是答案。
现在假设 n n n 不可以被 k 整除,我们将通过反证法证明这意味着答案为 n n n。假设其最小压缩表示 r r r 的长度为 p p p( p p p 整除 n n n),字符串 s s s 被划分为 n / p ≥ 2 n / p \ge 2 n/p≥2 块。那么前缀函数的最后一个值 n x t [ n − 1 ] nxt[n-1] nxt[n−1] 必定大于 n − p n - p n−p(如果等于则 n n n 可被 k k k 整除),也即其所表示的后缀将部分的覆盖第一个块。现在考虑字符串的第二个块。该块有两种解释:第一种为 r 0 r 1 ... r p − 1 r_0 r_1 \dots r_{p-1} r0r1...rp−1,另一种为 r p − k r p − k + 1 ... r p − 1 r 0 r 1 ... r p − k − 1 r_{p-k} r_{p-k+1} \dots r_{p-1} r_0 r_1 \dots r_{p-k-1} rp−krp−k+1...rp−1r0r1...rp−k−1。由于两种解释对应同一个字符串,因此可得到 p p p 个方程组成的方程组,该方程组可简写为 r ( i + k )   m o d   p = r i   m o d   p r_{(i + k) \bmod p} = r_{i \bmod p} r(i+k)modp=rimodp。
r 0 r 1 r 2 ... r k − 1 r k ... r p − 1 r 0 r 1 r 2 ... r p − 1 − k ⏞ n x t [ n − 1 ] r p − k ... r p − 1 r 0 r 1 ... r p − 1 − k r p − k ... r p − 1 r 0 r 1 r 2 ... r p − 1 − k ⏟ p r p − k ... r p − 1 r 0 r 1 r 2 ... r k − 1 r k ... r p − 1 r 0 r 1 r 2 ... r p − 1 − k r p − k ... r p − 1 ⏟ n x t [ n − 1 ] \begin{gathered} \overbrace{r_0 ~ r_1 ~ r_2 ~ \dots ~ r_{k-1}~r_k~\dots~r_{p-1}r_0 ~ r_1 ~ r_2 ~ \dots ~ r_{p-1-k}}^{nxt[n-1]}~r_{p-k}~\dots~r_{p-1}\\ r_0 ~ r_1 ~ \dots ~ r_{p-1-k}~\underbrace{r_{p-k}~\dots~r_{p-1}r_0 ~ r_1 ~ r_2 ~ \dots ~ r_{p-1-k}}{p}~r{p-k}~\dots~r_{p-1}\\ r_0 ~ r_1 ~ r_2 ~ \dots ~ r_{k-1}~\underbrace{r_k~\dots~r_{p-1}r_0 ~ r_1 ~ r_2 ~ \dots ~ r_{p-1-k}~r_{p-k}~\dots~r_{p-1}}_{nxt[n-1]}\\ \end{gathered} r0 r1 r2 ... rk−1 rk ... rp−1r0 r1 r2 ... rp−1−k nxt[n−1] rp−k ... rp−1r0 r1 ... rp−1−k p rp−k ... rp−1r0 r1 r2 ... rp−1−k rp−k ... rp−1r0 r1 r2 ... rk−1 nxt[n−1] rk ... rp−1r0 r1 r2 ... rp−1−k rp−k ... rp−1

根据扩展欧几里得算法我们可以得到一组 x x x 和 y y y 使得 x k + y p = gcd ⁡ ( k , p ) xk + yp = \gcd(k, p) xk+yp=gcd(k,p)。通过与等式 p k − k p = 0 pk - kp = 0 pk−kp=0 适当叠加我们可以得到一组 x ′ > 0 和 y ′ < 0 x' > 0 和 y' < 0 x′>0和y′<0 使得 x ′ k + y ′ p = gcd ⁡ ( k , p ) x'k + y'p = \gcd(k, p) x′k+y′p=gcd(k,p)。这意味着通过不断应用前述方程组中的方程我们可以得到新的方程组 r ( i + gcd ⁡ ( k , p ) )   m o d   p = r i   m o d   p r_{(i + \gcd(k, p)) \bmod p} = r_{i \bmod p} r(i+gcd(k,p))modp=rimodp。

由于 gcd ⁡ ( k , p ) \gcd(k, p) gcd(k,p) 整除 p p p,这意味着 gcd ⁡ ( k , p ) \gcd(k, p) gcd(k,p) 是 r r r 的一个周期。又因为 n x t [ n − 1 ] > n − p nxt[n - 1] > n - p nxt[n−1]>n−p,故有 n − n x t [ n − 1 ] = k < p n - nxt[n - 1] = k < p n−nxt[n−1]=k<p,所以 gcd ⁡ ( k , p ) \gcd(k, p) gcd(k,p) 是一个比 p p p 更小的 r r r 的周期。因此字符串 s s s 有一个长度为 gcd ⁡ ( k , p ) < p \gcd(k, p) < p gcd(k,p)<p 的压缩表示,同 p p p 的最小性矛盾。

相关推荐
慢慢来_40 分钟前
【力扣热题100】[Java版] 刷题笔记-448. 找到所有数组中消失的数字
笔记·算法·leetcode
橘子遇见BUG3 小时前
算法日记 33 day 动态规划(打家劫舍,股票买卖)
算法·动态规划
格雷亚赛克斯3 小时前
黑马——c语言零基础p139-p145
c语言·数据结构·算法
南宫生3 小时前
力扣-位运算-3【算法学习day.43】
学习·算法·leetcode
码农多耕地呗3 小时前
区间选点:贪心——acwing
算法
醉酒柴柴3 小时前
【代码pycharm】动手学深度学习v2-08 线性回归 + 基础优化算法
深度学习·算法·pycharm
财富探秘者3 小时前
贵州茅台[600519]行情数据接口
大数据·c语言·python·算法·金融·restful
至善迎风3 小时前
施密特正交化与单位化的情形
线性代数·算法·决策树·机器学习
HABuo4 小时前
【数据结构与算法】合并链表、链表分割、链表回文结构
c语言·开发语言·数据结构·c++·学习·算法·链表