字符串处理-SA数组

后缀数组 S A \tt SA SA 可以用于各方面字符串问题,其目的是求出这个字符串的所有后缀的按照字典序的排名。

前置知识

倍增 / DC3

算法用途

可以用来求最长公共子串,最长回文串等

算法复杂度

字符串长度为 n \tt n n

时间

O ( n log ⁡ n ) \tt O(n\log n) O(nlogn)

空间

O ( n ) \tt O(n) O(n)

算法实现

要知道,最暴力算法就是把所有后缀找出来,然后用 s o r t \tt sort sort 排序

虽然时间复杂度和这个算法的优化版本一样 O ( n log ⁡ n ) \tt O(n\log n) O(nlogn),但是这个算法的空间却是 O ( n 2 ) \tt O(n^2) O(n2) 的。很容易炸。

后缀数组需要求两个数组:后缀数组 S A \tt SA SA 和排名数组 r a n k \tt rank rank

S A i \tt SA_i SAi 代表从第 i \tt i i 个字符开始的后缀的排名。

r a n k i \tt rank_i ranki 代表排名为 i \tt i i 的后缀的开始字符的下标。

所以,其实 S A i = j \tt SA_i = j SAi=j 时, r a n k j = i \tt rank_j = i rankj=i

对于后缀数组的实现,主要有两种算法:

  • 倍增算法

  • DC3算法

我在这只讲倍增(主要是DC3我不会)。

倍增算法的思路主要是递推,

我们先求出每个后缀按照第一个字符的排序的 r a n k \tt rank rank(若第一个字符相同,则 r a n k \tt rank rank 相等),

即每个 i 对应的 S [ i , i ] S_{[i, i]} S[i,i] 在所有 S [ i , i ] S_{[i, i]} S[i,i] 中的排名。

然后用所得出的 r a n k rank rank 来计算出所有后缀按照前两个字符排序的 r a n k \tt rank rank(每个 i \tt i i 对应的 S [ i , i + 2 0 ] \tt S_{[i, i+2^0]} S[i,i+20] 在所有 S [ i , i + 2 0 ] \tt S_{[i, i +2^0]} S[i,i+20] 中的排名。)

接着是四个,八个…,直到全部字符。(每个 i \tt i i 对应的 S [ i , i + 2 k ] \tt S_{[i, i+2^k]} S[i,i+2k] 在所有 S [ i , i + 2 k ] \tt S_{[i, i +2^k]} S[i,i+2k] 中的排名。)

过程如下:

假设我们要计算后缀按照前 2 k \tt 2^k 2k 个字符排序的 r a n k \tt rank rank。而我们已经求出了按前 2 k − 1 \tt 2^{k-1} 2k1 个字符排序的 r a n k \tt rank rank

那么对于每个 p \tt p p,我们要求的就是 S [ p , p + 2 k ] \tt S_{[p, p+2^{k}]} S[p,p+2k] 在所有 S [ i , i + 2 k ] \tt S_{[i, i+2^{k}]} S[i,i+2k] 的排名。

于是我们要考虑如何用 S [ i , i + 2 k − 1 ] \tt S_{[i, i + 2^{k-1}]} S[i,i+2k1] 来表示 S [ p , p + 2 k ] \tt S_{[p, p+2^{k}]} S[p,p+2k]

发现 S [ p , p + 2 k ] = S [ p , p + 2 k − 1 ] + S [ p + 2 k − 1 + 1 , p + 2 k ] \tt S_{[p, p+2^{k}]} = S_{[p, p+2^{k-1}]} + S_{[p+2^{k-1}+1, p+2^{k}]} S[p,p+2k]=S[p,p+2k1]+S[p+2k1+1,p+2k]

S [ p , p + 2 k − 1 ] \tt S_{[p, p+2^{k-1}]} S[p,p+2k1] S [ p + 2 k − 1 + 1 , p + 2 k ] \tt S_{[p+2^{k-1}+1, p+2^{k}]} S[p+2k1+1,p+2k] 的排名我们都已经求过了。

