后缀数组学习笔记

后缀数组是一种数据结构,用于高效地处理字符串的各种问题,如最长公共前缀、重复子串等。本文介绍了后缀数组的定义、如何通过基数排序和倍增法求解,以及高度数组的计算。此外,还探讨了后缀数组在解决区间重复出现最长子串、本质不同子串个数等实际问题中的应用。
摘要由CSDN通过智能技术生成

后缀数组学习笔记

什么是后缀数组

后缀的数组

后缀数组是指将一个字符串的所有后缀按照字典序从小到大排序后的数组。
这里我们用 i i i代表从 i i i开始直到结尾的后缀。
定义两个数组 s a , r k sa, rk sa,rk
s a i sa_i sai表示排名为 i i i的后缀的编号
r k i rk_i rki表示编号为 i i i的后缀的排名
容易发现 r k s a i = s a r k i = i rk_{sa_i} = sa_{rk_i} = i rksai=sarki=i
例:
s = “ a b a b a ” s = “ababa” s=ababa

i i i编号为 i i i的后缀 r k i rk_i rki s a i sa_i sai
1 a b a b a ababa ababa35
2 b a b a baba baba53
3 a b a aba aba21
4 b a ba ba44
5 a a a12

(注:我们认为空串字典序是最小的,即一个串的任意前缀的字典序都小于这个串)

怎么求后缀数组

一个很显然的方法是把它们都拿出来 s o r t sort sort一下
这个方法显然是不对的,因为 s t r i n g c o m p a r e string compare stringcompare的复杂度是 O ( ∣ S ∣ ) O(|S|) O(S)的,这么做 O ( ∣ s ∣ 2 l o g ∣ s ∣ ) O(|s| ^ 2 log |s|) O(s2logs)你人就没了
这里介绍比较普遍的做法:倍增法

大体思想:
i i i次将从每个元素开始,长度为 2 i 2 ^ i 2i的所有子串排序。
这样做 l o g ∣ s ∣ log|s| logs次之后,当前排好序的所有串就都为原串后缀了。

具体实现:
假设已经把所有长度为 2 i 2 ^ i 2i的字符串排好了序。
要想对长度为 2 i + 1 2 ^ {i + 1} 2i+1的字符串排序,显然我们需要先比较前 2 i 2 ^ i 2i位,如果不相同则比较后 2 i 2 ^ i 2i位。
这时我们发现,前后 2 i 2 ^ i 2i位都不需要逐位比较了,因为上一轮已经比较完了,所以只需要比较上一轮排出来的 r k rk rk即可
一个简单的做法是把所有点按照 p a i r ( r k j , r k j + 2 i ) s o r t pair(rk_j, rk_{j + 2 ^ i}) sort pair(rkj,rkj+2i)sort一遍。这样显然是 O ( ∣ s ∣ l o g 2 ∣ s ∣ ) O(|s| log^2 |s|) O(slog2s)网上博客貌似也有这样写的,大多数题目或许也能过,但是可以通过一种并不是很麻烦的做法把复杂度降低到 O ( ∣ s ∣ l o g ∣ s ∣ ) O(|s| log |s|) O(slogs)

基数排序

我们注意到, 1 &lt; = r k i &lt; = ∣ s ∣ 1 &lt;= rk_i &lt;= |s| 1<=rki<=s
值域较小的情况下,我们有一种不基于比较的线性排序法——基数排序法
其实非常简单,就是将每个值的数量算出来,再在值上从前到尾做一遍前缀和,这样一个值上记录的就是小于等于这个值的数字个数了。到这里已经很显然了,因为一个数排名其实就是小于这个数的数字个数+1 。注意一下相等的情况即可。
代码:

