進行一個字串演算法的總結

AzusidNya發表於2024-06-17

本文參考 字串基礎 by Alex_Wei。


Manacher 演算法

這玩意是用來求迴文子串的。

雖然一個字串的子串數量是 \(O(n^2)\) 級別的,但是迴文串有更好的描述方式。

注意到若一個子串 \([l, r]\) 是以 \(mid\) 為迴文中心的迴文串,那麼將左端點和右端點朝著 \(mid\) 方向挪動若干單位也是迴文串。因此我們只需要記錄迴文中心和最大的迴文半徑就可以得到所有迴文子串的資訊。

迴文中心的個數是 \(O(n)\) 級別的,所以能高度壓縮地記錄迴文串的資訊。

Manacher 演算法就是一個 \(O(n)\) 的時間複雜度求出每個點的迴文半徑的演算法。


隨便敲個字串 \(\texttt {aazuusuuzazza}\) 出來。

觀察到偶數長度的迴文子串的迴文中心在兩個字元之間,而奇數長度的迴文子串迴文中心是一個字元。

為了把這兩種子串統一起來,我們在兩個字元之間加入一個不會在串中出現的字元,例如 \(\texttt{@}\)

然後我們得到了串串 \(\texttt {@a@a@z@u@u@s@u@u@z@a@z@z@a@}\)

\(R_i\) 為新串的第 \(i\) 個字元的最長迴文半徑,那麼我們要求的是 \(R\) 陣列。

手玩一下,以第 \(i\) 個字元為迴文中心的最長串串的長度就是 \(R_i - 1\)

考慮一種暴力。列舉迴文中心後嘗試向兩邊擴充套件,能擴充套件則擴充套件。

因為迴文串級別是 \(O(n^2)\) 的,所以這個演算法的時間複雜度是 \(O(n^2)\) 的。

但是我們發現迴文串有些比較好的性質。

舉個例子,我們在上面抓個子串出來。就決定是你了,\(\texttt {@a@a@z@u@u@s@u@u@z@a@z@}\)

假設我們已經知道了最中間的那個 \(\texttt{s}\)\(R_i\)\(10\),能擴充套件到最遠的地方是倒數第二個 \(\texttt @\)

再考慮這個 \(\texttt s\) 後面三個字元的那個 \(\texttt @\)。我們的 \(R_i\) 還用從 \(0\) 開始列舉嗎?

把這個 \(\texttt @\) 對稱過去,找到 \(\texttt s\) 前面三個的 \(\texttt @\)。我們求出了這個 \(\texttt @\) 的最長迴文半徑是 \(3\)

那對稱地,這個 \(\texttt @\) 的最長迴文半徑至少是 \(3\),這樣我們將 \(R_i\) 賦初始值為 \(3\) 就行了。

再考慮倒數第四個字元的那個 \(\texttt a\)。對稱過去是第二個字元的 \(\texttt a\),其 \(R\)\(2\),所以賦這個 \(a\) 初值為 \(2\)。這個時候就能繼續擴充套件到 \(4\)。然後能擴充套件到最遠的地方更遠了,所以更新當前對稱中心和擴充套件到的最遠地方。

實現上,設當前擴充套件到最遠的地方為 \(r\),對稱中心為 \(c\)

如果當前字元 \(i > r\),那麼令 \(R_i = 0\)。否則令 \(R_i = \min\{R_{2c - i}, r - i + 1\}\)

然後暴力擴充套件,更新 \(r\)\(c\)

對於每個 \(r\),在 \(i < r\) 的時候更新是 \(O(1)\) 的,而 \(r\) 最多變化 \(n\) 次。

所以時間複雜度是 \(O(n)\) 的。


P3805 【模板】manacher

\(R\) 陣列求出來後對 \(R_i - 1\)\(max\) 即可。

