【知识总结】后缀数组(Suffix_Array)

又是一个学了n遍还没学会的算法……

后缀数组是一种常用的处理字符串问题的数据结构,主要由 s a sa sa r a n k rank rank两个数组组成。以下给出一些定义:

s t r str str表示处理的字符串,长度为 l e n len len。(下标从 0 0 0开始)

[ i , j ) [i,j) [i,j)表示 s t r str str i i i j − 1 j - 1 j1的字串。

后缀 i i i表示子串 [ i , l e n ) [i,len) [i,len),以字典序排序。

s a [ i ] sa[i] sa[i]表示排名为 i i i的后缀的起始位置(即后缀 s a [ i ] sa[i] sa[i]是第 i i i名)

r a n k [ i ] rank[i] rank[i]表示后缀 i i i的排名(从 0 0 0开始)。显然 r a n k [ s a [ i ] ] = i rank[sa[i]]=i rank[sa[i]]=i

一、基数排序

先简单介绍一下后缀数组的前置技能:基数排序。

以对整数数组 a r r arr arr排序为例。从低到高遍历每一个十进制位,对于每个位:

1. 1. 1. a r r arr arr数组已经按照前 i − 1 i-1 i1位排好序,( i = 0 i=0 i=0时忽略这句),现在我们将把它变为按前 i i i位排好序。脑补以下整数的比较方式,现在应该把第 i i i位作为第一关键字,前 i − 1 i-1 i1位作为第二关键字。

2. 2. 2.统计第 i i i位为数字 a a a的数的数量,存入 c o u n t [ a ] count[a] count[a]

3. 3. 3. c o u n t count count数组求前缀和,算出最后一个第 i i i位为 a a a的数在按照前 i i i位排序后数组中的位置的下一个。这句表达比较鬼畜,看下面的例子。

比如, i i i位为 0 0 0的有 2 2 2个,为 1 1 1的有 1 1 1个,为 2 2 2的有 3 3 3个,第 3 3 3步以后 c o u n t count count { 2 , 3 , 6 } \{2,3,6\} {2,3,6},那么排序后 a r r [ 0 ] arr[0] arr[0] a r r [ 1 ] arr[1] arr[1]的第 i i i位为 0 0 0 a r r [ 2 ] arr[2] arr[2]的第 i i i位为 1 1 1 a r r [ 3 ] arr[3] arr[3] a r r [ 5 ] arr[5] arr[5]的第 i i i位为 2 2 2

4. 4. 4.逆序遍历 a r r arr arr,按照上一步中算出的第 i i i位为 a a a的数排序后的位置逆序填充临时数组。两个均逆序保证了对于第 i i i位相同的数按照最初在 a r r arr arr中的位置排序。

5. 5. 5.最后,把临时数组复制给 a r r arr arr,此时 a r r arr arr按照前 i i i位有序。

int count[10];
for(int i = 1; i <= 10; i++, ra *= 10)
{
	memset(count, 0, sizeof(count));
	for (int j = 1; j <= n; j++)
		++count[arr[j] / ra % 10];//step 2
	for (int j = 1; j < 10; j++)
		count[j] += count[j - 1];//step 3
	for(int j = n - 1; j >= 0; j--)
		buc[--count[arr[j] / ra % 10]] = arr[j];
	memcpy(arr, buc, sizeof(int[n]));
}

二、倍增构造后缀数组

考虑我们现在有了对所有形如 [ i , m i n ( i + t m p , l e n ) ) [i,min(i+tmp,len)) [i,min(i+tmp,len))的子串排序的数组 s a sa sa r a n k rank rank(对于相同的子串,它们的 r a n k rank rank值相同,在 s a sa sa中顺序任意),我们现在要构造对所有形如 [ i , m i n ( i + 2 t m p , l e n ) ) [i,min(i+2tmp,len)) [i,min(i+2tmp,len))的子串排序。最坏情况下,当 2 t m p ≥ l e n 2tmp\geq len 2tmplen时就得到了答案。