void sort(int a[])
{
	int m = 0; 
	for(int i = 1; i <= n; i++)
	{
		cnt[a[i]]++; 
		m = max(m, a[i]); 
	}
	for(int i = 1; i <= m; i++)
		cnt[i] += cnt[i - 1]; 
	
	for(int i = 1; i <= n; i++)
		s[cnt[a[i]]--] = a[i]; 
	
	for(int i = 1; i <= n; i++)
		a[i] = s[i]; 
	
	for(int i = 1; i <= m; i++)
		cnt[i] = 0; 

现在我们要进行双关键字的排序,怎么办呢?如果要将第一关键字乘上一个足够大的值再加上第二关键字,会导致值域退化到平方级别。但实际上,由于基数排序是稳定排序算法,我们只需先对第二关键字进行排序,再对第一关键字进行排序即可。在计算 s s s数组的时候倒序计算,就能确保第一关键字相等的元素能够按照原序也就是第二关键字排序。证明较为简单,请读者自行思考。
到这里,我们就已经完全掌握了后缀数组的构造。在循环最后还需更新 r k rk rk数组,注意相同元素排名需一致即可。

模板:后缀排序

代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std; 

const int N = 2e6 + 5; 
struct SA
{
	int n, rk[2 * N], sa[N], trk[N], tmp[N], cnt[2 * N]; 
	char s[N]; 
	void read()
	{
		scanf("%s", s + 1); 
		n = strlen(s + 1);  
	}
	void build()
	{
		int m = 0; 
		for(int i = 1; i <= n; i++)
		{
			cnt[s[i]]++; 
			m = max(m, int(s[i])); 
		}
		
		for(int i = 1; i <= m; i++)
			cnt[i] += cnt[i - 1]; 
		
		for(int i = n; i >= 1; i--)
			sa[cnt[s[i]]--] = i; 
		
		rk[sa[1]] = 1; 
		int p = 1; 
		for(int i = 2; i <= n; i++)
			rk[sa[i]] = ((s[sa[i]] == s[sa[i - 1]]) ? p : ++p); 
		
		for(int k = 1; k <= n; k <<= 1)
		{
			for(int i = 0; i <= m; i++)
				cnt[i] = 0; 
			m = 0; 
			
			for(int i = 1; i <= n; i++)
			{
				cnt[rk[i + k]]++; 
				m = max(m, rk[i + k]); 
			}
			
			for(int i = 1; i <= m; i++)
				cnt[i] += cnt[i - 1]; 
			
			for(int i = n; i >= 1; i--)
				tmp[cnt[rk[i + k]]--] = i; 
				
			for(int i = 0; i <= m; i++)
				cnt[i] = 0; 
			m = 0; 
			
			for(int i = 1; i <= n; i++)
			{
				cnt[rk[tmp[i]]]++; 
				m = max(m, rk[tmp[i]]); 
			}
			for(int i = 1; i <= m; i++)
				cnt[i] += cnt[i - 1]; 
			
			for(int i = n; i >= 1; i--)
				sa[cnt[rk[tmp[i]]]--] = tmp[i]; 
				
			trk[sa[1]] = 1; 
			int p = 1; 
			for(int i = 2; i <= n; i++)
				trk[sa[i]] = ((rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + k] == rk[sa[i - 1] + k]) ? p : ++p); 
			
			for(int i = 1; i <= n; i++)
				rk[i] = trk[i]; 
		}
	}
}S; 
int main()
{
	S.read(); 
	S.build(); 
	for(int i = 1; i <= S.n; i++)
		printf("%d ", S.sa[i]); 
	return 0; 
}

height数组

如果只能做模板题,后缀数组貌似也没有什么用。真正让后缀数组发挥出威力的是 h e i g h t height height数组。

h e i g h t height height数组的定义: h e i g h t i height_i heighti表示排序后排名为 i i i的的后缀和排名为 i − 1 i - 1 i1的后缀的最长公共前缀的长度

h e i g h t i = s t r l e n ( L C P ( s a i , s a i − 1 ) ) height_i = strlen(LCP(sa_i, sa_{i - 1})) heighti=strlen(LCP(sai,sai1))
特别地, h e i g h t 1 height_1 height1是没有意义的,为了方便可以把它设成 0 0 0
例:
s = “ a b a b a ” s = “ababa” s=ababa

i i i编号为 i i i的后缀 r k i rk_i rki s a i sa_i sai h e i g h t i height_i heighti
1 a b a b a ababa ababa350
2 b a b a baba baba531
3 a b a aba aba213
4 b a ba ba440
5 a a a122
height数组的求法

