SA 学习笔记

前言

这是我发布的第一篇博客,从学习SA开始。

基本概念

一些规定

字符串的下标从1开始,\(n=|S|\),记\(suf(x)\)表示以\(s[x]\)开头的后缀,\(pre(x)\)表示以\(s[x]\)为结尾的前缀。记\(suf(x),pre(x)\)的编号为\(x\)\(lcp(i,j)\)表示\(suf(i)\)\(suf(j)\)的最长公共前缀。

后缀数组

后缀数组储存的是关于后缀的一些信息,然后运用这些信息来解决字符串问题。

后缀数组实际上是两个数组\(sa[],rk[]\)
\(sa[i]\)表示将所有后缀按照字典序排序之后排名第\(i\)位的后缀的编号;
\(rk[i]\)表示\(suf(i)\)的排名。

所以容易发现一个性质:\(sa[rk[i]]=rk[sa[i]]=i\)

后缀数组求法

后缀数组求法一般是用倍增,复杂度\(O(nlogn)\),当然也有\(O(n)\)的做法,但是目前我并没有学习,所以就只在这里写一下倍增的求法。

前置知识

计数排序:我感觉类似于桶排序,将每种元素统计出来储存在\(cnt[]\)中,然后求一个前缀和,然后\(cnt[x]\)的值就是\(x\)的排位。

基数排序:这是针对于有\(k(k>1)\)维参数的元素的排序的方法。具体方法是:先按照第\(k\)维进行排序,在按照\(k-1\)维排序,一直进行到第\(1\)维。正确性可以感性理解一下:因为越靠后的参数越不重要,所以先对最不重要的排序,然后一层一层的用更重要的参数的排序方法将它覆盖,最终就是正确的排序,代码如下(来自OI-WiKi):

void counting_sort(int p) {
	memset(cnt, 0, sizeof(cnt));
	for (int i = 1; i <= n; ++i) ++cnt[a[i].key[p]];
	for (int i = 1; i <= w[p]; ++i) cnt[i] += cnt[i - 1];
	
	// 为保证排序的稳定性,此处循环i应从n到1
	// 即当两元素关键字的值相同时,原先排在后面的元素在排序后仍应排在后面
	for (int i = n; i >= 1; --i) b[cnt[a[i].key[p]]--] = a[i];
	memcpy(a, b, sizeof(a));
}

void radix_sort() {
	for(int i = k; i >= 1; --i) counting_sort(i);
}

后缀排序

字符串是由一个个字符组成的,其实也可以将这每一个字符都当做这一个字符串的其中一维参数,而将字符串按照字典序排序就可以把它当成将有多维参数的元素进行排序,所以考虑基数排序

但是一维一维的去比较明显复杂度过高,所以考虑倍增优化

先把每一个字符进行排序。然后假设说我们已经将所有\(w\)(是2的幂)位字符的子串排序完毕,那么我们考虑把编号为\(i\)和编号为\(i+w\)子串合并当成一个二元组,然后再对二元组进行基数排序即可,直到排序到所有编号的子串的排名都不相同,也就是排序完毕。具体的过程参见下图(来自OI-Wiki):

当然还可以进行一些常数级别优化。这里记\(id[i]\)表示按照第二维排序后排名第\(i\)位的后缀,\(lrk[i]\)表示只按照第一维排序后后缀\(i\)的排名。

  1. 因为在编号\(n-w+1, n-w+2,\cdots,n\)的后缀的第二元为空,所以说它们在进行第二位的排序中肯定在排名的前几位,所以现将它们排序放入排名,接下来再对其他排序。代码如下:
for(i = n, p = 0; i > n - w; --i) id[++p] = i;
for(int i = 1; i <= n; ++i)
	if(sa[i] > w) id[++p] = sa[i] - w;
  1. 可以新建一个数组\(px[]\),其中\(px[i]\)表示按照第二维排序后排名为\(i\)的后缀的只按照第一维排序时的排名,即\(px[i]=rk[id[i]]\)

  2. 对于完全相等的二元组排名应当是相同的,所以要查重,判断函数如下:

bool cmp(int x, int y, int w){
	return lrk[x] == lrk[y] && lrk[x + w] == lrk[y + w];
}

整体的板子代码放在下面:(常数优化在代码中有注释)

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 30;

int n, m = 300, sa[N], rk[N], px[N], id[N], cnt[N], lrk[N];
char s[N];

bool cmp(int x, int y, int w){
	return lrk[x] == lrk[y] && lrk[x + w] == lrk[y + w];
}