可以发现此时很类似于基数排序时排到某一位时的情况。此时,第一关键字是 [ i , i + t m p ) [i,i+tmp) [i,i+tmp),第二关键字是 [ i + t m p , i + 2 t m p ) [i+tmp, i+2tmp) [i+tmp,i+2tmp)。并且,现在已经按照第二关键字排好序了。

于是我们先看看此处的基数排序。其中 k i n d kind kind r a n k rank rank中不同值的种数(由于 r a n k rank rank 0 0 0开始,也可以看成 r a n k rank rank中最大值加 1 1 1), t p [ i ] tp[i] tp[i]表示哪个串的第二关键字在所有第二关键字中的排名是 i i i

void radix_sort()
{
	static int count[N];
	memset(count, 0, sizeof(int[kind]);
	for (int i = 0; i < len; i++)
		count[rank[tp[i]]]++;
	for (int i = 1; i < kind; i++)
		count[i] += count[i - 1];
	for (int i = len - 1; i >= 0; i--)
		sa[--count[rank[tp[i]]]] = tp[i];
}

然后我们来构造 t p tp tp数组。首先,对于起点在 [ l e n − t m p , l e n ) [len-tmp,len) [lentmp,len)中的串,它们的第二关键字都是空串,排名是最低的。所以它们应当在 t p tp tp的开头:

for (int i = len - tmp; i < len; i++)
	tp[cnt++] = i;

然后,按照 s a sa sa加入剩下的串。注意只有起点在 t m p tmp tmp及以后的串才能作为第二关键字。

for(int i=0;i<len;i++)
	if(sa[i]>=tmp)
		tp[cnt++]=sa[i]-tmp;

至此, t p tp tp数组构造完毕,可以进行基数排序。排序后,我们要按照新的 s a sa sa和旧的 r a n k rank rank构造新的 r a n k rank rank。首先,把旧的 r a n k rank rank进行拷贝。为了优化常数可以这样写:

swap(rank,tp)

记住,此后 t p tp tp就只是旧的 r a n k rank rank的一份拷贝了,没有更多实际意义。更新 r a n k rank rank的过程比较显然。

rank[sa[0]] = 0;
kind = 1;
for (int i = 1; i < len; i++)
{
	if (tp[sa[i]] == tp[sa[i - 1]] && 
		(sa[i] + tmp < len && sa[i - 1] + tmp < len) && 
		(tp[sa[i] + tmp] == tp[sa[i - 1] + tmp]))
		rank[sa[i]] = rank[sa[i - 1]];
	else
		rank[sa[i]] = kind++;
}

最后,如果 k i n d = l e n kind=len kind=len,即 r a n k rank rank已经两两不同,则说明已经得出了答案。

三、应用:构造 h e i g h t height height数组

我不会,你开心不qwq

四、完整代码:

int sa[N], rank[N], tp[N], kind, len;
void radix_sort()
{
	static int count[N];
	memset(count, 0, sizeof(int) * kind);
	for (int i = 0; i < len; i++)
		count[rank[tp[i]]]++;
	for (int i = 1; i < kind; i++)
		count[i] += count[i - 1];
	for (int i = len - 1; i >= 0; i--)
		sa[--count[rank[tp[i]]]] = tp[i];
}
void build(const string &s)
{
	len = s.size();
	for (int i = 0; i < len; i++)
		rank[i] = s[i], tp[i] = i;
	kind = CH;
	radix_sort();
	for (int tmp = 1; tmp < len; tmp *= 2)
	{
		int cnt = 0;
		for (int i = len - tmp; i < len; i++)
			tp[cnt++] = i;
		for (int i = 0; i < len; i++)
			if (sa[i] >= tmp)
				tp[cnt++] = sa[i] - tmp;
		radix_sort();
		swap(rank, tp);
		rank[sa[0]] = 0;
		kind = 1;
		for (int i = 1; i < len; i++)
		{
			if (tp[sa[i]] == tp[sa[i - 1]] && 
				(sa[i] + tmp < len && sa[i - 1] + tmp < len) && 
				(tp[sa[i] + tmp] == tp[sa[i - 1] + tmp]))
				rank[sa[i]] = rank[sa[i - 1]];
			else
				rank[sa[i]] = kind++;
		}
		if (kind == len)
			break;
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值