h e i g h t height height数组有一个非常巧妙的线性求法。

定理: h e i g h t r k i &gt; = h e i g h t r k i − 1 − 1 height_{rk_i} &gt;= height_{rk_{i - 1}} - 1 heightrki>=heightrki11

h i = h e i g h t r k i h_i = height_{rk_i} hi=heightrki, 即等价于 h i &gt; = h i − 1 − 1 h_i &gt;= h_{i - 1} - 1 hi>=hi11
证明:
假设 h i − 1 = j h_{i - 1} = j hi1=j
j = 0 j = 0 j=0,
则等式直接成立;
j &gt; 0 j &gt; 0 j>0,
i − 1 i - 1 i1 s a r k i − 1 − 1 sa_{rk_{i - 1} - 1} sarki11这两个后缀的前 j j j位是相同的
那么将它们同时去掉首位
则有 i i i s a r k i − 1 − 1 + 1 sa_{rk_{i - 1} - 1} + 1 sarki11+1这两个后缀的前 j − 1 j - 1 j1位是相同的
由于 s a r k i − 1 − 1 sa_{rk_{i - 1} - 1} sarki11排在 i − 1 i - 1 i1前面
又由于 j &gt; 0 j &gt; 0 j>0,所以它们首位相等
则将它们去掉首位后,大小关系不变
所以 s a r k i − 1 − 1 + 1 sa_{rk_{i - 1} - 1} + 1 sarki11+1排在 i i i前面
该条件等价于 s a r k i − 1 − 1 + 1 &lt; = s a r k i − 1 sa_{rk_{i - 1} - 1} + 1 &lt;= sa_{rk_{i - 1}} sarki11+1<=sarki1
最感性的一步来了

s s s 是任意按字典序从小到大排好序的字符串数组,当 i i i固定且 j &lt; i j &lt; i j<i时, L C P ( s i , s j ) LCP(s_i, s_j) LCP(si,sj)的长度随着 i − j i - j ij的上升而单调不降

什么意思呢,就是眼前的 s a i sa_i sai,因为排好序了,所以显然在它前面和它最相似(也就是 L C P LCP LCP最长)的后缀一定是 s a i − 1 sa_{i - 1} sai1
比如以下几个串
a , a b a , a b a b a , b a , b a b a a, aba, ababa, ba, baba a,aba,ababa,ba,baba
此时已经排好序了,那么排名在 a b a b a ababa ababa前面,和它 L C P LCP LCP最长的是哪个串呢?一定是排名在比它小1的那个串
(请自行琢磨一下吧qwq 我也是想了半天才大概理解了)
所以,由于 s a r k i − 1 − 1 + 1 &lt; = s a r k i − 1 sa_{rk_{i - 1} - 1} + 1 &lt;= sa_{rk_{i - 1}} sarki11+1<=sarki1,则 L C P ( i , s a r k i − 1 − 1 + 1 ) &lt; = L C P ( i , s a r k i − 1 ) LCP(i, sa_{rk_{i - 1} - 1} + 1) &lt;= LCP(i, sa_{rk_{i - 1}}) LCP(i,sarki11+1)<=LCP(i,sarki1)
h i &gt; = h i − 1 − 1 h_i &gt;= h_{i - 1} - 1 hi>=hi11
有了这个性质,则 h e i g h t height height数组就可以在线性时间内求出来了。每次从 h i − 1 − 1 h_{i - 1} - 1 hi11开始暴力匹配即可。
代码:

	int j = 0; 
	for(int i = 1; i <= n; i++)
	{
		if(rk[i] == 1)continue;  
		while(s[i + j] == s[sa[rk[i] - 1] + j])j++; 
		h[rk[i]] = j; 
		if(j)j--; 
	}

注:这里的 h h h数组即为前文的 h e i g h t height height数组。

后缀数组的应用

有了 h e i g h t height height数组,后缀数组的功能就很强大了。它可以把一系列字符串问题转化为序列问题。
举几个经典问题,有些有原题,有些没找到

任意两个后缀的最长公共前缀