int main(){
	int i, p, w;
	scanf("%s", s + 1); n = strlen(s + 1);
	for(i = 1; i <= n; ++i) cnt[rk[i] = s[i]]++;
	for(i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
	for(i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
	for(w = 1;; w <<= 1, m = p){
		for(i = n, p = 0; i > n - w; --i) id[++p] = i;
		for(int i = 1; i <= n; ++i)
			if(sa[i] > w) id[++p] = sa[i] - w;
		memset(cnt, 0, sizeof(cnt));
		
		//rk[id[i]]可以理解为在基数排序中的第一维元素,
		//用px[i]将rk[id[i]]存起来是为了使原来不连续的下标id[i]转化为连续的下标i,优化常数。 
		for(int i = 1; i <= n; ++i) cnt[px[i] = rk[id[i]]]++;														 
		for(int i = 1; i <= m; ++i) cnt[i] += cnt[i - 1]; 
		
		//为保证排序的稳定性,此处循环i应从n到1
		//即当两元素关键字的值相同时,原先排在后面的元素在排序后仍应排在后面
		for(int i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i]; 
		memcpy(lrk, rk, sizeof(rk));

		//对于二元组完全相同的元素,使它们的排名变成相等的(该操作类似于离散化) 
		for(p = 0, i = 1; i <= n; ++i)
			rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;

		//考虑新的rk数组,若其值域为[1, n]那么每个排名都不同,此时无需再排序。
		if(p == n){
			for(int i = 1; i <= n; ++i) sa[rk[i]] = i;
			break;
		}
	}
	for(int i = 1; i <= n; ++i) printf("%d ", sa[i]);
	return 0;
}

height 数组

这个数组是我认为SA中最有用的部分,对于只用\(sa[]\)的题目其实大多数也可以用\(exKMP\)之类完成,而有些题目则只能用\(height\)求(当然也可以用\(SAM\)求,但我还没学...),而且经常结合一些数据结构。

定义\(height[i]\)表示\(suf(sa[i])\)\(suf(sa[i-1])\)\(LCP\)(最长公共前缀)。

性质: \(height[rk[i]] \ge height[rk[i-1]]-1\)。这个性质我还不太会证明,只能硬记啦。

LCP 求解

\[lcp(i,j)=\min_{k=rk[i]+1}^{rk[j]}height[k] \]

正确性证明:首先对于排名越远的两个后缀,它们的\(lcp\)一定越小(因为是按字典序排序,所以这很显然),而且\(lcp(sa[i],sa[i+2])\)一定是\(lcp(sa[i],sa[i+1])\)的前缀(因为\(sa[i+1]\)\(sa[i]\)相似的程度一定比\(sa[i+2]\)\(sa[i]\)的相似程度高),所以结合\(height\)的定义感性理解一下就能得到这个式子。

求法:利用性质求解就行:

for(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;
}

SA 的基本应用

这里有一个技巧:对于处理多个字符串之间的问题,一般可以将它们接在一起,中间用没有出现过的字符连接起来,当然连接字符也要互不相同。

sa 数组的运用

1、P4051 [JSOI2007]字符加密最小表示法

本题让循环排序使字典序最小,其实就是求最小表示法,用sa数组也可以做,方法就是把\(S\)倍长变成\(SS\)然后进行后缀排序,从\(sa[1]\)开始输出就行。

2、匹配

判断S在模式串T是否出现、求出现的位置,可以先对T进行后缀排序,然后在排序后的后缀之后二分查找即可。

height 数组的基本应用

这一类题主要是询问某个子串出现的次数、相同的子串的最长长度、在多个字符串中找相同的子串等等。下面给出几道题目:

1、UVA1223 Editor(最长相同子串)

height数组的板子题,因为排名越远的两个后缀的\(lcp\)肯定越小,所以求完\(height\)数组之后取个最大值即可。代码把 SA 模板和\(height\)数组模板拼一块儿然后加一行\(mx = max(mx, k);\)就行。

2、P2852 [USACO06DEC]Milk Patterns G(多次出现的子串)

因为出现\(k\)次,排名距离越近的后缀\(lcp\)越近,所以就是求任意排名连续的\(k\)个后缀的\(lcp\)的最大值。用一个st表或者单调队列维护就行。我用的\(multiset\)维护。代码除了模板内容如下:

for(i = 1; i <= n; ++i){
	q.insert(height[i]);
	if(i > k) q.erase(q.find(height[i - k]));
	ans = max(ans, *q.begin());
}

3、P2408 不同子串个数

不同子串的个数就是所有子串的个数减去重复的子串的个数。而子串就是
字符串后缀的前缀,因此可以想到用sa数组,而子串相等,也就是后缀的前缀相等,而两个相等的字符串的子串也一定相等,所以说我们只要找到\(lcp\)并把它减掉就行了,因此想到\(height\)数组,因为排名越近相同前缀越长,所以只用减去对于后缀\(sa[i]\)\(|suf(sa[i])| - height[i]\)就是它新增加的前面没有出现过的子串的个数,因此答案就是:

\[ans = \sum_{i=1}^{n}n-sa[i]+1-height[i] = \frac{n*(n+1)}{2} - \sum_{i=1}^{n}height[i] \]

把板子敲上去,然后统计答案即可。代码就不放了。

4、P1117 [NOI2016] 优秀的拆分

因为一个优秀拆分由两部分组成, AA 和 BB ,这两部分结构都是两个连续的相等的子串,因此可以先预处理出出现结构\(AA\)(表示两个相等的连续子串)的位置,存在数组\(a[],b[]\)中,其中\(a[i]\)表示在\(i\)处开始的结构\(AA\)的数量,\(b[i]\)表示在\(i\)处结束的\(AA\)的数量,那么最后的答案很显然就是:

\[\sum_{i=1}^{n-1}b[i]*a[i+1] \]

所以接下来关键的问题就是如何处理出数组\(a[],b[]\)

法一

之前说过字符串后缀的前缀是子串,现在要找到两个连续且相等的子串,其实就是找到两个子串\(suf(i),suf(j)\),使\(lcp(i,j) \ge i-j\)。因此计算\(height\)数组枚举即可,复杂度\(O(n^2)\)

法二

上述方法显然会超时,因此我么考虑对这个算法进行优化。

因为我们只关心在哪里会有\(AA\)结构,而不是关心这样的结构有多长,所以说没有必要去对每一个\(AA\)去枚举它的位置然后用\(lcp\)去判断,这样的话超出\(lcp\)长度的子串就在以后还要再枚举判断,因此我们现在考虑在什么样的情况之下会出现\(AA\)。因为\(AA\)出现是要两个完全相等的子串连接在一起,所以长度至关重要,考虑枚举\(|A|\)然后判断。不难发现,如果\(|A|=len\),那么结构\(AA\)一定经过两个点,满足这两个点的下标\(i,j\)满足\(len|i,len|j\),并且这两个点一定位于两个不同位置的\(A\)中。所以说可以让\(s[i],s[j]\)向两边扩展,使在扩展的子串中\(pre(i-1)=pre(j-1)\)\(suf(i)=suf(j)\)并且让该子串尽可能的长。这时候如果这两个子串在原字符串中相交说明出现\(AA\)结构。下图是扩展时的情形:

注意:在本图中\(LCS(i-1,j-1)\)表示最长的满足\(pre(i-1)=pre(j-1)\)的字符串。

\[\cdots\cdots \underbrace{s_{i-LCS(i-1,j-1)},\cdots, s_{i-1}}_{LCS(i-1,j-1)},\overbrace{s_{i}, \cdots\cdots}^{LCP(i,j)},\overbrace{\cdots\cdots}^{?},\underbrace{\cdots\cdots, s_{j-1}}_{LCS(i-1,j-1)},\overbrace{s_{j},\cdots,s_{j+LCP(i,j)-1}}^{LCP(i,j)}\cdots\cdots \]

如果 \(|?| > 0\), 就说明以\(i,j\)为关键点无法构成结构\(AA\)

\(|?| \le 0\)就说明可以构成结构\(AA\),并且容易发现:当第二个A的开头在\([LCS(i-1,j-1),LCP(i,j)]\)时,就都可以构成结构\(AA\)

\(LCP\)可以利用\(height\)数组套\(ST\)表求解,\(LCS\)其实也是类似,把字符串翻转后接在原字符串处理一下即可。

接下来剩余最后一个问题:如何统计答案。

因为合法的临界点在一段连续的区间,所以说\(a[],b[]\)更改的元素的下标也是一段连续的区间,可以用查分进行统计,但还有一些细节要注意

因为我们现在只要\(A\)的长度是\(len\),所以说如果\(LCP(i,j)\)或者\(LCS(i-1,j-1)\)大于它应该处于的范围,这一段超出范围的长度我们目前应该先不算,否则会与之后计算重复。因此为了避免重复,我们进行以下操作:

\[LCP(i,j) = \min(LCP(i,j), j-i) = \min(LCP(i,j), len) \]

\[LCS(i-1,j-1) = \min(LCS(i-1,j-1),(j-1)-i)=\min(LCS(i-1,j-1), len-1) \]

然后就可以正常进行差分了!

\(a[i - lcs(i-1,j-1)]++, a[i - len + lcp(i,j) + 1]--;\)

\(b[j + len - lcs(i-1,j-1) - 1]++, b[j + lcp(i,j)]--;\)
本题除去模板部分如下:

scanf("%s", s + 1);
ans = 0ll; p = 0; k = 0; m = 300; n = strlen(s + 1);
s[n + 1] = '#';
for(i = n + 2; i <= 2 * n + 1; ++i) s[i] = s[(n + 1) * 2 - i];
n = n * 2 + 1;
                    
//SA模板
                    
n /= 2;
for(len = 1; len * 2 <= n; ++len){
	for(i = len, j = i + len; j <= n; i += len, j += len){
		lcp = min(LCP(i, j), len); lcs = min(LCP(2 * n + 3 - i, 2 * n + 3 - j), len - 1);
		if(lcs + lcp < len) continue;
		a[i - lcs]++; a[i - len + lcp + 1]--;
		b[j + len - lcs - 1]++; b[j + lcp]--;
	}
}
for(i = 1; i <= n; ++i) a[i] += a[i - 1], b[i] += b[i - 1];
for(i = 1; i < n; ++i) ans += 1ll * b[i] * a[i + 1];
printf("%lld\n", ans); 

5、P3975 [TJOI2015]弦论

考虑 \(t=0\) 的时候,跟P2408 不同子串个数 一样,但要多记录一个数组 \(sum[]\) ,其中 \(sum[i]\)表示以所有排名前 \(i\) 的后缀的本质不同的子串的总数,然后找到第一个比 \(k\) 大的 \(sum[p]\) ,输出 \(suf(sa[p])\) 的前 \(k-sum[p-1]+height[p]\) 位即可。

\(t=1\) 的时候自然就不能这么做了,因为相同子串算多个,并且后缀排序是按照字典序排序,所以算排名第 \(k\) 位的子串应该一位一位去确定,这样前 \(p\) 位相同的字符串在一个连续的区间内,就可以二分 \(p+1\) 位应该是什么。还是应该处理出来 \(sum[]\) ,但含义变成前 \(i\) 个后缀所有子串的数量。先确定第一位,在 \([1,n]\) 二分找到第一位字母应该是 \(ch\),然后令 \(k=k-\) 以小于 \(ch\) 开头的子串的个数,然后在第一位为 \(ch\) 的子串的区间内继续二分确定第二位,知道 \(k \le\) 某一位之前相同的子串的数量时结束。具体细节看代码:

if(!opt){
	for(i = 1; i <= n; ++i) sum[i] = sum[i - 1] + n - sa[i] + 1 - ht[i];
	if(pos > sum[n]) cout << -1;
	else{
		p = lower_bound(sum + 1, sum + 1 + n, pos) - sum;
		for(i = sa[p]; i <= n - sum[p] + pos; ++i) cout << s[i];
	}
}else{
	for(i = 1; i <= n; i++) sum[i] = sum[i - 1] + n - sa[i] + 1;
	if(sum[n] < k){
		cout << -1;
		return 0;
	}
	int L = 1, R = n;
	for(i = 1; i <= n; i++){
		tmp = L;
		for(j = 'a'; j <= 'z'; j++){
			l = tmp, r = R;
			while(l <= r){
				mid = l + r >> 1;
				if(s[sa[mid] + i - 1] > j) r = mid - 1;
				else l = mid + 1;
			}
			k = sum[r] - sum[tmp - 1] - (r - tmp + 1) * (i - 1);	//求出区间内不算长度小于i个字符的不同子串数
			if(pos <= r - tmp + 1){									//k <= 前i位相等的数量,说明到这一位结束
				for(j = sa[tmp]; j <= sa[tmp] + i - 1; j++) cout << s[j];
				return 0;
			}
			if(k >= pos){											//如果pos刚好在这位字符为j的时候小于等于k,说明这一位是j
				L = tmp, R = r;										//更改区间范围,确定下一位
				pos -= r - tmp + 1;
				break;
			}
			tmp = r + 1, pos -= k;									//这一位比j大,尝试更大的j
		}
		if(n - sa[L] + 1 == i) L++;									//如果发现排名为L的后缀长度不够了,那么这个后缀就没有用了,因此把它去除
	}
}

6、P6640 [BJOI2020] 封印

题目其实就是多次询问我们一个字符串 \(s\) 的一个子串的所有子串与一个固定的字符串 \(t\) 的子串的 \(lcp\)

\(lcp\) 肯定用 \(height\) 数组,而且是对一个固定的字符串求 \(lcp\) 所以说可以预处理出来每个后缀与 \(t\)\(lcp\) 的最大值 ,储存在 \(val[]\) , 其中 \(val[i] = \max_{p=1}^{\left|t\right|}lcp(i,p)\) ,而这个最大值一定在 \(rk[i]\) 的前驱、后继,所以说把 \(t\) 的所有后缀的排名插入到 \(set\) 中找前驱后继求最大值即可,这个方法具体可以看下一大块中的第七题。现在进行多次询问,询问一个子串的子串与 \(t\)\(lcp\) 的最大值,其实就类似于区间查询 \(val[l\cdots r]\) 的最大值,但其实此时的 \(val[i]\) 的值应该会有所改动,变为了 \(\min(val[i],r-i+1)\) ,因此难以用ST表维护,可是如果我们可以让 \(val[i]\) 的值小于 \(r-l+1\) 就可以了,即我们要消除 \(r-l+1\) 的影响,而这个影响只与它的位置有关,并且答案具有单调性,所以考虑二分答案 \(lcp=mid\) ,然后判断 \(\max_{i=l}^{r-mid+1}val[i]\) 是否 \(\ge mid\) ,不断二分即可。

复杂度 \(O(q\log n+n\log n)\) 。二分、预处理部分代码如下:

S.insert(0); S.insert(n + 1);
for(i = mid + 2; i <= n; ++i) S.insert(rk[i]);
for(i = 1; i <= mid; ++i){
	pre = *(--S.lower_bound(rk[i])); nxt = *S.lower_bound(rk[i]);
	mx[i][0] = max(lcp(pre, rk[i]), lcp(rk[i], nxt));
}
for(j = 1; j <= 18; ++j)
	for(i = 1; i + (1 << j) - 1 <= mid; ++i)
		mx[i][j] = max(mx[i][j - 1], mx[i + (1 << (j - 1))][j - 1]);
scanf("%d", &q);
while(q--){
	scanf("%d%d", &L, &R);
	l = 0, r = R - L + 1;
	while(l + 1 < r){
		pos = (l + r) >> 1;
	if(get(L, R - pos + 1) >= pos) l = pos;
		else r = pos;
	}
	if(get(L, R - r + 1) >= r) printf("%d\n", r);
	else printf("%d\n", l);
}

height 数组与数据结构结合应用

1、P2178 [NOI2015] 品酒大会,height 数组与并查集结合

\(k\)相似,其实就是找\(lcp \ge k\) 的后缀,容易想到先预处理出\(height\)数组。接下来,还容易观察发现:满足条件的后缀一定是一个个连续的区间,并且随着\(k\)的减小,满足条件的后缀单调不降,即将\(k\)从大到小枚举时只会从满足条件的区间向外扩展,这种操作就类似于并查集操作。所以维护并查集,将\(k\)从大到小枚举,每一次加入满足条件的后缀,然后计算新的对数、最大值即可。

注意:

因为有的美味程度是负数,所以有可能最后的最大值是两个负数相乘得到,所以并查集之内要维护最大值、次大值,最小值、次小值。然后再对没有满足条件的后缀的情况特判一下。

代码如下:

struct node{
  int fa, val, mx1, mx2, mn1, mn2, sz;
}a[N];
priority_queue< pair<int, int> > q;

int getfa(int x){
  if(a[x].fa == x) return x;
  return a[x].fa = getfa(a[x].fa);
}

void merge(int x, int y, int t){
  int fx = getfa(x), fy = getfa(y);
  ans[t] += a[fx].sz * a[fy].sz;
  a[fy].sz += a[fx].sz;
  if(a[fx].mx1 >= a[fy].mx1){
  	a[fy].mx2 = max(a[fy].mx1, a[fx].mx2);
  	a[fy].mx1 = a[fx].mx1;
  }else if(a[fx].mx1 > a[fy].mx2) a[fy].mx2 = a[fx].mx1;
  if(a[fx].mn1 <= a[fy].mn1){
  	a[fy].mn2 = min(a[fy].mn1, a[fx].mn2);
  	a[fy].mn1 = a[fx].mn1;
  }else if(a[fx].mn1 < a[fy].mn2) a[fy].mn2 = a[fx].mn1;
  mx[t] = max(mx[t], max(a[fy].mx1 * a[fy].mx2, a[fy].mn1 * a[fy].mn2));
  a[fx].fa = fy;
}

int main(){

//SA板子

  for(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;
  	q.push(make_pair(k, i));
  }
  ans[n] = 0; mx[n] = -INF;
  for(i = n - 1; i >= 0; --i){
  	ans[i] = ans[i + 1]; mx[i] = mx[i + 1];
  	while(!q.empty() && q.top().first >= i){
  		w = q.top().first, k = q.top().second;
  		q.pop();
  		merge(k, sa[rk[k] - 1], i);
  	}
  }
  for(i = 0; i <= n - 1; ++i) printf("%lld %lld\n", ans[i], mx[i] == -INF ? 0 : mx[i]);
}

2、P4094 [HEOI2016/TJOI2016]字符串,height 数组与主席树结合

首先注意一个关键词:\(s[a \cdots b]\)一个 子串和\(s[c \cdots d]\) 这个子串。

这道题让我们对一个区间内部的子串和指定的一个子串去匹配最长子串,我跟之前说过,一般匹配子串的问题都可以转化成求\(lcp\)的问题。并且这道题是一系列的子串去匹配一个固定的子串,也就是求在后缀排序后一堆点到定点的lcp的最大值。而我们知道。排名差距越远的后缀的\(lcp\)一定越小,所以说,到定点的\(lcp\)的最大值一定在后缀排序中它的前驱和后继中。

但是这样并不一定能够保证正确性。因为我们是要求在一个指定区间内的最大值,所以说\(ans_i = \min(lcp(i,c),b-i+1.d-c+1)\),因此,前驱和后缀的\(lcp\)最大,但如果它太靠近b,最终的答案可能比不上其他答案。

因此我么考虑限制\(i\)的取值范围,使他不会出现越界的情况,即使每一个可以选择的\(i\)的最终答案一定就是\(lcp(i,c)\)

可是对于不同的\(lcp\)长度,\(i\)的取值范围也是不一样的,所以说我们考虑二分答案,对于长度\(len(0 \le len \le \min(d-c+1,b-a+1))\),\(i\)的取值范围就是\([a,b-len+1]\)

范围有了,但如何在不超时的前提下多次访问不同区间对应的c的前驱和后继呢?

因为要求固定字符串的一个区间内部对于c的前驱和后继,其实也就是求在这个区间中排名第\(k\)大和第\(k+1\)的后缀的编号(\(k=\)区间内部排名在\(suf(c)\)之前的后缀的个数)。

求区间第\(k\)大,自然就想到了主席树。所以说这道题就是维护一个\(rk[]\)值域的主席树,然后二分答案即可。

复杂度\(O(n + q log^{2} n)\)

下面是主席树和二分答案部分的代码:

int build(int l, int r){
	int rt = ++tot;
	if(l < r){
		int mid = (l + r) >> 1;
		ls[rt] = build(l, mid);
		rs[rt] = build(mid + 1, r);
	}
	return rt;
}

int update(int lrt, int l, int r, int p){
	int rt = ++tot;
	sum[rt] = sum[lrt] + 1;
	ls[rt] = ls[lrt]; rs[rt] = rs[lrt];
	if(l < r){
		int mid = (l + r) >> 1;
		if(p <= mid) ls[rt] = update(ls[lrt], l, mid, p);
		else rs[rt] = update(rs[lrt], mid + 1, r, p);
	}
	return rt;
}

int query_sum(int rt, int lrt, int l, int r, int ql, int qr){
	if(ql <= l && r <= qr) return sum[rt] - sum[lrt];
	int mid = (l + r) >> 1, res = 0;
	if(ql <= mid) res += query_sum(ls[rt], ls[lrt], l, mid, ql, qr);
	if(qr > mid) res += query_sum(rs[rt], rs[lrt], mid + 1, r, ql, qr);
	return res;
}

int query_pos(int rt, int lrt, int l, int r, int k){
	if(k > sum[rt] - sum[lrt]) return n + 1;
	if(l == r) return l;
	int mid = (l + r) >> 1, tmp = sum[ls[rt]] - sum[ls[lrt]];
	if(k <= tmp) return query_pos(ls[rt], ls[lrt], l, mid, k);
	else return query_pos(rs[rt], rs[lrt], mid + 1, r, k - tmp);
}

bool check(int val, int l, int r, int p){
	int pre = query_pos(rt[r], rt[l - 1], 1, n, query_sum(rt[r], rt[l - 1], 1, n, 1, rk[p]));
	int nxt = query_pos(rt[r], rt[l - 1], 1, n, query_sum(rt[r], rt[l - 1], 1, n, 1, rk[p]) + 1);
	return max(lcp(pre, rk[p]), lcp(rk[p], nxt)) >= val;
}

int main(){
  
	//SA模板,lg、st预处理
  
  	rt[0] = build(1, n);
	for(i = 1; i <= n; ++i) rt[i] = update(rt[i - 1], 1, n, rk[i]);
	while(T--){
		scanf("%d%d%d%d", &a, &b, &c, &d);
		l = 0, r = min(d - c + 1, b - a + 1);
		while(l + 1 < r){
			mid = (l + r) >> 1;
			if(check(mid, a, b - mid + 1, c)) l = mid;
			else r = mid;
		}
		if(check(r, a, b - r + 1, c)) printf("%d\n", r);
		else printf("%d\n", l);
	}
}

3、 [AHOI2013]差异 ( SA 与单调栈结合)

题意:给出一个字符串,求: \(\sum_{i=1}^{n-1}\sum_{j=i+1}^{n} (n-i+1)+(n-j+1)-2*lcp(i,j)\)

这个式子可以拆成两部分:\(\sum_{i=1}^{n-1}\sum_{j=i+1}^{n} (n-i+1)+(n-j+1) - 2\sum_{i=1}^{n-1}\sum_{j=i+1}^{n} lcp(i,j)\) 。先考虑第一个式子,将它化简后变成 \(\frac{n(n-1)(n+1)}{2}\) 。现在主要说一下第二个式子:

要求解 \(lcp\) 自然就可以想到用 \(height\) 数组。因为枚举 \(i,j\) 复杂度太高,所以说我们可以换一个角度去想:去看每一个 \(height\) 的值可以是多少对 \(i,j\)\(lcp\) ,其实也就是在后缀数组中对于 \(i\) ,找到两个集合 \(S, T\), 使得 \(\forall p \in S, \forall q \in T\) 都有 \(lcp(p,q)=height[i],p\le i,q \ge i\) ,并让这两个集合的大小尽可能大,此时这个 \(height[i]\) 的贡献就是 \(2*height[i]*l[i]*r[i]\) (为了方便记 \(l[i]=\left|S\right|,r[i]=\left|T\right|\) )。因为 \(lcp(sa[p],sa[q])=\min_{w=p+1}^{q}height[w]\) ,所以容易发现这两个集合都是后缀排序中连续的一段,而要满足这个条件其实就是让集合内部与 \(suf(sa[i])\)\(lcp\) 都不小于 \(height[i]\) ,可以跑两边单调栈去维护,当然也可以跑一遍,但思维量较大,具体见CF802I Fake News (hard) 中的方法。

综上所述,式子可以变成:

\[\frac{n(n-1)(n+1)}{2}-2\sum_{i=1}^{n}height[i]*l[i]*r[i] \]

时间复杂度 \(O(n\log{n})\) 。单调栈代码如下:

st.push(n + 1);
for(i = n; i > 1; --i){
	while(st.size() > 1 && ht[i] <= ht[st.top()]) st.pop();
	r[i] = st.top() - i;
	st.push(i);
}
while(!st.empty()) st.pop();
st.push(1);
for(i = 2; i <= n; ++i){
	while(st.size() > 1 && ht[i] < ht[st.top()]) st.pop();
	l[i] = i - st.top();
	st.push(i);
}
for(i = 1; i <= n; ++i) ans -= 2 * ht[i] * l[i] * r[i];

4、P318suf [HAOI2016]找相同字符( SA 与单调栈结合)

本题要对两个字符串进行匹配,可以对两个字符串的每个后缀的每个前缀进行对比判断。容易发现,对于\(s1\)的后缀\(i\)\(s2\)的后缀\(j\),只要是\(lcp(i, j)\)的子串都可以匹配成功,即\(ans=\sum_{i=1}^{\left|s1\right|}\sum_{j=1}^{\left|s2\right|}lcp(i,j)\) 因此想到\(height\)数组,将\(s2\)连在\(s1\)后边,中间用一个没出现过的字符隔开,求解height数组然后枚举判断。 但是枚举\(i,j\)复杂度\(O(n^2)\)会TLE,因此考虑优化。

因为\(lcp(i,j)\)\(\min(height[rk[i]+1],\cdots,height[rk[j]])\),所以求每个后缀为匹配做的贡献就是求在多大的范围之内它对应的\(height\)值为最小值,因此容易想到用单调栈去维护。先用\(s1\)匹配\(s2\),再用\(s2\)匹配\(s1\)。现在具体说一下\(s1\)匹配\(s2\)的具体方法,另一部分类似:

对于值\(height[i]\),它可以更新\(j\)(满足\(height[j] < height[i],height[j+1 \cdots i-1]\ge height[i])\))之后、\(i\)之前的元素。而且\(j\)可以匹配到的数量i也可以匹配到(可以把\(height\)数组当成一个台阶,\(height[i],height[j]\) 是两层台阶的高度,可以匹配到的数量就是台阶的体积,\(i\)\(j\)高,所以说\(j\)以下台阶的体积一定也在\(i\)以下),所以\(ans[i] = ans[j] + height[i] * (i,j之间属于s2的字符的数量,不包括j)\)。其中\(i,j\)之间属于s2的字符的数量可以用前缀和计算。最后将属于\(s1\)的后缀的答案加起来即可,记为\(res_{s1}\)。用\(s2\)匹配\(s1\)的方法相同,只是要用属于\(s2\)的后缀去更新答案。求属于\(s1\)的后缀的数量的前缀和就行,然后将属于\(s2\)的后缀的答案加起来记为\(res_{s2}\)。最终答案就是\(res_{s1}+res_{s2}\)

单调栈匹配部分代码如下:

for(i = 1; i <= n; ++i){
	f[i] = f[i - 1]; g[i] = g[i - 1];
	if(sa[i] <= mid) f[i]++;
	if(sa[i] > mid + 1) g[i]++;
}
st.push(make_pair(1, 0)); 
for(i = 2; i <= n; ++i){													//s1匹配s2 
	while(st.size() > 1 && ht[i] <= ht[st.top().first]) st.pop();
	k = (g[i - 1] - g[st.top().first - 1]) * ht[i] + st.top().second;		//first是height的下标,second是ans[] 
	st.push(make_pair(i, k));
	if(sa[i] <= mid) ans += 1ull * k;
}
while(!st.empty()) st.pop(); st.push(make_pair(1, 0));
for(i = 2; i <= n; ++i){													//s2匹配s1 
	while(st.size() > 1 && ht[i] <= ht[st.top().first]) st.pop();
	k = (f[i - 1] - f[st.top().first - 1]) * ht[i] + st.top().second;
	st.push(make_pair(i, k));
	if(sa[i] > mid + 1) ans += 1ull * k;
}
printf("%llu", ans);

5、CF802I Fake News (hard)( SA 与单调栈结合——新感悟)

题意:所有本质不同的子串出现次数的平方和。

因为要统计出现次数,容易想到 \(height\) 数组,维护一个单调栈统计出来 \(height[i]\) 这个长度的贡献,即分别从左往右、从右往左跑两边单调栈找到最大的区间\([l_i, r_i]\)使得这个区间内部所有后缀的 \(lcp\) 长度至少为 \(height[i]\) ,然后贡献 \(val=heigh[i]*(r_i-l_i+1)^2\),最终的答案就是:

\[ans = \sum_{i=1}^{n}height[i]*(r[i]-l[i] + 1)^2 \]

但很可惜,这样写答案错的十分彻底。 但为什么会错呢?有两个原因:

1、本题是要求本质不同的子串出现的次数的平方的和,但如果按照上面的式子去计算,从左往右跑单调栈当遇到\(height[i]\le height[st.top()]\),我们会进行\(st.pop()\) 操作,这样对于以 \(suf(sa[i])\) 来说它出现的次数+1,贡献计算没有任何问题,而对于 \(height[st.top()]\) 来说,在子串长度为 $ [height[i]+1,height[st.top()]]$ 时也没有问题,它无法再向右扩展,可是这个时候我们计算的是 \(height[st.top()]*(r[st.top()] - l[st.top()] + 1) ^ 2\) 而并没有把已经统计过的字串长度在 \([1,height[i]]\) 这一部分重复计算的贡献减去,即重复计算了长度较短的子串的贡献。

2、\(height\) 表示的是两个字符串之间的关系,也就是说只要是在这里统计上贡献的子串出现的次数一定至少为2次,换句话说,我们少统计了出现次数是1的子串的贡献,所以答案就统计少了。

综上所述:上面的式子统计多了一部分,统计少了一部分,错的不能再错了。

那么如何去把这个式子改对呢?

先去考虑第一个问题:首先要明确一点这道题和一般的题的区别:我们要求的是本质相同的子串出现的个数,也就是说我们是要把每个本质不同的子串当做一部分去考虑,而不能再去将 \(height\) 当做一个整体去处理,否则就会有多余计算的情况出现。这道题与 [AHOI2013]差异 这道题是很不一样的,《差异》是要求出每个 \(height[i]\) 能够对于多少对 \(lcp(i,j)\) 做出贡献,强调的是位置,而不是像本题强调的是本质不同子串出现的次数。也因此我们就不是用 \(height\) 数组求贡献,而是以本质不同的子串为一个单位去计算贡献,采取另一种策略:不去考虑每一个 \(height\) 这个整体对于答案的贡献,而是去考虑每一种子串对于答案的贡献,即换句话说为了避免重复计算的情况我们可以先找到一个固定的长度,然后把等于这个长度的所有本质相同的子串出现的次数计算出来,计算出所有这个长度的本质不同的子串的贡献,再把各种长度的贡献加起来,这样就不会出现上面那种重复计算短子串贡献的情况。

可是去枚举子串长度相当于把 \(height\) 数组的作用完全忽略了,复杂度也肯定不过关,所以应该去考虑优化。

我们想要去利用 \(height\) 数组,其实就是想去利用已知的 \(lcp\) 的长度,那么如何去利用呢?其实仔细考虑一下我们之前重复计算的部分,发现只要能够判断数来哪一部分是要计算的、哪一部分是它新贡献的就行,而这其实就是当前的 \(height[i]\) 减去比左右两边第一个比它低 \(height[j]\) 的大小 \(mx[i]\) 即可(因为 \(height[j]\) 一定会将 \(height[i]\) 中的短子串的贡献计算过一遍)。此时出现超过一次的子串的总贡献为:

\[\sum_{i=1}^{n}(height[i]-mx[i])*(r[i]-l[i]+1)^2 \]

如何找到左右两边第一个比它小的 \(height[j]\) 呢? 在这里有两种方法:

1、还是跑两边单调栈,在记录区间范围的同时分别记录左、右两边第一个 \(height\) 小于它的位置的 \(height\) 值然后比较一下大小就行。当然,就像一般的单调栈套路一样,因为会有相等的 \(height\) 值出现,但我们既不能两边遍历都将对方 \(pop\) 掉,这样仍然会出现问题一;也不能都保留,否则这种子串的出现次数会少算。因此随便定一个方向,让这个方向的遍历取等,另一边不取等就行。

2、考虑单调栈在 \(pop\) 时的条件是什么:当 \(height[i] < height[st.top()]\) 时进行 \(pop\) ,而当我们从左往右扫时这个操作其实也就是让我们找到了 \(st.top()\) 右边第一个比它小的 \(height\) 值,这个值的位置就是 \(i\) ,因此我们可以在进行 \(pop\) 操作时再统计当时的 \(st.top()\) 的贡献(\(st.top()\) 左边第一个比他小的是此时 \(st.top()\) 的上一个元素,右边第一个比它小的是 \(i\) )。这样只需要进行一次单调栈即可。但还有一点要注意,因为在进行到 \(height[n]\) 之后栈内依然有元素,并且这些元素的贡献没有计算,所有要增加一个模拟结束点 \(n+1\) ,并且 \(height[n+1]=0\) ,这样可以保证使所有元素的贡献都计算完毕,并且考虑实际意义:右边没有元素,所以可以到达0长度。

现在来讨论第二个问题:如何处理只出现过一次的子串。我们想要把每个位置开头的只出现一次的子串从中剥离出来其实就是用后缀总长度-最长的出现两次及以上的子串的长度(这个长度只会从在后缀排序中与它相邻的元素中产生),即以 \(i\) 开头的子串中只出现一次的子串的个数为: \(sum_i=n-i+1-\max(ht[rk[i]],ht[rk[i]+1])\) 。(以上方法为口胡,没有代码验证)。

接下来我们说另一种方法:既然无法剥离,那我们其实就可以先把所有子串都当成只出现过一次,然后再减去重复计算的次数即可。换句话说,就是让 \(ans\) 的初始值为 \(\frac{n(n+1)}{2}\) ,然后跑单调栈计算贡献。但要注意此时如果一种子串出现了 \(p\) 次,新增加贡献就不是 \(p^2\),而是 \(p^2-p\) ,这样才能将初始值多增加的这 \(p\) 个子串的贡献抵消掉。

到这里这个问题就结束啦,下面给出两种统计贡献的代码(注意: 代码中的 \(l_i,r_i\) 不表示可以做贡献的区间范围,而只是为了代码间接,计算方便而定义,真正的区间的范围用代码中 \(l_i,r_i\) 的含义表示应该是 \([l_i,r_i-1]\)):

//跑两遍单调栈的方法
ht[0] = ht[n + 1] = 0; ans = (n + 1) * n / 2;
while(!st.empty()) st.pop(); st.push(0);
for(i = 1; i <= n; ++i){
	while(st.size() > 1 && ht[i] < ht[st.top()]) st.pop();
	mx[i] = ht[st.top()]; l[i] = st.top();
	st.push(i);
}
while(!st.empty()) st.pop(); st.push(n + 1);
for(i = n; i >= 1; --i){
	while(st.size() > 1 && ht[i] <= ht[st.top()]) st.pop();
	mx[i] = max(mx[i], ht[st.top()]); r[i] = st.top();
	st.push(i);
}
for(i = 1; i <= n; ++i) ans += (ht[i] - mx[i]) * (r[i] - l[i]) * (r[i] - l[i] - 1);
//跑一遍单调栈的方法
while(!st.empty()) st.pop();
st.push(1); ans = n * (n + 1) / 2;
for(i = 2; i <= n + 1; ++i){
	while(st.size() > 1 && ht[i] < ht[st.top()]){
		h = ht[st.top()];
		st.pop();
		lst = ht[st.top()]; len = i - st.top();
		ans += (len * len - len) * (h - max(lst, ht[i]));
	}
	st.push(i);
}

有一道及其类似的题目 CF123D String,思路与本道题完全一样,只是把计算贡献的式子换一下即可。

6、CF204E Little Elephant and Strings( SA 与单调队列结合、多字符串公共子串匹配问题)

多个字符串匹配的问题,按照固定的套路,把所有字符串连接在一起,中间用没有出现过的字符间隔开(注意:这里有一个细节,因为这道题最多会有\(1e5\)个字符串,所以最好把字符型转成整型,同时要注意定义个间隔符不要越界!!!!(之前就因为这个问题调了一周...)。然后题目让找相同的字符串,很自然就能想到用\(height\)数组,找\(lcp\)。如果\(lcp\)满足条件那么\(lcp\)的子串一定也满足条件,即找出\(lcp\)之后它会产生的贡献就是\(\left|lcp\right|\)。所以说先把板子敲上去。

现在只剩一个问题,就是如何计算每个后缀的贡献:

因为只有当后缀\(suf(i)\)出现在\(k\)个不同的字符串的时候才能产生贡献,而从\(lcp\)求解的方法就能看出来,要求覆盖的区间越大,区间内部的 \(lcp\) 就一定越小。因此就可以得出:\(suf(i)\)的贡献就是在后缀排序中:所有包含\(rk[i]\)的,含有来自\(k\)个不同字符串的后缀的,以不同后缀为结尾,长度尽可能小区间的\(lcp\)的最大值。其中以不同后缀为结尾的,长度尽可能小的区间的含义是:1、所有满足条件区间\([l_i,r_i]\),\(r_i\)互不相同;2、任何区间\([l_i,r_i]\)满足\([l_i,r_i]\)中有来自\(k\)个不同字符串的后缀(记为条件1)并且\([l_i+1,r_i]\)没有有来自至少\(k\)个不同字符串的后缀。通俗一点说就是首先对于后缀排序中的每一个元素,求出来以它结尾的最短的满足条件1的区间并保存,然后在这些区间中找出所有包含\(rk[i]\)的区间,找出这些区间中\(lcp\)的最大值就是它的贡献。

所有的区间很容易就可以预处理出来,把后缀排序上每个后缀打上一个标记 \(pos\) 表示它是哪个字符串的后缀,然后存一个\(cnt\)数组记录当前每个字符串的后缀出现了几次。然后把区间的 \(r\) 从1到 \(n\) 枚举,再不断增大\(l\)的数值知道刚好再+1就不满足条件1为止。

但如何求包含后缀 \(suf(i)\) 的所有区间的\(lcp\)的最大值呢?

因为区间的 \(l,r\) 大小肯定是单调不降的,而我们从后缀排序中的排名1的后缀遍历到排名n的后缀的时候下标也是单调递增的,也就是说如果区间 \(i\) 不包含排名 \(p\) 的后缀,那也一定不包含排名 \(p+1\) 的后缀,即要靠后的区间对于以后的后缀作用就越大,这就很像单调队列可以解决的问题,单调队列队首维护 \(lcp\) 最大的区间,队列内部 \(lcp\) 单调递减,如果队首区间无法覆盖当前后缀就一直 \(pop\) 队首,如果队尾 \(lcp\le\) 当前 \(lcp\) 就一直 \(pop\) 队尾,最后所有覆盖 \(suf(sa[i])\) 的区间的 \(lcp\) 的最大值就是队首的 \(lcp\) 。这道题到这里就结束了。

这里给出合并字符串,预处理区间和单调队列部分的代码:

//主函数中:

for(i = 1; i <= T; ++i){
	scanf("%s", tmp + 1); p = strlen(tmp + 1);
	for(j = 1; j <= p; ++j) s[++n] = tmp[j], pos[n] = i, len[n] = p - j + 1;
	s[++n] = ('z' + i);
}
s[n--] = ('z' + T);

//模板...

//pos[i]表示suf(i)属于的字符串的编号,T是字符串数量,n是合并所有字符串之后的总长度
for(i = 1; i <= T; ++i) cnt[i] = 0;
//这里T只枚举到n-T+2是因为其余部分都是间隔字符,没有贡献,没必要去算。
for(l = 1, r = 1, p = 0; r <= n - T + 2; ++r){
	if(!cnt[pos[sa[r]]]) p++;
	cnt[pos[sa[r]]]++;
	while(p >= k){
		if(p == k) v.push_back(node(l, r, lcp(l, r))), mx = max(mx, lcp(l, r));
		if(cnt[pos[sa[l]]] == 1){
			if(p == k) break;
			p--;
		}
		cnt[pos[sa[l]]]--;
		l++;
	}
}
p = 0;
for(i = 1; i <= n - T + 2; ++i){
	while(!q.empty() && v[q.front()].r < i) q.pop_front();
	while(p < v.size() && v[p].l <= i){
		while(!q.empty() && v[p].val >= v[q.back()].val) q.pop_back();
		q.push_back(p);
		p++;
	}
	ans[pos[sa[i]]] += v[q.front()].val;
}

7、CF427D Match & Catch SA 与 set 结合

我也不知道 \(set\) 是不是数据结构,但就先放到这一块儿

题意:给出两个字符串 \(s1,s2\),要找到这两个字符串的一个公共子串 \(ss\) ,使得 \(\forall i \in [1,\left|s1\right|-\left|ss\right|+1],\forall j \in [1,\left|s2\right|-\left|ss\right|+1]\) ,满足 \(s1[i\cdots i+\left|ss\right|-1]\ne ss,s2[j\cdots j+\left|ss\right|-1]\ne ss\) 并且使得 \(\left|ss\right|\) 最小。

先看前提条件:是公共子串,因此可以求 \(height\) 数组,然后就可以 \(O(n^2)\) 枚举子串的开头,用ST表算一下也就知道了所有 \(lcs\)\(lcs\) 的前缀就是公共子串。

接下来考虑第二个条件:在同一个字符串内不能出现多次。越长越不容易出现多次,所以先判断每种 \(lcs\) 是否合法,如果 \(lcs(i,j)\) 不合法,那么以 \(i,j\) 开头的所有公共子串肯定都不合法。其实就是说对于以 \(i\) 开头的 \(lcs\) 不能有 \(lcp(i,j)\ge lcs\) ( \(i,j\) 在为同一个字符串的下标) 。而我们知道排名越近的两个后缀的 \(lcp\) 一定越大,因此只要找到 \(i\) 在排名中的前驱后继,比教一下它们与 \(i\)\(lcp\) 是否比 \(lcs\) 就行,这个操作可以用 set 进行。建立两个 \(set:S1,S2\) ,分别插入 \(s1,s2\) 的后缀,按照排名排序即可找到前驱后继。

如何找到最短?其实也很简单,只要满足以上两个条件的 \(ss\) 中求最小的。可以先考虑让以 \(i,j\) 开头的 \(ss\) 最小,容易想到 \(ss\) 最小就是让它的长度刚好满足要求,即让最大的 \(lcp+1\) 即是答案。然后 \(O(n^2)\) 找每个位置的最小的是 \(ss\) 的最小值即可。

因为 set 带一个 \(\log\) ,最终复杂度为 \(O(n^2 \log{n})\) 。判断、统计部分代码如下:

S.insert(0); S.insert(n + 1); T.insert(0); T.insert(n + 1);
for(i = 1; i <= mid; ++i) S.insert(rk[i]);
for(i = mid + 2; i <= n; ++i) T.insert(rk[i]);
for(i = 1; i <= mid; ++i){
	for(j = mid + 2; j <= n; ++j){
		w = lcp(rk[i], rk[j]);
		pre = *(--S.lower_bound(rk[i])); nxt = *(++S.lower_bound(rk[i]));
		p = max(lcp(pre, rk[i]), lcp(rk[i], nxt));
		if(p < w){
			pre = *(--T.lower_bound(rk[j])); nxt = *(++T.lower_bound(rk[j]));
			k = max(lcp(pre, rk[j]), lcp(rk[j], nxt));
			if(k < w) ans = min(ans, max(p, k) + 1);
		}
	}
}
if(ans == INF) puts("-1");
else printf("%d\n", ans);

height 数组与其他字符串算法结合应用

1、P3649 [APIO2014]回文串 (SA 与 manacher 结合)

先不考虑回文串这个限制,十分明显可以用 SA 做,求出现次数可以在后缀排序之后二分左右边界,用ST表去判断。求出来这些之后求个存在值的最大值即可,复杂度\(O(n\log n)\)

现在要求这个字符串应该是个回文串,自然就想到用 \(manacher\) 算法。但如果是每个回文串都算一下复杂度有可能退化到 \(O(n^2\log n)\) ,是会超时的,因此我们应该思考一下 \(manacher\) 是如何将寻找回文串复杂度降到 \(O(n)\) 的。其实就是对于相同的回文串它会跳过,那我们要找贡献,而相同的回文串贡献也一定是相同的,所以说对于相同的回文串我们也可以跳过,只对于使 \(r\) 增大了的回文串进行计算,这样复杂度就变成了 \(O(n \log n)\)

但是还有一些细节是需要处理的:因为我们在做 \(manacher\) 算法时增加了#字符,而回文串的实际有效长度为 \(d[i]\) ,并且这个 \(d[i]\) 指的是边界到达 # 时的 \(d[i]\) ,而**不是任何时候回文串长度都是 \(d[i]\) **,所以只要在边界到达#并且超过 \(r\) 时才去更新。具体内容看代码:

void work(int x, int val){
	if((x & 1) == 0) return;		//细节要注意,如果边界不是#就不要更新答案,否则会WA
	x = (x + 1) >> 1;
	int l, r, mid, L, R;
	l = 1, r = rk[x];
	while(l + 1 < r){
		mid = (l + r) >> 1;
		if(lcp(mid, rk[x]) >= val) r = mid;
		else l = mid;
	}
	if(lcp(l, rk[x]) >= val) L = l;
	else L = r;
	l = rk[x], r = n;
	while(l + 1 < r){
		mid = (l + r) >> 1;
		if(lcp(rk[x], mid) >= val) l = mid;
		else r = mid;
	}
	if(lcp(rk[x], r) >= val) R = r;
	else R = l;
	mx = max(mx, 1ll * val * (R - L + 1));
}
int main(){

	//SA板子
	
		for(i = 1; i <= n; ++i){
		tmp[i << 1] = s[i];
		tmp[i << 1 | 1] = '#';
	}
	tmp[1] = '#'; N = (n << 1) | 1;
	for(i = 1, mid = 0, r = 0; i <= N; ++i){
		if(i <= r) d[i] = min(r - i, d[(mid << 1) - i]);
		while(i - d[i] > 1 && i + d[i] < N && tmp[i - d[i] - 1] == tmp[i + d[i] + 1]){
			d[i]++; work(i - d[i], d[i]);			//能进入这个循环一定都超过r
		}
		if(i + d[i] > r){
			work(i - d[i], d[i]);
			r = i + d[i];
			mid = i;
		}
	}
	printf("%lld\n", mx);
}
posted @ 2023-02-24 16:15  _zyc  阅读(167)  评论(1编辑  收藏  举报