前綴函數
我個人覺得 oiwiki 上的學習順序是很合理的,學 KMP 之前先了解前綴函數是非常便于理解的。
前后綴定義
前綴 prefixprefixprefix 指的是從字符串 SSS 的首位到某個位置 iii 的一個子串,這樣的子串寫作 prefix(S,i)prefix(S,i)prefix(S,i)。
后綴 suffixsuffixsuffix 指的是從字符串 SSS 的某個位置 iii 到末尾的一個子串,這樣的子串寫作 suffix(S,i)suffix(S,i)suffix(S,i)。
SSS 的 真前綴 指的是不等于 SSS 的一個前綴,SSS 的 真后綴 指的是不等于 SSS 的一個后綴。
如 S=aabS=aabS=aab,那么 aabaabaab 是 SSS 的一個前綴,但不是 SSS 的真前綴,是 SSS 的后綴,但不是 SSS 的真后綴。
前綴函數定義
給定一個長度為 nnn 的字符串 sss,其 前綴函數 寫作 π\piπ ,則 π(i)\pi(i)π(i) 的定義為子串 s[0...i]s[0...i]s[0...i] 的 最長相等真前綴與真后綴 長度。
意思就是
- 如果子串 s[0...i]s[0...i]s[0...i] 有一對相等的真前綴與真后綴,那么 π(i)\pi(i)π(i) 就是這個相等的真前綴的長度。
- 如果有多對相等的,π(i)\pi(i)π(i) 取最長的一對作為答案。
- 如果不存在相等,那么 π(i)=0\pi(i)=0π(i)=0。
如 s=aabbas=aabbas=aabba,則 π(0)=0,π(1)=1,π(2)=0,π(3)=0,π(4)=1\pi(0)=0,\pi(1)=1,\pi(2)=0,\pi(3)=0,\pi(4)=1π(0)=0,π(1)=1,π(2)=0,π(3)=0,π(4)=1。
其中 π(0)\pi(0)π(0) 表示字符串 aaa 的最長相等真前綴與真后綴,由于 aaa 長度為 111,所以沒有真前綴,故 π(0)=0\pi(0)=0π(0)=0。
其中 π(4)\pi(4)π(4) 表示字符串 aabbaaabbaaabba 的最長相等真前綴與真后綴,答案是 aaa,故 π(4)=1\pi(4)=1π(4)=1。
前綴函數的求法
樸素算法
利用雙重循環,第一重循環枚舉 當前子串長度 s[0...i]s[0...i]s[0...i],第二層循環枚舉子串的所有 真前綴的長度,長度從大到小枚舉,并判斷當前真前綴與真后綴是否相同,如果相同的話當前長度就等于 π(i)\pi(i)π(i)。
for (int i = 1; i < s.size(); i++) {for (int j = i; j >= 0; j--) {if (s.substr(0, j) == s.substr(i - j + 1, j)) {p[i] = j;break;}}
}
其中 s.substr(pos, len)
是字符串的一個函數,意思是提取出 sss 從 pospospos 位置開始往下數 lenlenlen 個字符的子串,等價于子串 s[pos...pos+len?1]s[pos...pos+len-1]s[pos...pos+len?1],要減 111 是因為從 pospospos 開始,pospospos 也算一個字符。
所以 s.substr(0, j)
表示的是子串 s[0...j?1]s[0...j-1]s[0...j?1],s.substr(i - j + 1, j)
表示的是子串 s[i?j+1,i]s[i-j+1,i]s[i?j+1,i]。
該算法的時間復雜度是 O(n3)O(n^3)O(n3)。
優化一
當我們求 π(i)\pi(i)π(i) 的時候,我們沒有 充分運用 之前求過的 π\piπ 值。
對于 s[0...i]s[0...i]s[0...i],考慮如何充分利用 π(i?1)\pi(i-1)π(i?1) :
- π(i?1)=0\pi(i-1)=0π(i?1)=0,說明 π(i)\pi(i)π(i) 的值 至多 為 111。如果 π(i)\pi(i)π(i) 的值大于 111,說明 s[0...i?1]s[0...i-1]s[0...i?1] 的最長相等真前綴與后綴的長度至少為 111,與 π(i?1)=0\pi(i-1)=0π(i?1)=0 矛盾。
- π(i?1)≠0\pi(i-1)\neq 0π(i?1)=0,如果 s[i]==s[π(i?1)]s[i]==s[\pi(i-1)]s[i]==s[π(i?1)],那么 π(i)=π(i?1)+1\pi(i)=\pi(i-1)+1π(i)=π(i?1)+1。否則 π(i)\pi(i)π(i) 的大小必然小于等于 π(i?1)\pi(i-1)π(i?1)。
不難發現,π(i)\pi(i)π(i) 的 上限至多 比 π(i?1)\pi(i-1)π(i?1) 多 111,所以第二重循環只需要從 π(i?1)+1\pi(i-1)+1π(i?1)+1 枚舉即可。
for (int i = 1; i < s.size(); i++) {for (int j = p[i - 1] + 1; j >= 0; j --) {if (s.substr(0, j) == s.substr(i - j + 1, j)) {p[i] = j;break;}}
}
關于時間復雜度的計算,當我們計算 π(i)\pi(i)π(i) 的時候 多枚舉 了 xxx 次,說明 π(i)\pi(i)π(i) 的值相對于 π(i?1)\pi(i-1)π(i?1) 減少了 xxx。也就是說 π(i+1)\pi(i+1)π(i+1) 的第二重循環的上限也就 減少 了 xxx。
也就是說,多增加的次數,在后續的求解中會被抵消,那么就只剩下了最終至少需要枚舉的第一次。
所以第二重循環的時間就主要在 substr
函數的 O(n)O(n)O(n) 上,故總時間復雜度為 O(n2)O(n^2)O(n2)。
優化二
第二重循環從 π(i?1)+1\pi(i-1)+1π(i?1)+1 開始遍歷,每次判定還是依靠了 substr
,有沒有不用 substr
的方法?
如果想不用 substr
就能判斷前綴后綴是否相等,說明我們就得跳到 前綴后綴一定相等 的位置。
也就是說當 s[π(i?1)]≠s[i]s[\pi(i-1)]\neq s[i]s[π(i?1)]=s[i] 的時候,我們就得找到一個僅次于 π(i?1)\pi(i-1)π(i?1) 的長度 jjj,使得 s[0...j?1]=s[i?j...i?1]s[0...j-1]=s[i-j...i-1]s[0...j?1]=s[i?j...i?1],如果找到了這樣的 jjj,我們再判斷 s[j]s[j]s[j] 和 s[i]s[i]s[i] 是否相等就行了。
如果相等,說明 π(i)=j\pi(i)=jπ(i)=j,否則我們就找下一個僅次于 jjj 的長度 … 直到 jjj 削減為 000,此時 π(i)=0\pi(i)=0π(i)=0。
我們可以看到這張圖,當 s[π(i?1)]s[\pi(i-1)]s[π(i?1)] 與 s[i]s[i]s[i] 匹配失敗,我們就要找一個僅次于 π(i?1)\pi(i-1)π(i?1) 的長度 jjj,使之滿足s[0...j?1]=s[i?j...i?1]s[0...j-1]=s[i-j...i-1]s[0...j?1]=s[i?j...i?1],在圖上就是深紅色的兩個位置。
又因為一定有 s[0...p[i?1]?1]=s[i?p[i?1]...i?1]s[0...p[i-1]-1]=s[i-p[i-1]...i-1]s[0...p[i?1]?1]=s[i?p[i?1]...i?1] 成立,這是 π(i?1)\pi(i-1)π(i?1) 的定義,所以可以認為 s[0...p[i?1]?1]s[0...p[i-1]-1]s[0...p[i?1]?1] 的 后綴必然有一個長度為 jjj 的子串 等于 s[i?j...i?1]s[i-j...i-1]s[i?j...i?1]。
又因為 s[0...p[i?1]?1]s[0...p[i-1]-1]s[0...p[i?1]?1] 的 前綴必然有一個長度為 jjj 的子串 等于 s[i?j...i?1]s[i-j...i-1]s[i?j...i?1],所以 s[0...p[i?1]?1]s[0...p[i-1]-1]s[0...p[i?1]?1] 有 一對相等的前后綴,其長度為 jjj。
所以我們可以得出,下一個長度僅次于 π(i?1)\pi(i-1)π(i?1) 的長度 jjj 等于 π(π(i?1)?1)\pi(\pi(i-1)-1)π(π(i?1)?1)。
于是,我們就可以省略掉 substr
的 O(n)O(n)O(n),只需要每次去比較 s[j]s[j]s[j] 和 s[i]s[i]s[i] 是否相等即可。
經過兩次優化,求前綴函數的算法的時間復雜度為 O(n)O(n)O(n)。
void getPrifixFunction () {p[0] = 0;for (int i = 1; i < n; i++) {int j = p[i - 1];while (j && s[j] != s[i]) {j = p[j - 1];}if (s[j] == s[i]) j ++;p[i] = j;}}
這串代碼似乎和上面描述的有一些出入,所以這里解釋一下每一句話。
首先 getPrifixFunction
是求前綴函數的函數,前綴函數的第一個值 p[0]=0p[0]=0p[0]=0。
然后枚舉所有長度的子串 s[0...i]s[0...i]s[0...i],最初 jjj 是滿足 s[0...j?1]=s[i?j...i?1]s[0...j-1]=s[i-j...i-1]s[0...j?1]=s[i?j...i?1] 的最大長度 π(i?1)\pi(i-1)π(i?1)。
然后循環判斷是否 s[j]==s[i]s[j]==s[i]s[j]==s[i],如果不等于那么就往下跳到下一個長度 j=π(j?1)j=\pi(j-1)j=π(j?1)。
最后特判一下長度為 111 的情況,因為長度為 111 的時候是 s[0]==s[i]s[0]==s[i]s[0]==s[i],所以 jjj 已經削減到 000 了。
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define int long long
#define endl '\n'
#define PII pair<int,int>
#define INF 1e18
const int N = 1e6 + 7;struct PrifixFunction {int n;string s;vector <int> p;PrifixFunction (int _n, string _s) : s(_s), n(_n), p(_n + 1){}void getPrifixFunction () {p[0] = 0;for (int i = 1; i < n; i++) {int j = p[i - 1];while (j && s[j] != s[i]) {j = p[j - 1];}if (s[j] == s[i]) j ++;p[i] = j;}}};signed main() {}