s 1 , s 2 s1, s2 s1,s2为两个任意串
s 1 &lt; = s 2 s1 &lt;= s2 s1<=s2
则有 L C P ( s 1 , s 2 ) = m i n ( L C P ( s 1 , s ) , L C P ( s , s 2 ) ) ( s 1 &lt; s &lt; s 2 ) LCP(s1, s2) = min(LCP(s1, s), LCP(s, s2)) (s1 &lt; s &lt; s2) LCP(s1,s2)=min(LCP(s1,s),LCP(s,s2))(s1<s<s2)
这里的 &lt; &lt; <指字典序 &lt; &lt; <
L C P LCP LCP函数返回的是长度
为什么呢?下面口胡给一个简要证明

如果 s 1 s1 s1的第一位不等于 s 2 s2 s2的第一位
s s s的第一位要么不等于 s 1 s1 s1的第一位 要么不等于 s 2 s2 s2的第一位
所以等式右边 m i n min min函数中总有一项是0
而等式左边为0
所以成立
如果 s 1 s1 s1的第一位等于 s 2 s2 s2的第一位
则由于字典序限制, s s s的第一位必然也等于它们的第一位
那么把第一位删去,必然不影响字典序的顺序
则把第一位删去,左右两边都加上 1 1 1,递归做下去即可。

因此,
i i i, j j j为编号为 i , j i, j i,j的后缀
r k i &lt; = r k j rk_i &lt;= rk_j rki<=rkj
将排名在它们中间的每一个后缀依次当做中间串 s s s
L C P ( i , j ) = m i n ( h e i g h t r k i , h e i g h t r k i + 1 , . . . , h e i g h t r k j ) LCP(i, j) = min(height_{rk_i}, height_{rk_{i + 1}}, ..., height_{rk_j}) LCP(i,j)=min(heightrki,heightrki+1,...,heightrkj)
ST表, O ( ∣ s ∣ l o g ∣ s ∣ ) − O ( 1 ) O(|s|log|s|)-O(1) O(slogs)O(1)

区间重复出现最长子串(可重叠)

首先介绍一个事实,
串的任意一个子串都可以唯一地表示为它的一个后缀的前缀。
很显然就不证了。
那么重复出现最长子串就是两个后缀的公共前缀
又因为 L C P ( s a i , s a j ) LCP(sa_i, sa_j) LCP(sai,saj)的长度随着 i − j i - j ij的上升而单调不降
所以如果取了一个后缀 第二个后缀取离它较远的后缀显然不优
所以答案就是 h e i g h t height height数组的最大值

区间重复出现最长子串(不可重叠)

显然答案具有可二分性
二分长度 设当前二分到的答案为 m i d mid mid
对于一个位置 p o s pos pos, 如果 h e i g h t p o s &lt; m i d height_{pos} &lt; mid heightpos<mid, 则由于 L C P ( i , j ) = m i n ( h e i g h t r k i , h e i g h t r k i + 1 , . . . , h e i g h t r k j ) LCP(i, j) = min(height_{rk_i}, height_{rk_{i + 1}}, ..., height_{rk_j}) LCP(i,j)=min(heightrki,heightrki+1,...,heightrkj), 所以对于任意一对 r k i &lt; = p o s &lt; = r k j rk_i &lt;= pos &lt;= rk_j rki<=pos<=rkj L C P ( i , j ) &lt; m i d LCP(i, j) &lt; mid LCP(i,j)<mid
所以我们对于每个 h e i g h t p o s &lt; m i d height_{pos} &lt; mid heightpos<mid,将 p o s pos pos作为断点将 h e i g h t height height数组分段
那么段内每一对 i , j i,j i,j L C P LCP LCP长度都合法,跨越断点的每一对 i , j i, j i,j L C P LCP LCP长度都不合法
接下来解决不可重叠问题
后缀 i , j i, j i,j长度为 m i d mid mid的前缀不重叠,当且仅当 ∣ j − i ∣ &gt; = m i d |j - i| &gt;= mid ji>=mid
所以我们检查每一个长度不为 1 1 1的段中 h e i g h t height height数组的最大值与最小值的差是否大于等于 m i d mid mid即可。

区间重复出现k次最长子串(可重叠)

