1. 定义
1.1 前缀 & 真前缀
前缀是指从串首开始到某个位置 i 结束的一个特殊子串。字符串 S 的以 i 结尾的前缀表示为
prefix(S,i)=S[0..i]
真前缀指除了 S 本身的 S 的前缀。
1.2 后缀 & 真后缀
后缀是指从某个位置 i 开始到整个串末尾结束的一个特殊子串。字符串 S 的从 i 开头的后缀表示为
suffix(S,i)=S[i..∣S∣−1]
真后缀指除了 S 本身的 S 的后缀。
1.3 前缀函数
给定一个长度为 n 的字符串 s,其前缀函数定义为一个长度为 n 的数组 π。其中 π[i] 含义为:
- 如果子串 s[0..i] 有相等的真前缀 s[0..kj−1] 和真后缀 s[i−(kj−1)..i],那么 π 为最大的相等的真前后缀长度,即
π[i]=max{kj}
- 如果子串 s[0..i] 没有相等的真前后缀,则
π[i]=0
1.4 字符串的周期
对于字符串 s 和 0<p≤∣s∣,若 s[i]=s[i+p] 对于所有 i∈[0,∣s∣−p−1] 成立,则称 p 是 s 的周期。
1.5 字符串的 border
对于字符串 s 和 0≤r<∣s∣,若 s 长度为 r 的前缀和长度为 r 的后缀相等,就称 s 长度为 r 的前缀(后缀)是 s 的 border 。
【注】易知前缀函数 π[i] 对应的就是字符串 s[0..i] 的最长 border 的长度。
2. 性质
- s 所有的 border 长度为 π[n−1],π[π[n−1]−1],⋯ 。
- s 所有的周期为 n−π[n−1],n−π[π[n−1]−1],⋯ 。
- π[n−1] 是 s 的最长 border 的长度,n−π[n−1] 是 s 的最小周期。
3. 实现
根据前缀函数的定义我们可以发现,相邻的前缀函数值至多增加 1 ,故可以得到字符串 s 的前缀函数的计算公式:
π[i]=π[i−1]+1
- 如果 s[i]=s[π[i−1]],令 j=π[i−1]。若 s[i]=s[j],则令 j=π[j−1],直到 j=0∨s[i]=s[j] 为止,则
π[i]={0j+1if s[i]=s[j]if s[i]=s[j]
【注】计算字符串的前缀函数的思想和 KMP 算法中计算字符串失配数组的思想非常相似。
4. 应用
4.1 KMP
前缀函数可以用来实现 KMP 算法,思路为:拼接模式串 s 和主串 t,得到 S=s+#+t,# 为不在 s 和 t 中出现的字符。设
m=∣s∣n=∣t∣
计算拼接后的字符串 S 的前缀函数,当出现 i>m∧π[i]=m 时,说明此时模式串匹配上了主串的子串 ti−2m⋯ti−m−1。
整个算法时间复杂度为 O(n+m) 。
4.2 字符串周期 & border
根据上文中给出的性质,可以很容易求出字符串 s 的字符串周期 & border。假设 ∣s∣=m,则可以在 O(m) 时间内求出 s 的所有周期 & border。
4.3 统计每个前缀出现次数
- 统计字符串 s 的所有前缀子串在 s 中出现的次数,m=∣s∣。
- 首先统计前缀数组值 π[i],π[i] 表示字符串 s[0..i] 最长相等真前后缀长度,即说明前缀 s[0..π[i]−1] 在 s[0..i] 中出现了 1 次(不包括前缀本身)。
- 前缀数组值统计后,只统计出了每个前缀作为某个字符串 s[0..i] 的最长真后缀的出现次数,而没有统计非最长真后缀的出现次数,故根据 π 数组的性质统计非最长真后缀的出现次数。
- 加上每个前缀本身 1 次。
1 2 3 4 5 6 7
| ll ans[MAXN]; void getAns(ll m) { for(ll i = 0; i < m; ++i) ++ans[pi[i]]; for(ll i = m-1; i; --i) ans[pi[i-1]] += ans[i]; for(ll i = 0; i <= m; ++i) ++ans[i]; }
|
- 统计字符串 s 的所有前缀子串在 t 中出现的次数,m=∣s∣,n=∣t∣ 。拼接字符串 s 和 t,使得 S=s+#+t 。
- 首先统计前缀数组值 π[i],i>m,π[i] 表示字符串 S[0..i] 最长相等真前后缀长度,即说明前缀 S[0..π[i]−1] 在 S[0..i] 中出现了 1 次(不包括前缀本身),易知最长真前后缀都不会包含界定符 #,故统计得到的只是字符串 t 中的。
- 前缀数组值统计后,只统计出了每个前缀作为某个字符串 S[0..i] 的最长真后缀的出现次数,而没有统计非最长真后缀的出现次数,故根据 π 数组的性质统计非最长真后缀的出现次数。
1 2 3 4 5 6 7
| ll ans[MAXN]; void getAns(ll m, ll n) { for(ll i = m+1; i < n+m+1; ++i) ++ans[pi[i]]; for(ll i = m; i; --i) ans[pi[i-1]] += ans[i]; }
|
4.4 不同子串数目
给定字符串 s,其长度 ∣s∣=m,计算 s 中不同的子串的数目。
-
设字符串 s[0..i] 的不同子串数目为 k,则向 s[0..i] 末尾添加一个字符后得到字符串 s[0..i+1]。显然 s[0..i+1] 的子串中可能会出现一些新的以 s[i+1] 结尾的子串。
-
反转字符串 s[0..i+1] 得到字符串 t,则问题变成统计以 s[i+1] 开头且未在 t 的其他地方出现的前缀数目。
-
设 t 的前缀函数的最大值为 πmax,则最长的出现在 t 其他地方的前缀长度为 πmax,故更短的前缀也一定出现了。
-
因此,字符串 s 新增一个末尾字符 s[i+1] 后新出现的子串的数目为 ∣s∣+1−πmax。
【注】从头部添加、头部移除或尾部移除后计算不同子串的思想类似。
4.5 字符串压缩
根据上文的性质可知,如果计算出 s 的前缀函数之后,s 的最小周期为 k=n−π[n−1]。由字符串的周期的定义可知,最后字符串 s 删去每段周期长度的字符串后,剩余的最后一段字符串长度不一定是 k。故如果 k∣n,则 k 即是 t 的长度,否则不存在一个有效的压缩,即 t 的长度为 n 。
5. 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| #include <bits/stdc++.h> using namespace std;
#ifndef _PREFIXFUNCTION_ #define _PREFIXFUNCTION_ #define ll int #define MAXN 1000005
struct PrefixFunction { ll cnt; ll pi[MAXN]; ll border[MAXN]; ll period[MAXN]; PrefixFunction(): cnt(0) {} void getPi(char *str, ll n) { pi[0] = 0; ll i = 1, j = pi[i-1]; while(i < n) { if(str[i] == str[j]) { pi[i++] = j++ + 1; } else if(!j) { pi[i++] = j; } else { j = pi[j-1]; } } } void getBorder(ll n) { ll count = 0; ll j = pi[n-1]; while(j) { border[count++] = j; j = pi[j-1]; } cnt = count; } void getPeriod(ll n) { ll count = 0; ll j = pi[n-1]; while(j) { period[count++] = n - j; j = pi[j-1]; } cnt = count; } }; #endif
|