【題解】CF432D - Prefixes and Suffixes
題目大意
給定一個長度為 \(n\) 的模式串 \(S\)。定義 完美子串 表示 \(S\) 的子串中既是 \(S\) 的字首又是 \(S\) 的字尾的子串。對於給出的模式串 \(S\),試求所有可能的完美子串以及它們出現的位置。輸出每個完美子串對應字首的尾下標以及它在 \(S\) 中出現的次數,按尾下標升序輸出。
\(1 \leq n \leq 10^5\)
解題思路
正常來說不會想到用擴充套件 \(KMP\),但是因為擴充套件 \(KMP\) 做法程式碼量比 \(KMP\) 做法略小,因此本文著重講解擴充套件 \(KMP\) 解法並賦上程式碼。擴充套件 \(KMP\) 做法的字串下標從 \(0\)
- 正解:\(KMP + dp\)
- 正解:\(SAM\)(\(+ AC\) 自動機)
- 亂搞:\(KMP + AC\) 自動機
從題目中 既是字首又是字尾 可以聯想到用擴充套件 \(KMP\) 來維護。顯然如果以 \(i\) 為左端點的字尾是 \(S\) 的完美子串,那麼一定有 \(nxt_i = n - i\) 。我們可以很輕鬆地判斷出某個字尾是否是完美子串,難點在於維護完美子串的出現次數。
最暴力的做法是對於每一個完美子串都 \(KMP\) 一次,時間複雜度為 \(O(n ^ 2)\)。優化的思路是把若干個完美子串用 \(AC\) 自動機來維護,時間複雜度是正確的,但是程式碼量比正常的 \(KMP\)
不妨令新串 \(S^{\prime} = S|S\),設 \(m = |S^{\prime}|\),下文中的 \(S\) 統一指代 \(S^{\prime}\)。顯然我們對構造出的新串進行擴充套件 \(KMP\) 以後,我們只需要關注 \(S\) 的後半段即可。對於構造出的新串 \(S\),\(\forall n + 1 \leq i \leq 2 \times n\),如果以 \(i\) 為左端點的字尾是原串的完美子串,那麼必定有 \(nxt_i = m - i\)
考慮維護每個完美子串的出現次數。在 \(S\) 的後半段中,對於一個對應字尾以 \(i\) 為左端點的 完美子串 \(a\),如果存在另一個左端點為 \(j\) 的 子串 \(b\) 使得 \(nxt_j \geq nxt_i\),那麼 \(j\) 一定小於 \(i\) 並且 \(a\) 一定是 \(b\) 的子串。因為我們判斷時取到了等於,因此完美子串 \(a\) 可能等於子串 \(b\),所以可以統計到完美子串 \(a\) 本身。我們可以考慮維護滿足 \(\forall n + 1 \leq j \leq 2 \times n, nxt_j \geq nxt_i\) 的 \(j\) 的個數來統計出子串 \(a\) 出現的次數。具體的實現我們可以使用 字尾和 來維護一個 桶,從而快速求出大於等於某值的元素個數。
總時間複雜度 \(O(n)\),瓶頸在於擴充套件 \(KMP\),詳見程式碼。
參考程式碼
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 5;
const int maxm = 2e5 + 5;
int n, m;
int nxt[maxm], cnt[maxn];
char a[maxn], s[maxm];
void get_nxt()
{
int p = 1, q, j = 0;
nxt[0] = m;
while (j + 1 < m && s[j + 1] == s[j])
j++;
nxt[1] = j;
for (int i = 2; i < m; i++)
{
q = p + nxt[p] - 1;
if (i + nxt[i - p] <= q)
nxt[i] = nxt[i - p];
else
{
j = max(q - i + 1, 0);
while (i + j < m && s[i + j] == s[j])
j++;
nxt[i] = j, p = i;
}
}
}
int main()
{
int ans = 0;
scanf("%s", a);
n = strlen(a);
m = 2 * n + 1;
// 構造新串
for (int i = 0; i < n; i++)
s[i] = a[i];
s[n] = '|';
for (int i = 0; i < n; i++)
s[i + n + 1] = a[i];
// 擴充套件 KMP
get_nxt();
for (int i = n + 1; i < m; i++)
cnt[nxt[i]]++; // 維護桶
for (int i = n; i >= 1; i--)
cnt[i] += cnt[i + 1]; // 求字尾和
for (int i = n + 1; i < m; i++)
if (nxt[i] == m - i) // LCP長度等於字尾長度即為完美子串
ans++;
printf("%d\n", ans);
// i - n 表示當前列舉的字尾的左端點
// m - i 表示當前列舉的字尾的長度
// 因為 nxt[i] == m - i 時該字尾為完美子串
// 長度為 m - i 的完美子串一定在原串中對應著字首 [1, m - i]
// 即對應字首的尾下標為 m - i
// 因此當 i 從大往小列舉時 m - i 遞增,符合要求
for (int i = m - 1; i >= n + 1; i--)
{
if (nxt[i] == m - i)
printf("%d %d\n", m - i, cnt[nxt[i]]);
}
return 0;
}