显然答案子串必须是至少 k k k个后缀的 L C P LCP LCP
则选字典序连续的 k k k个显然不会更劣
单调队列,看做滑动窗口问题,求每连续 k − 1 k - 1 k1 h e i g h t height height的最小值的最大值
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std; 

const int N = 1e6 + 5; 
struct SA
{
	int n, m, k, cnt[N], rk[N * 2], sa[N], tmp[N], trk[N], h[N], q[N]; 
	int s[N]; 
	
	void read()
	{
		scanf("%d%d", &n, &k); 
		for(int i = 1; i <= n; i++)
			scanf("%d", &s[i]); 
	}
	
	void build()
	{
		for(int i = 1; i <= n; i++)
		{
			cnt[s[i]]++; 
			m = max(m, int(s[i])); 
		}
		
		for(int i = 1; i <= m; i++)
			cnt[i] += cnt[i - 1]; 
		
		for(int i = n; i >= 1; i--)
			sa[cnt[s[i]]--] = i; 
		
		int p = 0; 
		for(int i = 1; i <= n; i++)
			rk[sa[i]] = ((s[sa[i]] == s[sa[i - 1]]) ? p : ++p); 
		
		for(int k = 1; k <= n; k <<= 1)
		{
			for(int i = 0; i <= m; i++)
				cnt[i] = 0; 
			m = 0; 
			
			for(int i = 1; i <= n; i++)
			{
				cnt[rk[i + k]]++; 
				m = max(m, rk[i + k]); 
			}
			
			for(int i = 1; i <= m; i++)
				cnt[i] += cnt[i - 1]; 
			
			for(int i = n; i >= 1; i--)
				tmp[cnt[rk[i + k]]--] = i; 
			
			for(int i = 0; i <= m; i++)
				cnt[i] = 0; 
			m = 0; 
			
			for(int i = 1; i <= n; i++)
			{
				cnt[rk[tmp[i]]]++; 
				m = max(m, rk[tmp[i]]); 
			}
			
			for(int i = 1; i <= m; i++)
				cnt[i] += cnt[i - 1]; 
			
			for(int i = n; i >= 1; i--)
				sa[cnt[rk[tmp[i]]]--] = tmp[i]; 	
					 
			int p = 0; 
			for(int i = 1; i <= n; i++)
				trk[sa[i]] = ((rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + k] == rk[sa[i - 1] + k]) ? p : ++p); 
			
			for(int i = 1; i <= n; i++)
				rk[i] = trk[i]; 
		}
		
		int j = 0; 
		for(int i = 1; i <= n; i++)
		{
			if(rk[i] == 1)continue;  
			while(s[i + j] == s[sa[rk[i] - 1] + j])j++; 
			h[rk[i]] = j; 
			if(j)j--; 
		}
	}
	int solve()
	{
		int ret = 0, l = 1, r = 0; 
		for(int i = 1; i <= k - 1; i++)
		{
			while(r && h[q[r]] >= h[i])r--; 
			q[++r] = i; 
		}
		ret = h[q[l]]; 
		
		for(int i = k; i <= n; i++)
		{
			while(l <= r && q[l] <= i - k + 1)l++; 
			while(l <= r && h[q[r]] >= h[i])r--; 
			q[++r] = i; 
			ret = max(ret, h[q[l]]); 
		}
		return ret; 
	}
}S; 

int main()
{
	S.read(); 
	S.build(); 
	printf("%d", S.solve()); 
	return 0; 
}
本质不同子串个数

等价于求 ∣ s ∣ − s a i + 1 − h i |s| - sa_i + 1 - h_i ssai+1hi的和
显然
每个后缀的贡献显然是长度减去重复长度
又因为
排好序
所以和它重复最多的就是排在它前面的那一个
减去后在它前面就没有别的前缀和它重复又没被减去了
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std; 

const int N = 1e5 + 5; 
struct SA
{
	int n, m, cnt[N], rk[N * 2], sa[N], tmp[N], trk[N], h[N]; 
	char s[N]; 
	
	void read()
	{
		scanf("%d", &n); 
		scanf("%s", s + 1); 
	}
	