r a n k p \tt rank_p rankp r a n k p + 2 k − 1 + 1 \tt rank_{p + 2^{k-1} +1} rankp+2k1+1

我们把所有 r a n k i \tt rank_{i} ranki r a n k i + 2 k − 1 + 1 \tt rank_{i + 2^{k-1} +1} ranki+2k1+1 合并形成 n \tt n n 个二元组。

然后对于这个二元组排序,于是就可以得出新的 r a n k i \tt rank_i ranki

重复这个步骤,直到所有 r a n k i \tt rank_i ranki 都不相同为止。然后求出来的 r a n k \tt rank rank 就是我们要求的排名数组的。

这时候,我们最多需要 log ⁡ n \tt\log n logn 次求 r a n k \tt rank rank,每次求需要 O ( n log ⁡ n ) \tt O(n\log n) O(nlogn) 的快速排序时间,所以总时间复杂度是 O ( n log ⁡ 2 n ) \tt O(n\log^2n) O(nlog2n)

算法应用

最长公共前缀(LCP)

S A \tt SA SA 数组还有一种基本操作,给两个后缀的开始字符串,求他们最长公共前缀长度。

我们可以快速求出排序后相邻两个字符串的最长公共前缀的长度。

我们让 h e i g h t i \tt height_i heighti 代表 S A i − 1 \tt SA_{i - 1} SAi1 S A i \tt SA_i SAi 代表的两个后缀字符串的最长前缀的长度。

然后,我们可以得到两个后缀的排名 r a n k i , r a n k j \tt rank_i, rank_j ranki,rankj,然后我们只需要求出 min ⁡ i + 1 ≤ x ≤ j ( h e i g h t i ) \tt \min_{i+1\leq x\leq j}(height_i) mini+1xj(heighti)

怎么求?只需要预处理然后使用 R M Q RMQ RMQ 就行了。

最长公共子串(LCS)

N \tt N N 个串的最长公共子串

我们发现这个问题可以转化为求一些后缀的最长公共前缀的最大值,这些后缀应分属于 N \tt N N 个串。

具体方法如下:

N \tt N N 个串分别为 S 1 , … , S N \tt S_1,…, S_N S1,,SN,首先建立一个串 S \tt S S,把这 N \tt N N个串用不同的分隔符连接起来。 S = S 1 [ P 1 ] S 2 [ P 2 ] S 3 … S N − 1 [ P N − 1 ] S N \tt S = S_1[P_1]S_2[P_2]S_3…S_{N-1}[P_{N-1}]S_N S=S1[P1]S2[P2]S3SN1[PN1]SN

这些分隔符应为 N − 1 \tt N-1 N1 个不在字符集中的字符。

接下来,求出字符串 S \tt S S 的后缀数组和 h e i g h t \tt height height 数组。

二分枚举答案 A \tt A A,于是问题就转化成 N \tt N N 个串是否可以有度为 A \tt A A 的公共字串,而代价是,我们多出来了一个 O ( log ⁡ L ) \tt O(\log L) O(logL) 的时间复杂度。

如果能找出 h e i g h t height height 数组中连续的一段 [ i , j ] \tt [i,j] [i,j],满足排完序的后缀中第 [ i , j ] \tt [i, j] [i,j] 个后缀的起始字符分别属于 { S 1 , … , S n } \tt \{S_1,…,S_n\} {S1,,Sn} 中的每个字符串,使得

min ⁡ k ∈ [ i + 1 , j ] ( h e i g h t [ k ] ) > = A \tt \min_{k \in [i +1, j]}(height[k])>=A k[i+1,j]min(height[k])>=A

那么 A \tt A A 就是可行解,否则不是。

具体查找 [ i , j ] \tt [i, j] [i,j] 时,可以先从前到后枚举 i \tt i i 的位置,如果发现 h e i g h t i > = A \tt height_i>=A heighti>=A,则开始从 i \tt i i 向后枚举 j \tt j j 的位置,直到找到了 h e i g h t j + 1 < A \tt height_{j+1}<A heightj+1<A,判断是否满足上述情况。

