1. 程式人生 > 其它 >【題解】CF432D - Prefixes and Suffixes

【題解】CF432D - Prefixes and Suffixes

題目大意

題目連結

給定一個長度為 \(n\) 的模式串 \(S\)。定義 完美子串 表示 \(S\) 的子串中既是 \(S\) 的字首又是 \(S\) 的字尾的子串。對於給出的模式串 \(S\),試求所有可能的完美子串以及它們出現的位置。輸出每個完美子串對應字首的尾下標以及它在 \(S\) 中出現的次數,按尾下標升序輸出。

\(1 \leq n \leq 10^5\)

解題思路

正常來說不會想到用擴充套件 \(KMP\),但是因為擴充套件 \(KMP\) 做法程式碼量比 \(KMP\) 做法略小,因此本文著重講解擴充套件 \(KMP\) 解法並賦上程式碼。擴充套件 \(KMP\) 做法的字串下標從 \(0\)

開始。另外的做法還有:

  1. 正解:\(KMP + dp\)
  2. 正解:\(SAM\)\(+ AC\) 自動機)
  3. 亂搞:\(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;
}