	void build()
	{
		for(int i = 1; i <= n; i++)
		{
			cnt[s[i]]++; 
			m = max(m, int(s[i])); 
		}
		
		for(int i = 1; i <= m; i++)
			cnt[i] += cnt[i - 1]; 
		
		for(int i = n; i >= 1; i--)
			sa[cnt[s[i]]--] = i; 
		
		int p = 0; 
		for(int i = 1; i <= n; i++)
			rk[sa[i]] = ((s[sa[i]] == s[sa[i - 1]]) ? p : ++p); 
		
		for(int k = 1; k <= n; k <<= 1)
		{
			for(int i = 0; i <= m; i++)
				cnt[i] = 0; 
			m = 0; 
			
			for(int i = 1; i <= n; i++)
			{
				cnt[rk[i + k]]++; 
				m = max(m, rk[i + k]); 
			}
			
			for(int i = 1; i <= m; i++)
				cnt[i] += cnt[i - 1]; 
			
			for(int i = n; i >= 1; i--)
				tmp[cnt[rk[i + k]]--] = i; 
			
			for(int i = 0; i <= m; i++)
				cnt[i] = 0; 
			m = 0; 
			
			for(int i = 1; i <= n; i++)
			{
				cnt[rk[tmp[i]]]++; 
				m = max(m, rk[tmp[i]]); 
			}
			
			for(int i = 1; i <= m; i++)
				cnt[i] += cnt[i - 1]; 
			
			for(int i = n; i >= 1; i--)
				sa[cnt[rk[tmp[i]]]--] = tmp[i]; 	
					 
			int p = 0; 
			for(int i = 1; i <= n; i++)
				trk[sa[i]] = ((rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + k] == rk[sa[i - 1] + k]) ? p : ++p); 
			
			for(int i = 1; i <= n; i++)
				rk[i] = trk[i]; 
		}
		
		int j = 0; 
		for(int i = 1; i <= n; i++)
		{
			if(rk[i] == 1)continue;  
			while(s[i + j] == s[sa[rk[i] - 1] + j])j++; 
			h[rk[i]] = j; 
			if(j)j--; 
		}
	}
	long long solve()
	{
		long long ret = 0; 
		for(int i = 1; i <= n; i++)
			ret += n - i + 1 - h[rk[i]]; 
		return ret; 
	}
}S; 

int main()
{
	S.read(); 
	S.build(); 
	printf("%lld", S.solve()); 
	return 0; 
}
最长回文子串

把原串倒过来,塞在原串后面,中间加一个奇怪字符保证不会匹配时跨越边界
比如 a b a b c ababc ababc - > a b a b c ! c b a b a ababc!cbaba ababc!cbaba
那么这个新串的两个后缀的一个公共前缀必然对应着
原串的一个回文子串
对新串建立后缀数组 求 h e i g h t height height数组最大值 再奇偶分类讨论计算长度即可
回文串还有很多神仙做法比如回文自动机 m a n a c h e r manacher manacher算法
然而我并不会qwq

最长公共子串

把两个串接起来,中间加一个字符
求新串的 h e i g h t height height最大值即可
有字符保证,这里的 L C P LCP LCP不会跨过边界,但要注意只能统计不属于同一串的 h e i g h t height height数组

[SDOI2008]Sandy的卡片

把所有串接起来,二分答案+分段+统计段内元素是否出现在每个子串中
然而这里能匹配的不只是相等元素。怎么办呢?
我们注意题目中的描述等价于相邻元素间的差相等。所以只要对差建立后缀数组即可。
代码:

#include<iostream>
#include<cstdio>
#include<cstdio>
#include<cstring>
using namespace std; 

const int N = 500005; 
struct SA
{
	int n, m, k, tt, s[N], a[N], c[N], rk[2 * N], cnt[N], sa[N], tmp[N], trk[N], h[N], ck[N]; 
	void read()
	{
		int p; 
		scanf("%d", &k); 
		tt = 2000; 
		for(int i = 1; i <= k; i++)
		{
			scanf("%d", &p); 
			for(int j = 1; j <= p; j++)
			{
				scanf("%d", &a[j]); 
				if(j != 1)
				{
					s[++n] = a[j] - a[j - 1]; 
					c[n] = i; 
				}
			}
			s[++n] = ++tt; 
		}
	}
	