如果满足,则 A \tt A A 为可行解,然后直接返回,否则令 i = j + 1 \tt i=j+1 i=j+1 继续向后枚举。

S \tt S S 中每个字符被访问了 O ( 1 ) \tt O(1) O(1) 次, S \tt S S 的长度为 N L + N − 1 \tt NL+N-1 NL+N1,所以时间复杂度为 O ( N L ) \tt O(NL) O(NL)

到这里,我们就可以理解为什么分隔符 P 1 . . P N − 1 \tt P_1..P_{N-1} P1..PN1 必须是不同的 N − 1 \tt N-1 N1 个不在字符集中的字符了,这样才能保证 S \tt S S 的后缀的公共前缀不会跨出一个原有串的范围。(当然你也可以用其他方法来避免)

算法优化

  1. 基数排序优化

我们知道,因为 r a n k \tt rank rank 数组里的数一定在 [ 1 , n ] \tt [1, n] [1,n] 的范围内,所以我们可以使用基数排序来代替快速排序。

二元组基数排序,我们需要先把第二关键字放进桶中,然后从小到大枚举每个桶,把桶里的数按照放进去的顺序挨个取出来,

再把第一关键字放进去,同样取出来,然后得到的数组就是排好序的了。

所以这个排序算法复杂度是 O ( n ) \tt O(n) O(n) 的,这样优化后的时间复杂度就是 O ( n log ⁡ n ) \tt O(n\log n) O(nlogn)

代码

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
char str[100010];
int cnt[100010];
int rk[100010];
int y[100010];
int SA[100010];
int height[100010];
int n, m;
int get_SA()
{
	for(int i = 1; i <= n; i++)
	{
		rk[i] = str[i];
		cnt[rk[i]]++;
	}
	for(int i = 2; i <= m; i++)
	{
		cnt[i] += cnt[i - 1]; 
	}
	for(int i = n; i >= 1; i--)
	{
		SA[cnt[rk[i]]--] = i; 
	}
	for(int k = 1; k <= n; k <<= 1)
	{
		int num = 0;
		for(int i = n - k + 1; i <= n; i++)
		{
			y[++num] = i;
		}
		for(int i = 1; i <= n; i++)
		{
			if(SA[i] > k)
			{
				y[++num] = SA[i] - k;
			}
		}
		for(int i = 1; i <= m; i++)
		{
			cnt[i] = 0;
		}
		for(int i = 1; i <= n; i++)
		{
			cnt[rk[i]]++;
		}
		for(int i = 2; i <= m; i++)
		{
			cnt[i] += cnt[i - 1];
		}
		for(int i = n; i >= 1; i--)
		{
			SA[cnt[rk[y[i]]]--] = y[i];
			y[i] = 0;
		}
		swap(rk, y);
		rk[SA[1]] = 1;
		num = 1;
		for(int i = 2; i <= n; i++)
		{
			if(y[SA[i]] == y[SA[i - 1]] && y[SA[i] + k] == y[SA[i - 1] + k])
			{
				rk[SA[i]] = num;
			}
			else
			{
				rk[SA[i]] = ++num;
			}
		}
		if (num == n)
		{
			break;
		}
		m = num;
	}
	for(int i = 1; i <= n; i++)
	{
		printf("%d ", rk[i]);
	}
	printf("\n");
}

void get_height()
{
	for(int i = 1; i <= n; i++)
	{
		rk[SA[i]] = i;
	}
	int k = 0;
	for(int i = 1; i <= n; i++)
	{
		if(k)
		{
			k--;
		}
		int j = SA[rk[i] - 1];
		while(str[i + k] == str[j + k])
		{
			k++;
		}
		height[rk[i]] = k;
	}
	for(int i = 1; i <= n; i++)
	{
		printf("%d ", height[i]);
	}
	printf("\n");
}

int main()
{
	scanf("%s", str + 1);
	n = strlen(str + 1);
	m = 256;
	get_SA();
	get_height();
	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值