一個小約定:下文中的所有字符串下標都從 \(0\) 開始。
#1.0 什么是 Z-函數
對於一個長度為 \(n\) 的字符串 \(S\),定義函數 \(z(i)\) 表示 \(S[i,n-1]\),即以 \(S[i]\) 開頭的后綴,與 \(S\) 的最長相同前綴(\(\texttt{Longest Common Prefix, LCP}\))的長度,特別地,我們定義 \(z(0)=0\)。\(z\) 被稱為 \(S\) 的 Z-函數。
別看它還有另一個名字擴展 KMP,但是實際上 \(\texttt{KMP}\) 算法與 \(\tt{Z}\)-函數除了看起來思想上很像,\(\tt{Z}\)-函數比 \(\tt{KMP}\) 能實現的功能好像多一點外,沒有任何聯系。
#2.0 求解 Z-函數
#2.1 朴素算法
很顯然,上面的做法是 \(O(n^2)\) 的。
#2.2 線性算法
這里有一個思想與 \(\tt{KMP}\) 有些類似:運用之前已有的狀態加速計算當前狀態。這種思想在 \(\tt{Manacher}\) 算法等許多字符串算法中同樣有體現。
我們假設當前已經計算出了 \(z(0),z(1),\cdots,z(i-1)\) ,現在來考慮如何計算 \(z(i).\)
先來定義幾個概念:
- 匹配段(Z-Box):對於 \(x\),我們稱 \([x,x+z(x)-1]\) 為 \(x\) 的匹配段;
- 記當前右端點最靠右的匹配段為 \([l,r].\)
在計算 \(z(i)\) 過程中,保證 \(l\leq i\)。初始時 \(l=r=0\)。
如果當前 \(i\leq r\),那么根據 \(z\) 的定義有 \(S[l,r]=S[0,r-l]\),所以有 \(S[i,r]=S[i-l,r-l]\),那么 \(S[i,n-1]\) 與 \(S\) 的 \(\tt{LCP}\) 長度 \(z(i)\) 只有以下可能:
-
當 \(z(i-l)<r-i+1\) 時,\(z(i)=z(i-l)\)。
來看下面這張圖,我們知道根據 \(z\) 的定義,應當有 \(S[i-l,i-l+z(i-l)-1]=S[0,z[i-l]-1]\),而又有 \(S[i,r]=S[i-l,r-l]\),所以如果 \(z(i-l)<r-i+1\),意味着相同前綴的長度不可能超過 \(z(i-l)\),否則與 \(z(i-l)\) 的定義相悖。
-
當 \(z(i-l)\geq r-i+1\) 時,應當先令 \(z(i)=r-i+1\),再盡可能地向后擴展。
如同下面這張圖,我們只能確定 \(S[i,r]=S[i-l,r-l]\) 相同,后面的無法確定。
當 \(i>r\) 時,我們無法用已知狀態進行轉移,只能暴力向后擴展。
結束當前計算后,我們檢查是否有 \(i+z(i)-1>r\),如果是,那么更新 \([l,r].\)
於是得到代碼:
inline void z_func() {
for (int i = 1, l = 0, r = 0; i < lenb; ++ i) {
if (i <= r && z[i - l] < r - i + 1)
z[i] = z[i - l];
else {
/*注意進入 else 的可能時 r < i 的情況,
所以下面的 z[i] 應當取 Max(0, r - i + 1)*/
z[i] = Max(0, r - i + 1);
while (i + z[i] < lenb && b[z[i]] == b[i + z[i]])
++ z[i]; //盡可能向后擴展。
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
/*更新 [l,r] 的范圍*/
}
}
#3.0 Z-函數的應用
#3.1 LG P5410 擴展 KMP(Z 函數)
並不知道該給這種應用起什么名字。
-
操作一就是基礎的 \(\tt{Z}\)-函數,只不過要注意需要單獨處理 \(z(0)\),顯然是 \(b\) 的長度。
-
操作二與 \(\tt Z\)-函數的定義十分相似,所以依舊考慮使用已經求出的 \(z\) 進行加速求解。
整體的討論與上面沒有任何區別,這里略去不寫。注意仍需單獨處理。
const int N = 20000100;
const int INF = 0x3fffffff;
int lena, lenb, z[N], p[N];
ll ans1, ans2;
string a, b;
template <typename T>
inline T Max(const T a, const T b) {
return a > b ? a : b;
}
template <typename T>
inline T Min(const T a, const T b) {
return a < b ? a : b;
}
inline void z_func() {
for (int i = 1, l = 0, r = 0; i < lenb; ++ i) {
if (i <= r && z[i - l] < r - i + 1)
z[i] = z[i - l];
else {
z[i] = Max(0, r - i + 1);
while (i + z[i] < lenb && b[z[i]] == b[i + z[i]])
++ z[i];
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
}
inline void p_func() {
for (int i = 1, l = 0, r = 0; i < lena; ++ i) {
if (i <= r && z[i - l] < r - i + 1)
p[i] = z[i - l];
else {
p[i] = Max(0, r - i + 1);
while (i + p[i] < lena && b[p[i]] == a[i + p[i]])
++ p[i];
}
if (i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
}
while (p[0] < Min(lena, lenb) && b[p[0]] == a[p[0]]) p[0] ++;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cin >> a >> b;
lena = a.length(), lenb = b.length();
z_func(); z[0] = lenb; p_func();
for (int i = 0; i < lenb; i ++)
ans1 ^= ((ll)(i + 1) * (z[i] + 1));
for (int i = 0; i < lena; i ++)
ans2 ^= ((ll)(i + 1) * (p[i] + 1));
printf("%lld\n%lld", ans1, ans2);
return 0;
}
#3.2 模式串匹配
用 \(\tt Z\) 函數做模式串匹配很簡單,將要尋找的串憑借在文本串前,兩者中間用 #
等不會在兩個串中出現的字符連接,求出新串的 \(\tt Z\) 函數,枚舉每個位置上的 \(z\),如果 \(z[i]\) 等於模式串的長度,那么該位置存在我們要找的模式串。
中間的 #
是為了保證匹配的最大長度不會超過模式串的長度。
正確性顯然。