	void build()
	{
		for(int i = 1; i <= n; i++)
		{
			cnt[s[i]]++; 
			m = max(m, s[i]); 
		}
		
		for(int i = 1; i <= m; i++)
			cnt[i] += cnt[i - 1]; 
		
		for(int i = 1; i <= n; i++)
			sa[cnt[s[i]]--] = i; 
		
		int p = 0; 
		for(int i = 1; i <= n; i++)
			rk[sa[i]] = (s[sa[i]] == s[sa[i - 1]]) ? p : ++p; 
			
		for(int k = 1; k <= n; k <<= 1)
		{
			for(int i = 1; i <= m; i++)
				cnt[i] = 0; 
			m = 0; 
			
			for(int i = 1; i <= n; i++)
			{
				cnt[rk[i + k]]++; 
				m = max(m, rk[i + k]); 
			}
			
			for(int i = 1; i <= m; i++)
				cnt[i] += cnt[i - 1]; 
			
			for(int i = n; i >= 1; i--)
				tmp[cnt[rk[i + k]]--] = i; 

			for(int i = 1; i <= m; i++)
				cnt[i] = 0; 
			m = 0; 
			
			for(int i = 1; i <= n; i++)
			{
				cnt[rk[tmp[i]]]++; 
				m = max(m, rk[tmp[i]]); 
			}
			
			for(int i = 1; i <= m; i++)
				cnt[i] += cnt[i - 1]; 
			
			for(int i = n; i >= 1; i--)
				sa[cnt[rk[tmp[i]]]--] = tmp[i]; 
			
			int p = 0; 
			for(int i = 1; i <= n; i++)
				trk[sa[i]] = (rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + k] == rk[sa[i - 1] + k]) ? p : ++p; 		
			
			for(int i = 1; i <= n; i++)
				rk[i] = trk[i]; 
		}
		int j = 0; 
		for(int i = 1; i <= n; i++)
		{
			if(rk[i] == 1)continue; 
			while(s[i + j] == s[sa[rk[i] - 1] + j])j++; 
			h[rk[i]] = j; 
			if(j)j--; 
		}
	}
	bool check(int x)
	{
		memset(ck, 0, sizeof(ck));  
		ck[c[sa[1]]] = 1; 
		int tmp = 1, last = 1; 
		for(int i = 2; i <= n; i++)
		{
			if(h[i] < x - 1)
			{
				if(tmp == k)return 1; 
				tmp = 0; 
				for(int j = last; j < i; j++)
					ck[c[sa[j]]] = 0; 
				last = i; 
			}
			if(!ck[c[sa[i]]])
			{
				tmp++; 
				ck[c[sa[i]]] = 1; 
			}
		}
		return 0; 
	}
	void solve()
	{
		int l = 1, r = n, ans = 0; 
		while(l <= r)
		{
			int mid = (l + r) >> 1; 
			if(check(mid))
			{
				ans = mid; 
				l = mid + 1; 
			}
			else
				r = mid - 1; 
		}
		printf("%d\n", ans); 
	}
}S; 
int main()
{
	S.read(); 
	S.build(); 
	S.solve(); 
	return 0; 
} 
[AHOI2013]差异

因为 L C P ( i , j ) = m i n ( h e i g h t r k i , h e i g h t r k i + 1 , . . . , h e i g h t r k j ) LCP(i, j) = min(height_{rk_i}, height_{rk_{i + 1}}, ..., height_{rk_j}) LCP(i,j)=min(heightrki,heightrki+1,...,heightrkj)
所以要求的就是这样一个问题:
给一个子序列
求所有子区间的两端和减去它们的最小值的两倍
正好刚学分治,就每次st表找到当前区间的最小值,前缀和计算答案,再分治下去计算即可
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std; 

