一些基本定義:
- \(\mathrm {lcp}(s,t)\) 表示兩個字符串 \(s,t\) 的最長公共前綴 longest common prefix。類似的,\(\mathrm{lcs}(s,t)\) 表示 \(s,t\) 的最長公共后綴 longest common suffix。
- \(s[l,r]\) 和 \(s_{l,r}\) 表示字符串 \(s\) 位置 \(l\sim r\) 上的字符連接而成的子串。若 \(l=1\) 或 \(r=n\) 則有時省略,即 \(s[,r]\) 表示 \(s\) 長度為 \(r\) 的前綴,\(s[l,]\) 表示長度為 \(|s|-l+1\) 的后綴,
- \(|s|\) 表示字符串 \(s\) 的長度。
- 真前綴表示非原串的前綴。真后綴同理。
Change log
- 2021.12.12. 新增 KMP 算法與 Z 算法。
- 2021.12.13. 修改部分筆誤。
- 2021.12.23. 新增前言。
- 2021.12.24. 新增 SA 應用部分。
- 2022.1.10 新增幾個 SA 應用與例題。
0. 前言
幾乎所有字符串算法都存在一個共同特性:基於所求信息的特殊性質與已經求出的信息,使用增量法均攤復雜度求得所有信息。這是動態規划算法的又一體現。
Manacher 很好地印證了這一點:它以所求得的最右回文子串的回文中心 \(d\) 與回文半徑 \(r\) 為基本,利用其回文的性質,通過均攤右端點移動距離保證在線性的時間內求得以每個位置為中心的最長回文半徑。
接下來的后綴數組 SA,KMP 算法,Z 算法與后綴自動機等常見字符串結構無不遵循這一規律。讀者在閱讀時可以着重體會該技巧,個人認為這種思想對於提升解決問題的能力有極大幫助。
1. Manacher 算法
1.1. 算法簡介
首先將字符串所有字符之間(包括頭尾)插入相同分隔符,因為 Manacher 僅能找到長度為奇數的回文串。並在整個字符串最前方額外插入另一種分隔符,防止越界。
設以字符 \(s_i\) 為對稱中心的回文串中最長的回文半徑為 \(p_i\)。一個位置 \(i\) 的回文半徑定義為以 \(i\) 為對稱中心的所有長度為奇數(顯然,若一個回文串以單獨某個字符而非兩字符之間的空隙為對稱中心,其長度必然為奇數)的回文串 \(s[l,r]\) 兩端與 \(i\) 的距離 \(+1\)。若 \(x\) 是 \(i\) 的回文半徑,則 \(s[i - x + 1, i + x - 1]\) 是回文串。
顯然,若 \(x\) 是某一位置的回文半徑,則任何小於 \(x\) 的正整數都是該位置的回文半徑。
Manacher 算法:記錄在所有遍歷過的位置 \(1\sim i-1\) 中,以任意一個點為對稱中心的回文串的右端點最大值 \(r\),即 \(r=\max_{\\j=1}^{i-1}j+p_j-1\),設 \(d\) 即為取到這個最大值的對稱中心。\(r\) 和 \(d\) 都需實時更新。
對於當前位置 \(i\),若 \(i>r\),則直接暴力求 \(p_i\)。否則 \(i\leq r\),先將 \(p_i\) 賦值為 \(\min(r-i+1,p_{2d-i})\),再逐位擴展。說明:因為位置 \(2d-i\) 與 \(i\) 對稱(在 \(d\) 的最長回文半徑范圍內),所以在 \([d-p_d+1,d+p_d-1]\) 范圍內,以 \(2d-i\) 為對稱中心的回文串也是以 \(i\) 為對稱中心的回文串。若 \(p_{2d-i}<r-i+1\),根據對稱性,\(p_i\) 的最終值就等於 \(p_{2d-i}\)(否則 \(p_{2d - i}\) 還可以更大,與其滿足的最大性矛盾)。否則 \(p_i\) 被初始化為 \(r-i+1\),使得每次擴展都會將 \(r\) 向右移動 \(1\),故時間復雜度均攤線性。模板題代碼如下:
const int N = 2.2e7 + 5;
int n, m, ans, p[N]; char s[N >> 1], t[N];
int main(){
scanf("%s", s + 1), n = strlen(s + 1), t[0] = '#', t[m = 1] = '@';
for(int i = 1; i <= n; i++) t[++m] = s[i], t[++m] = '@'; t[++m] = '!';
for(int i = 1, r = 0, d = 0; i < m; i++) {
if(i > r) p[i] = 1; else p[i] = min(p[2 * d - i], r - i + 1);
while(t[i - p[i]] == t[i + p[i]]) p[i]++;
if(i + p[i] - 1 > r) d = i, r = i + p[i] - 1; cmax(ans, p[i] - 1);
} cout << ans << endl;
return 0;
}
1.2. 應用
利用 Manacher 算法,我們可以求出以每個字符開頭或結尾的最長回文子串:考慮一個位置 \(i\) 及其最長回文半徑 \(p_i\),若 \(i+p_i-1>r\) 根據算法我們用 \(i+p_i-1\) 更新 \(r\)。考慮枚舉 \(j\in[r+1,i+p_i-1]\),若 \(t_j\) 對應原串字符 \(s_k\) 而非分隔符,則原串中以 \(s_k\) 結尾的最長回文子串長度為 \(j-i+1\)。實現見例題 I。
1.3. 例題
I. P4555 [國家集訓隊]最長雙回文串
對每個位置求出以該字符開頭和結尾的最長回文子串 \(l_i\) 和 \(r_i\),則 \(\max_{\\i=1}^{n=1}l_i+r_{i+1}\) 即為所求。時間復雜度線性。
const int N = 2e5 + 5;
int n, m, ans, p[N], x[N], y[N];
char s[N], t[N];
int main(){
scanf("%s", s + 1), n = strlen(s + 1), t[0] = '!', t[m = 1] = '@';
for(int i = 1; i <= n; i++) t[++m] = s[i], t[++m] = '@'; t[++m] = '#';
for(int i = 1, r = 0, d = 0; i < m; i++) {
p[i] = r < i ? 1 : min(r - i + 1, p[2 * d - i]);
while(t[i - p[i]] == t[i + p[i]]) p[i]++;
if(i + p[i] - 1 > r) {
for(int j = r + 1; j <= i + p[i] - 1; j++)
if(j % 2 == 0) x[j >> 1] = j - i + 1;
r = i + p[i] - 1, d = i;
}
} for(int i = m - 1, r = m, d = 0; i; i--) {
p[i] = i < r ? 1 : min(i - r + 1, p[2 * d - i]);
while(t[i - p[i]] == t[i + p[i]]) p[i]++;
if(i - p[i] + 1 < r) {
for(int j = i - p[i] + 1; j < r; j++)
if(j % 2 == 0) y[j >> 1] = i - j + 1;
r = i - p[i] + 1, d = i;
}
} for(int i = 1; i < n; i++) ans = max(ans, x[i] + y[i + 1]);
cout << ans << endl;
return 0;
}
II. P1659 [國家集訓隊]拉拉隊排練
以 \(i\) 為對稱中心所有長度 \(j\in [1,p_i]\) 的回文半徑都存在,相當於進行若干次前綴加之后全局查詢,差分即可。時間復雜度是快速冪的 \(\mathcal{O}(n\log n)\)。
III. P5446 [THUPC2018]綠綠和串串
轉化一下題意,任何回文中心 \(i\) 使得存在以位置 \(n\) 結尾的回文串都符合題意,以及若回文中心 \(i\) 存在以位置 \(1\) 開頭的回文串且位置 \(2i-1\) 符合題意也可。於是就是裸的馬拉車了。時間復雜度 \(\mathcal{O}(n)\)。
IV. P6216 回文匹配
首先 KMP 求出 \(s_2\) 在 \(s_1\) 中所有出現位置(結尾)\(\{p_i\}\),考慮一個回文子串 \(s_{l,r}\) 對答案的貢獻:\(|\{p_i\mid p_i\in [l+|s_2|-1,r]\}|\)。注意這對於固定的對稱中心 \(i\) 隨着回文半徑 \(j\) 從 \(1\) 增大至 \(p_i\),\([l+|s_2|-1,r]\) 的左右端點單調向兩側不斷擴展 \(1\),按照 \(i\) 割開,對於左邊就是 \([j\in p_i]\times (j-(i-p_i))\)。右邊同理。
使用一個被用爛掉的技巧:把形如 \(\sum_{\\i=1}^n(c+i)a_i\) 其中 \(c\) 是常數的柿子寫成 \(\sum_{\\i=1}^nca_i+ia_i\) 的形式,維護 \(a_i\) 和 \(ia_i\) 的前綴和即可做到線性。
V. UVA11475 Extend to Palindrome
和例題 III 差不多。
2. Suffix Array 后綴數組
作為復習寫下后綴數組相關博客。前置知識:桶排序。
2.1. 基本定義
- 設 \(su_i\) 表示字符串 \(s\) 以 \(s_i\) 為開頭的后綴,稱為 \(i\) 后綴,即 \(s_{i\sim n}\)。
- 定義 \(rk_i\) 表示 \(su_i\) 在所有后綴中的字典序排名。由於任意后綴長度不同,故排名唯一。
- 定義 \(sa_i\) 表示排名為 \(i\) 的后綴的開始位置,它與 \(rk\) 互逆:\(rk_{sa_i}=sa_{rk_i}=i\)。這就是我們要求的后綴數組,簡稱 SA。
- 簡記 \(i\) 后綴與 \(j\) 后綴的最長公共前綴為 \(\mathrm{lcp}(i,j)\)。
- 定義 \(ht_i\) 表示 \(su_{sa_{i-1}}\) 與 \(su_{sa_i}\) 的最長公共前綴長度,即 \(|\mathrm{lcp}(sa_{i-1},sa_i)|\)。特殊的,\(ht_1=0\)。
2.2. 后綴排序
后綴排序算法能夠通過一系列排序操作得到一個字符串的后綴數組。它主要運用倍增的思想。
假設我們知道了所有 \(2^{w-1}\) 級子串,即所有 \(s_{i,i+2^{w-1}-1}\)(如果 \(i+2^{w-1}-1>n\),則超出的部分定義為空)的排名 \(rk_i\),那么可以算出所有 \(2^w\) 級子串的排名以及對應的 \(sa_i\):對於位置 \(i\) 和 \(j\),若 \((rk_i,rk_{i+2^{w-1}})<(rk_j,rk_{j+2^{w-1}})\),則新的 \(rk'_i<rk'_j\)。也就是說 \((rk_i,rk_{i+2^{w-1}})\) 在所有這樣的二元組中的排名反映了 \(2^w\) 級子串中 \(i\) 的排名。直接排序的復雜度是線性對數平方。但是由於值域僅有 \(n\),所以基數排序優化即可做到線性對數。
在實現中,為了追求更小的常數,我們可以直接優化掉首先對第二關鍵字的排序:若 \(i+2^{w-1}-1>n\),那么它肯定在排序中被排到最前面,然后只需將 \(sa_i\) 按照 \(i\) 從 \(1\) 到 \(n\) 的順序,若 \(sa_i>2^{w-1}\) 則將 \(sa_i-2^{w-1}\) 依次加入排序后的數組(數組存儲的是 \(i\) 而非 \(i+2^{w-1}\) 因此需要減掉 \(2^{w-1}\)),再對其進行桶排即可。
此外,由於 \(rk_i\) 的值有上界 \(m\)(開始排序會有若干個位置 \(rk\) 值相同),桶排時只需枚舉到實時記錄的上界。重新統計 \(rk_i\) 時需更新上界 \(m\),若 \(m=n\) 說明排序完成,直接退出而不需要繼續排到 \(2^{\log_2 n+1}\) 級子串。給出一份實現較良好的后綴排序代碼,附有部分注釋:
char s[N];
int n, sa[N], rk[N], ork[N << 1]; // 由於統計 rk 的時候需要用到原來的 rk, 故復制一份並開兩倍空間 (i + 2 ^ {w - 1} - 1 可能超出 n 的范圍)
int buc[N], id[N], pid[N];
bool cmp(int a, int b, int w) {return ork[a] == ork[b] && ork[a + w] == ork[b + w];}
void build() {
int m = 1 << 7, p = 0;
for(int i = 1; i <= n; i++) buc[rk[i] = s[i]]++;
for(int i = 1; i <= m; i++) buc[i] += buc[i - 1];
for(int i = n; i; i--) sa[buc[s[i]]--] = i;
for(int w = 1; ; w <<= 1, m = p, p = 0) {
for(int i = n; i > n - w; i--) id[++p] = i; // 循環順序無關
for(int i = 1; i <= n; i++) if(sa[i] > w) id[++p] = sa[i] - w;
mem(buc, 0, m + 1); // 注意清空數組
for(int i = 1; i <= n; i++) buc[pid[i] = rk[id[i]]]++; // pid[i] 記錄 rk[id[i]] 使訪問連續
for(int i = 1; i <= m; i++) buc[i] += buc[i - 1];
for(int i = n; i; i--) sa[buc[pid[i]]--] = id[i];
cpy(ork, rk, n + 1), p = 0;
for(int i = 1; i <= n; i++) rk[sa[i]] = cmp(sa[i - 1], sa[i], w) ? p : ++p; // 原排名相同則新排名相同,否則排名 + 1
if(p == n) break; // n 個排名互不相同則排序完成
}
}
2.3. height 數組
極大多數關於 SA 的應用都需要 \(ht\) 數組的信息,可以說 后綴排序要求出 \(sa\) 是為了求出 \(rk\),求出 \(rk\) 是為了求出 \(ht\)。height 數組的定義見上部分,這里給出線性求 height 數組的方法。核心性質:\(ht_{rk_i}\geq ht_{rk_{i-1}}-1\)。
如上圖,我們定義 \(p\) 為 \(i-1\) 后綴排名的前一名所對應的位置,即 \(sa_{rk_{i-1}-1}\)。因為 \(p\) 后綴的排名小於 \(i-1\) 后綴的排名,所以當 \(ht_{rk_{i-1}}>1\) 時,總有 \(s_{p+1}=s_i\),故 \(p+1\) 后綴的排名小於 \(i\) 后綴的排名。又因為 \(p+1\) 后綴與 \(i\) 后綴的 LCP 長度已經等於 \(ht_{rk_{i-1}}-1\),而排名在 \(p+1\) 后綴和 \(i\) 后綴之間的字符串與 \(i\) 后綴之間的 LCP 長度不會減少。例如按字典序排序后,兩個 \(\tt abcd\) 開頭的字符串之間不會出現以 \(\tt abcc\) 或 \(\tt abce\) 開頭的字符串。因此 \(ht_{rk_i}\geq ht_{rk_{i-1}}-1\)。
下方求 \(ht_i\) 的代碼中 \(k\) 指針的移動順序不超過 \(2n\),故總復雜度線性。寫起來很短很舒服。
for(int i = 1, k = 0; i <= n; i++) {
if(k) k--;
while(s[i + k] == s[sa[rk[i] - 1] + k]) k++;
ht[rk[i]] = k;
}
2.4. 應用
2.4.1. 求任意兩個后綴的 LCP
有了 \(ht\) 數組,我們可以快速求一個字符串 \(s\) 的 \(i\) 后綴與 \(j\) 后綴的最長公共前綴 \(\mathrm{lcp}(s[i:],s[j:])\)。
接下來我們給出一個關鍵性質:若 \(i \neq j\),則有
\(i\) 后綴與 \(j\) 后綴的最長公共前綴長度就是夾在這兩個后綴排名之間的 \(ht\) 數組的最小值。這可以由所有后綴的有序性得到。
仍然是這個例子:兩個 \(\tt abcd\) 開頭的后綴之間不會出現以 \(\tt abcc\) 或 \(\tt abce\) 開頭的后綴。即對於任意 \(j'>j>i\),都有 \(\mathrm{lcp}(sa_i,sa_j)\geq \mathrm{lcp}(sa_i, sa_{j'})\),也即字典序排名相差越大,前綴差別越大。
上圖為對 \(\tt aabaaaab\) 后綴排序的結果及其 \(ht\) 數組,由矩形框起來的兩個字符串相等。形象地,兩個后綴之間的 \(\rm lcp\) 就是它們排名之間所有矩形寬度的最小值,即 \(ht\) 的最小值。
如果將整張圖逆時針旋轉 \(90\) 度,得到的將是一張矩形柱狀圖。而 \(ht\) 恰好表示了每個矩形的高度,想必這也是 height 這一名稱的來源吧。也正因如此,SA 可與單調棧相結合(眾所周知,單調棧可以求出柱狀圖中面積最大的矩形)。
由於我們需要查詢區間最值,使用倍增數組維護即可做到 \(\mathcal{O}(n\log n)\) 預處理,\(\mathcal{O}(1)\) 在線回答詢問。
注意點:查詢時范圍是 \(rk_i, rk_j\) 而非 \(i,j\),以及左邊界要 \(+1\)。結合 \(sa,ht\) 數組的實際意義理解!需要特判 \(i = j\) 的情況。
2.4.2 求本質不同子串個數
考慮每次添加一個后綴,並刪去這個后綴與已經添加的后綴的所有重復子串,即 \(\max_{j\in S} |\mathrm{lcp}(s_i, s_j)|\)。由於 \(\max_{j < i}|\mathrm{lcp}(sa_i, sa_j)| = |\mathrm{lcp}(sa_i, sa_{i - 1})| = ht_i\),因此考慮按照 \(sa_1,sa_2,\cdots,sa_n\) 的順序添加后綴,答案即 \(\dbinom n 2 - \sum_{\\ i = 2} ^ n ht_i\)。
2.4.3 應用:與單調棧結合
\(ht\) 數組可以被形象地認為是一張矩形柱狀圖,這是我們用單調棧解決相關問題的基礎。如求所有后綴兩兩 \(\mathrm{lcp}\) 之和,考慮按排名順序加入所有后綴並實時維護 \(F(i) = \sum_{\\ p = 1} ^ {i - 1}|\mathrm{lcp}(sa_p,sa_i)|\),即 \(F(i) = \sum_{\\ p = 1} ^ {i - 1}\min_{q = p + 1} ^ {i} ht_q\),可以看做往單調棧內加入高為 \(ht_i\),寬為 \(1\) 的矩形后,單調棧內矩形面積之和。對所有這樣的 \(F(i)\) 求和即為所求。見例題 V. & VI.
2.4.4. 應用:多個串的最長公共子串
給定 \(n\) 個字符串 \(s_1, s_2,\cdots, s_n\),求它們的最長公共子串:令 \(t = s_1c_1s_2c_2\cdots s_n\),\(L = |t|\),表示將所有字符串拼接起來,並用分隔符隔開。對 \(t\) 建出 SA 數組,問題相當於求 \(\max_{\\ 1\leq l < r\leq L} \min_{p = l + 1} ^ r|\mathrm{lcp}(p, p - 1)|\),其中 排名為 \([l, r]\) 的后綴包含了所有字符串,即對於任意 \(s_i\),它都有一個后綴的排名在 \([l, r]\) 中,這是因為我們需要同時考慮到所有 \(n\) 個字符串。
考慮用雙指針維護這個過程,因為若 \([l, r]\) 滿足限制,則 \([l’, r’]\ (l’ \leq l\leq r\leq r’)\) 必然滿足限制。我們需要實時維護區間最小值,單調隊列即可。時間復雜度是構建 SA 的線性對數,后半部分時間復雜度線性。例題見 XII.
2.5. 例題
*I. CF822E Liar
使用貪心的思想可知在一輪匹配中,我們能匹配盡量匹配,即若從 \(s_i\) 和 \(t_j\) 開啟新的一段,那么我們一定會匹配直到第一個 \(k\) 使得 \(s_{i + k} \neq t_{j + k}\)。這是因為若匹配到一半就斷掉,必然沒有匹配到不能繼續為止更優,即若前者存在符合題意的分配方案,則后者必然存在,調整法易證。
考慮到 \(x\leq 30\) 的限制很像動態規划,我們嘗試設計 DP:設 \(f_{i, j}\) 表示 \(s[1,i]\) 選出最多 \(j\) 個兩兩不交的子串,最多能匹配到 \(j\) 的哪個位置。對於每個 \(f_{i, j}\),首先可以轉移到 \(f_{i + 1, j}\) 表示不開啟一段匹配。若開啟一段匹配,則需要找到 \(s[i +1,n]\) 和 \(t[f_{i, j} + 1, m]\) 的最長公共前綴長度 \(L\),並令 \(f_{i +L, j}\gets \max(f_{i + L, j}, f_{i, j} + L)\)。
求一個字符串某兩個后綴的 \(\rm lcp\) 是后綴數組的拿手好戲,時間復雜度 \(\mathcal{O}(n(x+\log n))\)。本題同時用到了貪心,DP 和 SA 這三個跨領域的算法,是好題。
*II. P1117 [NOI2016] 優秀的拆分
本題巧妙的地方有兩點,一是通過乘法原理將 \(AABB\) 轉化為以每個位置開頭和結尾的 \(AA\) 數量 \(g_i / f_i\),二是枚舉 \(A\) 的長度 \(L\),設置關鍵點並通過差分求 \(f\) 和 \(g\)。對於固定的 \(L\),若每間隔 \(L\) 放置一個關鍵點,則 \(AA\) 必然恰好經過兩個關鍵點。不妨設為 \(p, q\ (p+L = q)\)。我們會在 \(q\) 處統計 \(f_i\) 即可能的結尾位置,在 \(p\) 處統計 \(g_i\) 即可能的開始位置,且 \(q\) 管轄的右端點范圍為 \([q,\min(n,q+L-1)]\),\(p\) 管轄的左端點范圍為 \([\max(1, p-L+1)]\)。
求出 \(s[p+1,n]\) 和 \(s[q+1,n]\) 的最長公共前綴長度 \(r\),以及 \(s[1,p]\) 和 \(s[1,q]\) 的最長公共后綴長度 \(l\)。這說明從 \([q+\max(0,L-l),q+\min(r,L-1)]\) 都可能成為 \(AA\) 的結尾位置:\(l\) 限制了右端點的最小值,即 \(AA\) 的左端點不能小於 \(p-l+1\),因為 \(s[1,p]\) 和 \(s[1,q]\) 的 LCS 只有 \(l\),這說明對於任意 \(i\in [p-l+1,p]\) 都有 \(s_i=s_{i+L}\),且 \(s_{p-l}\neq s_{q-l}\)。同理,\(r\) 限制了右端點的最大值。
求任意兩個前綴的 LCS 或任意兩個后綴的 LCP 用 SA 實現,時間復雜度線性對數,包括建出 SA,建出 \(ht\) 的 ST 表以及枚舉 \(L\) 的調和級數。
*III. P7361 「JZOI-1」拜神
還算不錯的題目。考慮建出 \(s\) 的后綴數組,考慮一次詢問的本質是什么:對於長度 \(L\),它合法當且僅當存在兩個位置 \(p,q\in [l,r-L+1]\ (p\neq q)\),使得 \(\mathrm{lcp}(s[p,n],s[q,n])\geq L\),這說明若將所有 \(\geq L\) 的 \(ht_i\) 值對應的兩個位置 \(sa_{i-1}\) 與 \(sa_i\) 之間連邊,則 \(p,q\) 在同一連通塊。
顯然答案滿足可二分性,因此着眼於判斷一個長度 \(L\) 是否合法。借鑒品酒大會的技巧,我們求出 \(ht\) 數組后從大到小加入並查集,相當於每次合並兩個位置 \((sa_{i-1},sa_i)\)。對於每個長度 \(L\),在線段樹上記錄每個位置 \(p\) 的前驅 \(v_p\),表示 \(v_p\) 是小於 \(p\) 且和 \(p\) 在相同連通塊的最靠右的位置。判斷合法只需查詢 \([l+1,r-L+1]\) 的區間最大值是否 \(\geq l\) 即可。
考慮如何更新 \(v_p\):啟發式合並。對於每個並查集維護一個 set \(S_i\),每往 \(S_i\) 中插入一個數 \(y\),lower_bound 查詢 \(y\) 的后繼 \(su_y\) 與前驅 \(pr_y\),在線段樹上更新 \(v_y\gets pr_y\) 且 \(v_{su_y}\gets y\)。由於要儲存每個長度的線段樹,所以需要可持久化。不難發現時空復雜度均為線性對數平方。
*IV. P2178 [NOI2015] 品酒大會
由於 \(r\) 相似也是 \(r'\ (0\leq r<r)\) 相似,如果僅考慮 \(\geq L\) 的 \(ht_i\),將 \(sa_{i-1}\) 和 \(sa_i\) 之間連邊,若 \(p,q\) 在同一連通塊,說明 \(\mathrm{lcp}(su_p,su_q)\geq L\),也即 \(p,q\) 是 “\(L\) 相似” 的。
這啟發我們求出 \(ht\) 后從大到小排序離線處理,並用啟發式合並實時維護每個連通塊的大小以及所有權值。只需要用最大值乘以次大值,以及最小值乘以次小值(因為有負數)更新 \(L\) 的答案即可,時間復雜度線性對數平方。
進一步地,如果只記錄四個極值並使用線性方法求 SA,可以做到 \(\mathcal{O}(n\alpha(n))\)。
V. P4248 [AHOI2013]差異
SA 與單調棧結合應用例題,時間復雜度線性對數。這里給出代碼。
ll solve() {
static ll stc[N], w[N], top = 0, area = 0, ans = 0;
for(int i = 2; i <= n; i++) {
ll width = 1;
while(top && stc[top] >= ht[i])
width += w[top], area -= w[top] * stc[top], top--;
area += ht[i] * width, stc[++top] = ht[i], w[top] = width, ans += area;
} return ans << 1;
}
int main() {
cin >> s + 1, n = strlen(s + 1), build();
cout << 1ll * (n - 1) * n * (n + 1) / 2 - solve() << endl;
return 0;
}
VI. P7409 SvT
雙倍經驗。
VII. CF1073G Yet Another LCP Problem
三倍經驗。注意區分 \(a_i\) 和 \(b_i\)。
VIII. P3763 [TJOI2017]DNA
枚舉開始位置,使用 SA 加速匹配即可。時間復雜度線性對數。
IX. P7769 丑國傳說 · 大師選徒(Selecting Apprentices)
考慮 \(a_{l+k} + a_{b + k} = s\) 的充要條件是什么:設 \(d\) 為 \(a\) 的差分數組,即 \(d_i = a_{i + 1} - a_i\),顯然需要滿足 \(a_l + a_b = s\) 且 \(d_{l\sim r - 1}\) 與 \(d_{b\sim b + (r - l) - 1}\) 按序互為相反數。
這啟發我們把 \(d\) 以及 \(d\) 的相反數 \(d'\) 拼接在一起,得到序列 \(D\)。然后求出其后綴數組,問題轉化為:求是否存在 \(D\) 的后綴 \(D[i,2n]\),滿足 \(i > n\) 且 \(a_{i - n} = s - a_l\) 且 \(|\mathrm{lcp}(D[i, 2n],D[l, 2n])| \geq r - l\)。對於第三條限制我們容易處理:定位 \(l\) 后綴的排名,那么符合要求的后綴的排名一定是一段區間 \([x, y]\ (x\leq rk_l\leq y)\)。可以首先預處理 \(ht\) 的倍增 RMQ 數組,然后二分 + RMQ 求出。
對於限制 1 和 2,開個桶 \(buc_c\) 記錄 \(i > n\) 且 \(a_{i - n} = c\) 的所有 \(i\) 后綴的 \(rk\),詢問即查詢 \(buc_{s - a_l}\) 中是否存在 \([x, y]\) 之間的數。首先對每個桶排序,回答詢問時直接二分查找即可。時空復雜度均為線性對數,強烈譴責出題人 std 使用 \(\rm 100M\) 空間限制開 \(\rm128M\) 的行為。
X. P5028 Annihilate
考慮枚舉每個字符串 \(s_i\),然后從小到大枚舉每個排名 \(j\),單調隊列維護區間最小值即可。時間復雜度 \(\mathcal{O}(n\sum s_i)\)。
XI. P2852 [USACO06DEC]Milk Patterns G
考慮從大到小添加每個 \(rk_i\),等價於在 \(sa_i\) 和 \(sa_{i - 1}\) 連邊,使得出現大小為 \(k\) 的連通塊的 \(rk_i\) 即為所求。
XII. P2463 [SDOI2008] Sandy 的卡片
將 \(M_i\) 數組差分后即求所有 \(n\) 個數組的最長公共子串,用雙指針加單調隊列實現。時間復雜度線性對數。
const int N = 1.1e5 + 5;
int n, L, ans, a[N], bel[N], s[N];
int sa[N], ht[N], rk[N], ork[N << 1];
int buc[N], id[N], pid[N];
bool cmp(int a, int b, int w) {return ork[a] == ork[b] && ork[a + w] == ork[b + w];}
void build() {
int m = N - 5, p = 0;
for(int i = 1; i <= L; i++) buc[rk[i] = s[i]]++;
for(int i = 1; i <= m; i++) buc[i] += buc[i - 1];
for(int i = L; i; i--) sa[buc[rk[i]]--] = i;
for(int w = 1; ; w <<= 1, m = p, p = 0) {
for(int i = L; i > L - w; i--) id[++p] = i;
for(int i = 1; i <= L; i++) if(sa[i] > w) id[++p] = sa[i] - w;
cpy(ork, rk, L + 1), mem(buc, 0, m + 1);
for(int i = 1; i <= L; i++) buc[pid[i] = rk[id[i]]]++;
for(int i = 1; i <= m; i++) buc[i] += buc[i - 1];
for(int i = L; i; i--) sa[buc[pid[i]]--] = id[i]; p = 0;
for(int i = 1; i <= L; i++) rk[sa[i]] = cmp(sa[i - 1], sa[i], w) ? p : ++p;
if(p == L) break;
}
for(int i = 1, k = 1; i <= L; i++) {
if(k) k--;
while(s[i + k] == s[sa[rk[i] - 1] + k]) k++;
ht[rk[i]] = k;
}
}
int main() {
cin >> n;
if(n == 1) cout << read() << endl, exit(0);
for(int i = 1, m; i <= n; i++) {
cin >> m;
for(int j = 1; j <= m; j++) a[j] = read();
for(int j = 2; j <= m; j++) s[++L] = a[j] - a[j - 1] + 2e3, bel[L] = i;
s[++L] = 1e5 + i;
} build(), mem(buc, 0, N);
static int d[N], hd = 1, tl = 0;
for(int i = 1, l = 1, cnt = 0; i <= L && s[sa[i]] <= 1e5; i++) {
cnt += !buc[bel[sa[i]]], buc[bel[sa[i]]]++;
while(cnt == n && buc[bel[sa[l]]] > 1) buc[bel[sa[l]]]--, l++;
while(hd <= tl && d[hd] <= l) hd++;
if(i > 1) {
while(hd <= tl && ht[d[tl]] >= ht[i]) tl--;
d[++tl] = i;
} if(cnt == n) cmax(ans, ht[d[hd]]);
} cout << ans + 1 << endl;
return flush(), 0;
}
XIII. P6095 [JSOI2015]串分割
顯然的貪心是讓最大位數最小,即答案串長度 \(len = \left \lceil \dfrac n k \right\rceil\)。
同時答案 在后綴數組中的排名 滿足可二分性。我們破環成鏈,枚舉 \(len\) 個起始點並判斷是否可行。具體來說,假設當前匹配到 \(i\),若 \(s_{i, i + len-1}\) 的排名不大於二分的答案,那么就匹配 \(len\) 位,否則匹配 \(len-1\) 位。若總匹配位數不小於 \(n\) 則可行。
正確性證明:若可匹配 \(len\) 位時匹配 \(len-1\) 位,則下一次最多匹配 \(len\) 位,這與首先匹配 \(len\) 位的下一次匹配的 最壞 情況,即匹配 \(len-1\) 位,是相同的。
3. KMP 算法
重新復習基礎算法 ing……
3.1. 算法簡介
KMP 算法可以在 \(|s|+|t|\) 的時間內解決兩個字符串的匹配問題。所謂匹配問題,就是求一個字符串 \(t\ (m=|t|)\) 在另一個字符串 \(s\ (n=|s|)\) 中所有出現位置。當然,也可以通過字符串哈希做到同樣復雜度,但 KMP 算法為我們提供了更多的信息。
維護兩個指針 \(i,j\) 表示當前 \(s_i\) 與 \(t_j\) 匹配。嘗試移動指針,若 \(s_{i+1}\neq t_{j+1}\),說明我們需要重新開始一輪匹配。暴力的做法是令 \(i\) 變成 \(i-j+2\),然后 \(j\) 變成 \(1\),相當於將匹配的開始位置向右移動 \(1\),但復雜度變成了更劣的 \(nm\)。
此時,三位大神橫空出世(大霧),提出了這樣一個解決方法:因為我們知道 \(s[i-j+1,i]\) 與 \(t[1,j]\) 是匹配的,所以接下來一個有可能匹配成功開始位置 \(p\),一定滿足 \(s[p,i]\) 與 \(s[i-j+1,i-j+(i-p+1)]\) 相等。
例如 \(s=\tt abababc\),\(t=\tt ababc\),我們首先會匹配到 \(i=4\),\(j=4\)。但是 \(s_5\neq t_5\) 失配了。根據暴力算法,我們會令 \(i\gets 2\),\(j\gets 1\) 重新進行匹配。但是 \(s_2\) 和 \(t_1\) 根本沒有匹配的可能:\(s[1,4]=t[1,4]\),而 \(s[2,4]=t[2,4]\neq t[1,3]\)。那么,究竟應該如何找到這樣的 \(p\) 呢?
給出結論:記 \(nxt_p\) 表示 \(t[1,p]\) 最長的相等真前綴與真后綴,即 \(t[1,nxt_p]=t[p-nxt_p+1,p]\) 且 \(nxt_p<p\) 且 \(nxt_p\) 最大,第一個可能的匹配位置為 \(i-nxt_j+1\)。原因如下:如果某個小於 \(i-nxt_j+1\) 的位置 \(p\) 可能匹配,那么說明 \(s[p,i]=t[1,i-p+1]\),又因為 \(s[p,i]=t[j-(i-p+1)+1,j]\),所以 \(t[1,i-p+1]=t[j-(i-p+1)+1,j]\)。這說明 \(nxt_j\) 可以等於 \(i-p+1\)。但 \(p<i-nxt_j+1\) 即 \(nxt_j<i-p+1\),這與 \(nxt_j\) 最大的定義矛盾。得證。
對於 \(t=\tt ababc\),其 \(nxt\) 數組即 \(\{0,0,1,2,0\}\)。我們在 \(s_5\neq t_5\) 處失配了,因此令 \(j\gets nxt_j=2\) 繼續嘗試匹配。\(j\gets nxt_j\) 一句跳過了很多東西:它首先忽略了根本沒有匹配可能的開始位置,又跳過了從第一個可能匹配的開始位置 \(i-nxt_j+1\) 匹配 \(nxt_j\) 個字符到 \(i\) 的過程:因為我們已經知道了 \(s[i-nxt_j+1,i]=t[j-nxt_j+1,j]=t[1,nxt_j]\),所以沒有必要再花 \(nxt_j\) 步模擬到達一個已知一定會出現的狀態(即 \(i\) 還是原來的 \(i\),\(j\) 變成了 \(nxt_j\)),而是直接一步得到。
求出 \(nxt_p\) 的過程也很有技巧性:我們讓 \(t\) 對自己做匹配,這其實是一種增量法,也是動態規划思想的體現。假設 \(nxt_1\sim nxt_p\) 已知,我們要求 \(nxt_{p+1}\):
- 首先令 \(i\gets nxt_p\),這表示 \(t[1,p+1]\) 的最長相等前綴后綴一定由 \(t[1,p]\) 的相等前綴后綴(不一定最長,因為可能不匹配)擴展而來,我們先嘗試最長的那個相等前綴后綴。
- 嘗試匹配 \(s_{i+1}\) 和 \(s_{p+1}\),若匹配,則得到 \(nxt_{p+1}=i+1\)。
- 否則我們要找到最大的 \(i'\) 使得 \(i'<i\) 且 \(t[1,p]\) 存在長度為 \(i'\) 的相等前綴后綴。注意到因為 \(t[1,p]\) 存在長度為 \(i\) 的相等前綴后綴,故 \(t[1,p]\) 長度不大於 \(i\) 的后綴也一定是 \(t[1,i]\) 的后綴。這表明我們要求的 \(i’\) 和 \(nxt_i\) 的定義本質相同。因此令 \(i\gets nxt_i\)。
- 不斷重復上述過程直到找到一個 \(i\) 使得 \(s_{i+1}\) 與 \(s_{p+1}\) 成功匹配(\(nxt_{p+1}=i+1\))或者 \(i=0\),此時直接判斷是否有 \(t_1=t_{p+1}\) 即可,若是,則 \(nxt_{p+1}=1\) 否則 \(nxt_{p+1}=0\)。
KMP 非常好寫,模板題 P3375 【模板】KMP 字符串匹配 代碼:
const int N = 1e6 + 5;
int n, m, nxt[N]; char s1[N], s2[N];
int main(){
scanf("%s %s", s1 + 1, s2 + 1), n = strlen(s1 + 1), m = strlen(s2 + 1);
for(int i = 2, p = 0; i <= m; i++) {
while(p && s2[p + 1] != s2[i]) p = nxt[p];
p = nxt[i] = p + (s2[p + 1] == s2[i]);
} for(int i = 1, p = 0; i <= n; i++) {
while(p && s1[i] != s2[p + 1]) p = nxt[p];
if((p += s1[i] == s2[p + 1]) == m) printf("%d\n", i - m + 1), p = nxt[p];
} for(int i = 1; i <= m; i++) printf("%d ", nxt[i]);
return 0;
}
相較於字符串哈希,KMP 算法為我們提供了非常有用的 \(nxt\) 數組,牢記它的定義:\(nxt_i\) 表示 \(s[1,i]\) 最長相等真前綴后綴。很多時候我們使用 KMP 算法不是為了求解字符串匹配,而是為了用 \(nxt\) 數組解題。
注意點:很多時候題目會給 \(nxt\) 數組一些奇奇怪怪的限制(如例題 II. 中,相同公共前綴后綴不能超過串長一半),但我們不能在一開始求 \(nxt\) 時就將這些限制條件加入,而是先完整地求完一遍 \(nxt\) 后再利用其提供的信息解題。因為 KMP 算法的正確性基於求 \(nxt_i\) 時已知的 \(nxt_1\sim nxt_{i-1}\) 的最大性。這好比我們不能給高速行駛的汽車(求 KMP 的過程)換輪胎(加限制),只有當車停下來之后(求得 \(nxt\) 數組)才可以(加限制)。
3.2. 擴展:KMP 自動機
KMP 自動機是一種確定有限狀態自動機。
對一個長度為 \(n\) 的字符串 \(s\) 建立 KMP 自動機,它的狀態集合 \(Q\) 為 \(0\sim n\)。每個結點 \(i\) 表示已經輸入的所有字符 \(t[1,p]\) 與 \(s\) 的前綴匹配長度為 \(i\),即 \(t[p-i+1,p]=s[1,i]\)。它的轉移函數 \(\delta(i,c)\) 為:
很好理解:若當前字符匹配,則匹配長度 \(+1\)。否則若 \(i=0\) 顯然匹配長度仍為 \(0\),否則找到 \(s[1,i]\) 的最長相等前綴后綴長度 \(nxt_i\),\(i\) 接受字符 \(c\) 轉移到的狀態就是 \(nxt_i\) 接受字符 \(c\) 轉移到的狀態,而 \(nxt_i<i\) 所以 \(\delta(nxt_i,c)\) 已知。例題見 I.
int tr[N][26];
for(int i = 0; i <= n; i++) {
for(int j = 'a'; j <= 'z'; j++)
if(i < n && s[i + 1] == j) tr[i][j] = i + 1;
else if(!i) tr[i][j] = 0;
else tr[i][j] = tr[nxt[i]][j];
}
3.3. 應用:失配樹與 Border 理論
見 Part 5. border 理論部分。
3.4. 應用:AC 自動機
3.5. 例題
I. P3193 [HNOI2008]GT考試
KMP 自動機。設 \(g_{i,j}\) 表示當匹配長度為 \(i\) 時有多少種加字符的方式能使得匹配長度變為 \(j\),形式化地:
設 \(f_{i,j}\) 表示長度為 \(i\) 的准考證,和 \(A\) 匹配到了第 \(j\) 位的方案數(有點 DP 自動機的味道),那么 \(f_{i+1,k}=\sum_{\\j=0}^{m-1}f_{i,j}g_{j,k}\)。注意到這是矩陣乘法的形式,因此矩陣快速冪解決即可。時間復雜度 \(\mathcal{O}(m^3\log n)\)。
*II. P2375 [NOI2014] 動物園
相當於對每個前綴求其最長的長度不超過串長一半的 border 在失配樹上的深度。為此,我們首先求一遍 \(nxt\) 數組,然后用 \(s\) 匹配自己,特別地,若任意 \(i\) 前綴與 \(s\) 的匹配長度 \(j\) 大於 \(\dfrac i 2\) 則需要不斷跳 \(nxt_j\) 直到 \(j\leq \dfrac i 2\)。
為什么不能在一開始就限制 \(nxt_i\leq \dfrac i 2\) 呢?因為這樣會導致求得的 \(nxt_i\) 錯誤。具體原因見 Part 3.1. 結尾部分注意事項。
III. UVA11022 String Factoring
經 典 老 題。設 \(f_{l,r}\) 表示 \(s[l,r]\) 能夠被壓縮成的最短長度,分兩種情況討論:枚舉分割點 \(p\),\(f_{l,r}=\min_{\\l\leq p<r} f_{l,p}+f_{p+1,r}\) 或者壓縮整個串,枚舉壓縮成的長度 \(p\),若 \(s[l,r]\) 具有整周期 \(p\) 則 \(\mathrm{checkmin}(f_{l,r},f_{l,l+p-1})\),周期和 border 的定義見 Part 5. Border 理論。
預處理 \(nxt_{i,j}\) 表示對 \(s[i+1,n]\) 建 \(nxt\) 數組后 \(j\) 的 \(nxt\) 值,不斷跳 \(nxt\) 找 border,因為一個 border 對應一個周期。這樣我們可以在 \(\mathcal{O}{(r-l)}\) 時間內找到所有周期。因此總復雜度 \(\mathcal{O}(n^3)\),比暴力判斷周期的 \(\mathcal{O}(n^4)\) 更快一點。
*IV. P3449 [POI2006]PAL-Palindromes
神仙題!看到題目,我的想法是如果 \(s_j\) 能接到 \(s_i\) 后面,說明 \(s_j\) 是 \(s_i\) 的前綴。因此,對枚舉每個 \(s_i\) 的每一位 \(p\),求出 \(s_{i}[1,p]\) 在 \(s\) 中的出現次數,以及 \(s_i[p+1,|s_i|]\) 是否是回文串。這可以通過簡單的字符串哈希在線性對數時間內做到。
注意到我們根本沒有用到 \(s\) 是回文串的性質。這好嗎?這不好。我們有一個更強的性質:在 \(s_i,s_j\) 是回文串的前提下,\(s_i+s_j\) 是回文串(命題 \(P\))當且僅當它們的最短回文整周期串相同(命題 \(Q\))。
充分性(\(Q\Rightarrow P\)):顯然。
在證明必要性之前,我們給出一個引理:若長度為 \(n\) 的回文串 \(s\) 存在回文周期 \(p\),則存在長為 \(\gcd(n,p)\) 的回文整周期。利用數學歸納法證明如下:
-
當 \(p\mid n\) 時,顯然成立。
-
設串 \(A=s[1,p]\),\(B=s[n-n\bmod p+1,n]\),即 \(s=AA\cdots AB\)。設 \(q=|B|\),顯然 \(q=n\bmod p>0\)。設 \(B_R\) 表示 \(B\) 翻轉后得到的串。
由於 \(s,A\) 都是回文串,故 \(s=B_RA\cdots AA\)。因為 \(B\) 是 \(A\) 的前綴,\(B_R\) 也是 \(A\) 的前綴,所以 \(B=B_R\) 即 \(B\) 回文。故 \(s=BA\cdots AA\)。
因為 \(A\) 是 \(BA\) 的前綴,所以 \(A[1,q]=A[q+1,2q]\)。同理,\(A[q+1,2q]=A[2q+1,3q]=\cdots\)。這說明 \(|B|\) 是 \(A\) 的回文周期。
-
這是一個遞歸式的子命題:若長度為 \(p\) 的回文串 \(A\) 存在回文周期 \(q\),則存在長為 \(\gcd(p,q)\) 的回文整周期。若子命題成立,則原命題成立。
-
由於 \(\gcd(n,p)=\gcd(p,n\bmod p)=\gcd(p,q)\),類似輾轉相除法,因此必然出現 \(p'\mid n\) 的情況,此時 \(p'=\gcd(n,p)\)。故原命題成立。
數學歸納法真好用。
必要性(\(P\Rightarrow Q\)):考慮反證法,設 \(s_i\) 最短回文周期對應的回文串為 \(x\)(下文省略 “對應的” 字樣),\(s_j\) 的為 \(y\)。不妨設 \(|x| < |y|\)(\(|x|=|y|\) 時顯然若 \(s_i+s_j\) 回文則 \(x=y\) )。
-
首先,\(x\) 不可能是 \(y\) 某個整周期回文串,否則 \(s_j\) 最短回文周期不大於 \(|x|\),顯然矛盾。
-
當 \(|s_i|<|y|\) 時,因為 \(s_i+s_j=s_iyy\cdots y\),而 \(s_i\) 和 \(s_i+s_j\) 是回文串,所以 \(s_i+s_j=yy\cdots ys_i\)。故 \(y[1,|s_i|]=y[|s_i|+1,2|s_i|]=\cdots\),即 \(|s_i|\) 是 \(y\) 的回文周期。
根據引理,這說明 \(y\) 存在回文整周期 \(d=\gcd(|y|,|s_i|)<|y|\),從而有 \(s_j\) 的最短回文周期不大於 \(d\) 即小於 \(|y|\),與 \(y\) 的定義矛盾。
-
當 \(|s_i|\geq |y|\) 時,因為 \(y\) 和 \(s_i+s_j\) 是回文串,所以 \(s_i[1,|y|]=y\)。這說明 \(|x|\) 是 \(y\) 的回文周期。
根據引理,\(y\) 存在回文整周期 \(d=\gcd(|x|,|y|)<|y|\),從而有 \(s_j\) 的最短回文周期不大於 \(d\) 即小於 \(|y|\),與 \(y\) 的定義矛盾。\(\square\)
本文用了多少遍反證法(大霧)?
綜上,我們只需 KMP 求出每個字符串的 \(nxt\) 數組,若 \(|s|-nxt_{|s|}\) 整除 \(|s|\) 則 \(s[1,|s|-nxt_{|s|}]\) 是最短回文周期字符串,否則最短回文周期字符串就是 \(s\) 本身。
每個最短回文周期字符串對答案的貢獻為 \(c^2\),其中 \(c\) 表示它作為 \(c\) 個串的最短回文周期字符串出現。為此,我們需要排序並檢查相鄰兩個最短回文周期字符串是否相等。時間復雜度線性對數。
*V. P3546 [POI2012]PRE-Prefixuffix
好題!不難發現若 \(S,T\) 循環相同則它們可以分別被寫成 \(AB\) 和 \(BA\) 的形式。因此題目相當於:對於 \(S\) 的每個 border 長 \(p\ (2p\leq S)\),求 \(S\) 去掉 \(p\) 前綴和 \(p\) 后綴后最長的不重疊 border 長 \(q\),則 \(\max p+q\) 即答案。
\(p\) 是好求的,但每個 \(S[p+1,n-p]\) 的 border 就不好求了。考慮 \(S\) 所有這樣的子串串 \(S_i=S[i+1,n-i]\),\(S_i\) 的 border 掐頭去尾后變成了 \(S_{i+1}\) 的 border,因此 \(|B_{\max}(S_i)|\leq |B_{\max}(S_{i+1})|+2\)。
根據這一性質,我們從大到小枚舉所有 \(i\ (1\leq i\leq n/2)\),維護長度 \(p\) 表示 \(S_i\) 的最長不重疊 border 長。當 \(i\to i-1\) 時,令 \(p\gets p+2\),然后不斷減小 \(p\) 直到 \(p\) 是 \(S_i\) 的一個不重疊 border 長。勢能分析,\(p\) 的移動距離不超過 \(2n\)。
判斷字符串是否相等使用哈希,自然溢出哈希會被卡。求 \(S\) 的所有 border 直接 KMP 就行。時間復雜度線性。雙 倍 經 驗。
4. Z Algorithrm
別稱 Z 算法,擴展 KMP 算法。
4.1. 算法簡介
我們定義一個長度為 \(n\) 的字符串 \(s\) 的 z 函數 \(z_i\) 表示 \(s\) 的 \(i\) 后綴與 \(s\) 本身的最長公共前綴長度,即 \(z_i=|\mathrm{lcp}(s[i:n],s)|\)。一般令 \(z_1=0\)。Z 算法利用已經求得的信息的性質,通過增量法求出 z 函數。OI-wiki。
稱 \([i,i+z_i-1]\) 為 \(i\) 的匹配段,也稱 z-box。根據定義,\(s[i,i+z_i-1]=s[1,z_i]\)。算法中,我們實時記錄最靠右側的匹配段 \([l,r]\),初始化 \(l=r=0\)。匹配到位置 \(i\) 時,分兩種情況討論:
- 若 \(i>r\),直接暴力匹配。
- 若 \(i\leq r\),因為 \(s[1,r-l+1]=s[l,r]\),所以 \(s[i,r]=s[i-l+1,r-l+1]\)。故首先令 \(z_i=\min(r-i+1,z_{i-l+1})\),然后暴力匹配。
看完我直呼:這也太像 Manacher 了!
時間復雜度分析:當 \(z_{i-l+1}<r-i+1\) 時,\(z_i\) 不可能繼續向下匹配,否則與 \(z_{i-l+1}\) 的最大性矛盾。因此,每次成功的匹配都會讓 \(r\) 增加 \(1\)。因此時間復雜度是優秀的線性,代碼如下:
for(int i = 2, l = 0, r = 0; i <= n; i++) {
z[i] = i > r ? 0 : min(r - i + 1, z[i - l + 1]);
while(t[1 + z[i]] == t[i + z[i]]) z[i]++;
if(i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
4.2. 應用:字符串匹配
求字符串 \(t\) 的每個后綴 \(i\) 與 \(s\) 的最長公共前綴長度 \(p_i\)。
- 解法 1:令 \(s’=s+c+t\),其中 \(c\) 是任意分隔符,對 \(s'\) 求 z 函數。
- 解法 2:類似求 z 函數的方法,我們維護最右匹配段 \([l,r]\) 表示 \(t[l,r]=s[1,r-l+1]\),若 \(i>r\) 則暴力匹配,否則令 \(p_i=\min(z_{i-l+1},r-i+1)\)。
兩種解法本質相同,因為 Z Algorithm 就相當於用 \(s\) 匹配 \(s\),類比 KMP 算法中自己匹配自己的方式求出 \(nxt\) 數組。解法 2 代碼見例題 I.
4.3. 例題
I. P5410 【模板】擴展 KMP(Z 函數)
const int N = 2e7 + 5;
int n, m, z[N], p[N]; ll ans;
char s[N], t[N];
int main(){
scanf("%s %s", s + 1, t + 1), n = strlen(s + 1), z[1] = m = strlen(t + 1);
for(int i = 2, l = 0, r = 0; i <= m; i++) {
z[i] = i > r ? 0 : min(r - i + 1, z[i - l + 1]);
while(t[1 + z[i]] == t[i + z[i]]) z[i]++;
if(i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
} for(int i = 1; i <= m; i++) ans ^= 1ll * i * (z[i] + 1);
cout << ans << endl, ans = 0;
for(int i = 1, l = 0, r = 0; i <= n; i++) {
p[i] = i > r ? 0 : min(r - i + 1, z[i - l + 1]);
while(p[i] < m && t[1 + p[i]] == s[i + p[i]]) p[i]++; // 注意這里應該判斷 p[i] 小於模式串長度而非匹配串長度
if(i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
} for(int i = 1; i <= n; i++) ans ^= 1ll * i * (p[i] + 1);
cout << ans << endl, ans = 0;
return 0;
}
*II. CF526D Om Nom and Necklace
重新表述題意:若 \(S\) 能被表示成 \(AAA\cdots AB\),其中 \(A\) 出現了 \(k\) 次且 \(B\) 是 \(A\) 的前綴,則 \(S\) 符合要求。考慮在 \(k\times A\) 處統計答案。根據 border 的性質 2:若 \(S\) 有長為 \(|S|-p\) 的 border 說明 \(S\) 有周期 \(p\),我們 KMP 求出 \(S\) 的 \(nxt\) 數組。若 \(i - nxt_i \left | \dfrac i k \right.\) 說明 \(S[1,i]\) 由 \(k\) 個相同字符串拼接而成,即 \(|A|=\dfrac i k\)。
此時僅需考慮可能的 \(B\) 的最長長度 \(r\),即 \(\min(|A|,|\mathrm{lcp}(S[i+1, n],S)|)\),后者可以用 Z 算法求得。這說明 \(S[1,i\sim i + r]\) 都可以成為答案,用差分維護即可,時間復雜度線性。
III. CF432D Prefixes and Suffixes
找到前綴后綴可用 KMP 求得 \(nxt\) 數組,求出現次數使用 \(z\) 函數 + 差分即可。時間復雜度線性。
5. Border 理論
5.1. 基礎定義
定義長度為 \(n\) 的字符串 \(s\) 的 \(\mathrm{Border}(s)\) 表示 \(s\) 的所有相等真前綴后綴集合,下文簡記為 \(B(s)\)。簡記 \(i\) border 表示長度 \(i\) 的 border。
定義 \(s\) 的 border 中最長的一個為 \(\mathrm{Border}_{\max}(s)\),簡記為 \(B_{\max}(s)\)。不難發現若對 \(s\) 建出 \(nxt\) 數組,\(nxt_n=|B_\max(s)|\)。
定義 \(p\) 為 \(s\) 的周期當且僅當 \(\forall i\in [1,n-p],s_i=s_{i+p}\)。
5.2. Border 的性質
-
性質 1:若 \(p\) 為 \(s\) 的周期,則 \(s[1,n-p]\) 為 \(s\) 的 border。
證明:因為 \(s_i=s_{i+p}\),所以 \(s[1,n-p]=s[p+1,n]\)。\(\square\)
-
性質 2:若 \(s\) 存在 border,則其最短 border 長度不超過字符串長度的一半。
證明:設 \(s\) 的最短 border 長度為 \(L\ (2L>n)\),那么 \(s[1,L]=s[n-L+1,n]\)。因為 \(n-L+1\leq L\),所以 \(s[n-L+1,L]=s[1,2L-n]\) 且 \(s[n-L+1,L]=s[2n-2L+1,n]\)。因此 \(s[1,2L-n]\) 也是 \(s\) 的 border,這與 \(L\) 的定義矛盾,如下圖。\(\square\)
-
性質 3:
5.3. 失配樹
在 KMP 算法中注意到 \(nxt_i<i\),因此若從 \(nxt_i\) 向 \(i\) 連邊,我們最終會得到一棵有根樹。這就是失配樹。
失配樹有很好的性質:對於樹上任意兩個具有祖先 - 后代關系的節點 \(u, v\ (u\in \mathrm{ancestor}(v))\),\(s[1,u]\) 是 \(s[1,v]\) 的 Border。這一點由 \(nxt\) 的性質可以得到。因此,若需要查詢 \(u\) 前綴和 \(v\) 前綴的最長公共 border,只需要查詢 \(u,v\) 在失配樹上的 LCA 即可。模板題 P5829 【模板】失配樹 代碼:
const int N = 1e6 + 5;
const int K = 20;
int n, m, lg, dep[N], fa[K][N]; char s[N];
int main(){
scanf("%s %d", s + 1, &m), n = strlen(s + 1), lg = log2(n), dep[0] = 1, dep[1] = 2;
for(int i = 2, p = 0; i <= n; i++) {
while(p && s[p + 1] != s[i]) p = fa[0][p];
dep[i] = dep[p = fa[0][i] = p + (s[p + 1] == s[i])] + 1;
} for(int i = 1; i <= lg; i++) for(int j = 2; j <= n; j++) fa[i][j] = fa[i - 1][fa[i - 1][j]];
while(m--) {
int u, v; scanf("%d %d", &u, &v), u = fa[0][u], v = fa[0][v];
if(dep[u] < dep[v]) swap(u, v);
for(int i = lg; ~i; i--) if(dep[fa[i][u]] >= dep[v]) u = fa[i][u];
for(int i = lg; ~i; i--) if(fa[i][u] != fa[i][v]) u = fa[i][u], v = fa[i][v];
cout << (u == v ? u : fa[0][u]) << "\n";
} return 0;
}
暫時沒見到失配樹有什么應用。
I. P4391 [BOI2009]Radio Transmission 無線傳輸
來一道基礎題:根據性質 1,要求 \(s_1\) 的最短周期,只需要用 \(n-nxt_n\) 即可。
*II. P3435 [POI2006]OKR-Periods of Words
若 \(s\) 存在 border,令其最短 border 長度為 \(L\),根據 border 的性質 1 和性質 2,\(s[1,n-L]\) 就是一個合法的周期。因此只需求出每個點在失配樹上的最淺非 \(0\) 祖先。
*III. P3426 [POI2005]SZA-Template
POI 的題目質量總是這么高,很有啟發性,好評
!
轉化題意,相當於求長度最小的 border \(s[1,p]\) 使得其在 \(s\) 中的出現位置間隔不超過 \(p\)。如果一個長為 \(p\) 的 border 在位置 \(i\) 出現了,說明 \(nxt_i\geq p\)(除了特例 \(i=p\))。這啟發我們從小到大枚舉所有可能的 border 長度 \(L\ (L\in \{|t|\mid t\in B(s)\})\),每次不斷刪去 \(nxt\) 值小於 \(L\) 的位置 \(i\)(特判 \(i=L\)),實時維護任意兩個相鄰的未被刪去的位置之間的距離最大值 \(mx\),可以用雙向鏈表實現。長為 \(L\) 的 border 可行當且僅當 \(mx\leq L\)。時間復雜度線性。
翻看了一遍題解區,發現一個驚為天人的 DP 做法。它用到了這樣一個性質:設 \(f_i\) 表示 \(s[1,i]\) 的答案,那么 \(f_i\) 要么等於 \(i\),要么等於 \(f_{nxt_i}\)。
定義 \(i\) 覆蓋 \(j\) 表示 \(s[1,j]\) 能由 \(s[1,i]\) 覆蓋得到,證明如下:若 \(i<k\) 且 \(i\) 覆蓋 \(k\),則 \(i\) 能覆蓋所有 \(j\),其中 \(i<j<k\) 且 \(j\) 是 \(k\) 的 border,這一點顯然。因此,若 \(f_{nxt_i}<f_i\leq nxt_i\),說明 \(f_i\) 覆蓋 \(nxt_i\)。因為 \(f_{nxt_i}\) 也能覆蓋 \(nxt_i\),所以 \(f_{nxt_i}\) 覆蓋 \(f_i\)。再結合 \(f_i\) 能覆蓋 \(i\),說明 \(f_{nxt_i}\) 能覆蓋 \(i\),這與 \(f_i\) 的定義矛盾。又因為 \(f_i\) 顯然不小於 \(f_{nxt_i}\) 且 \(f_i\) 若不等於 \(i\) 則必須是 \(s[1,i]\) 的 border 長,結合 \(nxt_i\) 的定義可知 \(f_i\) 只能等於 \(i\)。
\(f_i=f_{nxt_i}\) 的充要條件是存在 \(j\in [i-nxt_i,i-1]\) 使得 \(f_j=f_{nxt_i}\),開桶記錄即可,時間復雜度是常數更小的線性,實在是太巧妙了。
IV. P3538 [POI2012]OKR-A Horrible Poem
\(s\) 有長為 \(p\) 的周期 \(\Leftrightarrow\) \(s\) 有 \(|s|-p\) 的 border。因此,枚舉區間長 \(r-l+1\) 的所有因數 \(d\) 判斷是否有 \(|s|-d\) 的 border 即可。時間復雜度 \(\mathcal{O}(nd(n))\)。
由於若 \(p\) 是周期,那么 \(kp\ (kp\mid |s|)\) 也是周期,因此可以不斷除以 \(|s|\) 的質因數判斷,即從小到大枚舉所有 \(|s|\) 的質因數 \(d\),不斷將 \(p\) 除以 \(d\) 直到不是周期或不能整除為止。時間復雜度 \(\mathcal{O}(n\omega(n))\),拿到了最優解(2021.12.19)。