namespace azus{
	int n;
	string s, a;
	int R[23000005];
	int main(){
		cin >> s;
		n = s.length();
		for(int i = 0; i < n; i ++)
			a += "@", a += s[i];
		a = a + "@"; n = a.length(); a = " " + a;
		R[1] = 1;
		int r = 1, c = 1, ans = 0;
		for(int i = 1; i <= n; i ++){
			if(i <= r)
				R[i] = min(r - i + 1, R[2 * c - i]);
			while(i - R[i] >= 0 && i + R[i] <= n && a[i - R[i]] == a[i + R[i]])	R[i] ++;
			if(i + R[i] - 1 > r) r = i + R[i] - 1, c = i;
			ans = max(ans, R[i] - 1);
		}
		cout << ans;
		return 0;
	}
}

意外的發現 a += "@"a = a + "@" 有很大區別,前者複雜度 \(O(1)\),後者還要加上覆制串串的複雜度所以是 \(O(n)\)

P3501 [POI2010] ANT-Antisymmetry

和迴文串性質一樣,在 while 迴圈中改改條件,變成擴充套件 ANT-Antisymmetry 串就行了。

P4555 [國家集訓隊] 最長雙迴文串

對於每個 \(\texttt @\),統計以它為右端點的最長迴文串和以它為左端點的最長迴文串。

但是我們統計的 \(R_i\) 是一個點能擴充套件到的最長長度。

定義一個迴文串是飽和的,如果這個字串不能再擴充套件了。我們發現有些點結尾或開頭的不飽和字串比包和字串還長。

所以簡單遞推更新一下即可。

//Manacher 中
ls[i + R[i] - 1] = max(ls[i + R[i] - 1], R[i] - 1);
rs[i - R[i] + 1] = max(rs[i - R[i] + 1], R[i] - 1);
//進行一個簡單遞推和統計方案
for(int i = n; i >= 3; i -= 2)
	ls[i] = max(ls[i + 2] - 2, ls[i]);
for(int i = 3; i <= n; i += 2)
	rs[i] = max(rs[i - 2] - 2, rs[i]);
for(int i = 1; i <= n; i ++)
	if(a[i] == '@' && ls[i] && rs[i]) ans = max(ans, ls[i] + rs[i]);

P1659 [國家集訓隊] 拉拉隊排練

把每個長度的奇迴文串的長度用桶統計一下。

然後用快速冪直接做就行了。

for(int i = 2; i <= n; i += 2)
	t[R[i] - 1] ++;
for(int i = n - (!(n & 1)); i >= 1; i --){
	if(!t[i]) continue;
	if(k <= t[i]) {ans = ans * ksm(i, k) % P, k = 0; break;}
	if(i == 1) break;
	k -= t[i], ans = ans * ksm(i, t[i]) % P;
	t[i - 2] += t[i], t[i] = 0;
}

P5446 [THUPC2018] 綠綠和串串

這東西乍看下去有點複雜。

觀察下,首先發現如果有一個以最後一個字元結尾的迴文子串,那麼以這個子串迴文中心翻轉一下一定滿足條件。

然後,如果以 \(i\) 為軸翻轉的串滿足條件,那麼如果有個字串能翻轉造出字串 \([1, i]\) 那也能滿足條件。

這個字串必須從 \(1\) 開始翻轉,所以這個字串的迴文中心固定為 \(\frac 12 (1 + i)\) 這個位置。

從後往前遞推判斷每個字元是否能滿足條件即可。

//Manacher 中
if(a[i] != '@' && i + R[i] - 1 == n) flg[i] = 1;
//進行一個遞推
for(int i = n - 1; i >= 2; i -= 2){
	if(flg[i]){
		int u = 1 + (i + 1) / 2;
		if(a[u] == '@') continue;
		if(u + R[u] - 1 == i + 1) flg[u] = 1;
	}
}
for(int i = 1; i <= n; i ++)
	if(flg[i]) cout << i / 2 << " ";

相關文章