const int N = 500005; 
struct SA
{
	int n, cnt[N], rk[2 * N], sa[N], tmp[N], trk[N], h[N], st[N][21];
	long long sum[N]; 
	char s[2 * N]; 
	void read()
	{
		scanf("%s", s + 1); 
		n = strlen(s + 1); 
	}
	int mmin(int x, int y)
	{
		return (!x || !y) ? (x | y) : ((h[x] < h[y]) ? x : y); 
	}
	void build()
	{
		memset(cnt, 0, sizeof(cnt)); 
		memset(h, 0, sizeof(h)); 
		memset(rk, 0, sizeof(rk)); 
		memset(sum, 0, sizeof(sum)); 
		
		int m = 0; 
		for(int i = 1; i <= n; i++)
		{
			cnt[s[i]]++; 
			m = max(m, int(s[i])); 
		}
		for(int i = 1; i <= m; i++)
			cnt[i] += cnt[i - 1]; 
		
		for(int i = 1; i <= n; i++)
			sa[cnt[s[i]]--] = i; 
		
		int p = 1; rk[sa[1]] = 1; 
		for(int i = 2; i <= n; i++)
			rk[sa[i]] = ((s[sa[i]] == s[sa[i - 1]]) ? p : ++p); 
		
		for(int k = 1; k <= n; k <<= 1)
		{
			for(int i = 0; i <= m; i++)
				cnt[i] = 0; 
			m = 0; 
			
			for(int i = 1; i <= n; i++)
			{
				cnt[rk[i + k]]++; 
				m = max(m, rk[i + k]); 
			}
			
			for(int i = 1; i <= m; i++)
				cnt[i] += cnt[i - 1]; 
			
			for(int i = n; i >= 1; i--)
				tmp[cnt[rk[i + k]]--] = i; 
			
			for(int i = 0; i <= m; i++)
				cnt[i] = 0; 
			m = 0; 
			
			for(int i = 1; i <= n; i++)
			{
				cnt[rk[tmp[i]]]++; 
				m = max(m, rk[tmp[i]]); 
			}
			
			for(int i = 1; i <= m; i++)
				cnt[i] += cnt[i - 1]; 
			
			for(int i = n; i >= 1; i--)
				sa[cnt[rk[tmp[i]]]--] = tmp[i]; 
			
			int p = 1; trk[sa[1]] = 1; 
			for(int i = 2; i <= n; i++)
				trk[sa[i]] = ((rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + k] == rk[sa[i - 1] + k]) ? p : ++p); 
			
			for(int i = 1; i <= n; i++)
				rk[i] = trk[i]; 
		}
		
		int j = 0; 
		for(int i = 1; i <= n; i++)
		{
			if(rk[i] == 1)continue; 
			while(s[i + j] == s[sa[rk[i] - 1] + j])j++; 
			h[rk[i]] = j; 
			if(j)j--; 
		}

		h[1] = 1e9; 
		for(int i = 1; i <= n; i++)
			st[i][0] = i; 
		for(int j = 1; j <= 20; j++)
			for(int i = 1; i + (1 << j - 1) <= n; i++)
				st[i][j] = mmin(st[i][j - 1], st[i + (1 << j - 1)][j - 1]); 
				
		for(int i = 1; i <= n; i++)
			sum[i] = sum[i - 1] + sa[i]; 
	}
	int query(int l, int r)
	{
		int k = log2(r - l); 
		return mmin(st[l + 1][k], st[r - (1 << k) + 1][k]); 
	}
	long long solve(int l, int r)
	{
		if(l >= r)return 0; 
		long long ret = 0; 
		int p = query(l, r); 
		ret += 1ll * (sum[p - 1] - sum[l - 1]) * (r - p + 1) + 1ll * (sum[r] - sum[p - 1]) * (p - l); 
		ret -= 2ll * h[p] * (p - l) * (r - p + 1); 
		ret += solve(l, p - 1) + solve(p, r); 
		return ret; 
	}
}S; 
int main()
{
	S.read(); 
	S.build(); 
	printf("%lld", S.solve(1, S.n)); 
	return 0; 
} 

貌似应该有别的更直观一些的做法

[SCOI2012]喵星球上的点名

听人说就是个大暴力
把所有串接起来
搞一搞
这波口胡
什么时候我不咕了再看看是莫队还是什么别的东西好搞 再更吧

还有很多变化了的问题,然而都是换汤不换药,总体思想是拼成想要的串+二分+分段+乱搞求解
咕掉的代码慢慢补
待续

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值