SA后缀数组详解与运用

@

1、后缀数组作用

主要用于解决最长公共前缀(lcp)问题,大多数时候此类问题都可以用sam(后缀自动机)来解决。不过应为sa算法相对更加优秀的时空复杂度,在大数据集上可以防止TMLE。

2、后缀数组的构造

  1. 首先声明几个变量。
    1、Str :需要处理的字符串(长度为Len)
    2、Suffix[i] :Str下标为i ~ Len的连续子串(即后缀)
    3、Rank[i] : Suffix[i]在所有后缀中的排名
    4、SA[i] : Rank的逆运算,就是排名第i大的字符串是啥。

  2. 构造方式大概有两种:DC3和倍增
    DC3 时间O(n),空间O(3*n) 常数大 (看脸)
    倍增 时间O(nlogn),常数小

    这里介绍倍增 其实是我只会倍增

    考虑暴力
    对于一个后缀,想要直接构造出它的rank可以用快排(nlogn排序+每次O(n)比较)
    这种是n^2logn的时间复杂度,显然不能接受。

    使用倍增的思想 设SubStr(i, len)为从第i个字符开始,长度为len的字符串我们可以把第k轮SubStr(i, 2^k)看成是一个由SubStr(i, 2k−1)和SubStr(i + 2k−1, 2k−1)拼起来的东西。
    这两个长度而2^k−1的字符串是上一轮计算过的,当然上一轮的rank也知道。
    那么把每个这一轮的字符串都转化为这种形式,并且大家都知道字符串的比较是从左往右,左边和右边的大小我们可以用上一轮的rank表示。
    这就可以视为一些第一关键字和第二关键字比较大小再把这些两位数重新排名就是这一轮的rank。

    tips:如果使用传统排序方法(快排)那么时间复杂度就是nlog^2n,在这里我们可以使用基数排序,将时间复杂度降为nlogn。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
char s[1000011];
int len,n;
int SA[1000011];
int x[1000011],y[1000011],t[1000011];
int p[1000011];
inline void GET_SA(){
	//y是第二关键字,x是第一关键字
	//SA是排名为下标的子串的字符串中的位置
	//y和SA:排名为下标,映射位置
	//x:位置为下标,映射排名
	int m=122;n=len;//m->字符集
	for(int i=1;i<=n;++i) t[x[i]=s[i]]++;  
	for(int i=1;i<=m;++i) t[i]+=t[i-1];
	for(int i=n;i;--i) SA[t[x[i]]--]=i;		//先求出k=0时候的SA		
	/*for(int i=1;i<=n;i++){
			printf("%d ",SA[i]);
		}
	putchar('\n');*/
	int num=0;
	for(int k=1;k<=n&&num<=n;k<<=1){
		num=0;
		for(int i=1;i<=m;++i) t[i]=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;	//其余:如果有第二关键字,按第二关键字的顺序(sa)存入y
		for(int i=1;i<=n;++i){
			t[x[y[i]]]++;
		}	//可以用p[i]=x[y[i]]来卡常
		for(int i=1;i<=m;++i) t[i]+=t[i-1];	
		for(int i=n;i;--i) SA[t[x[y[i]]]--]=y[i];	
			//桶排计算SA
			//按第二关键字的顺序访问第一关键字,要倒序(越大的桶排顺序越后)
			//x已经计算好了
		/*for(int i=1;i<=n;i++){
			printf("%d ",SA[i]);
		}
		putchar('\n');*/
		swap(x,y);
		x[SA[1]]=1;num=1;
		for(int i=2;i<=n;++i){
			x[SA[i]]=(y[SA[i]]==y[SA[i-1]]&&y[SA[i-1]+k]==y[SA[i]+k])?num:++num;
		}	//按照SA的顺序计算下一轮的x,模拟即可,注意边界
		m=num;
	}
}
void prt(int x){
    int tmp[20],*t =tmp;
    for(;x;x/=10)*t++=x%10+'0';
    if(t==tmp)putchar('0');
    else for(--t;t>=tmp;--t)putchar(*t);
    putchar(' ');
}
int main(){
	scanf("%s",s+1);
	len=strlen(s+1);
	GET_SA();
	for(int i=1;i<=n;++i){
		prt(SA[i]);
	}
	return 0;
}

3、 SA算法的用途

  1. 同样先是定义一些变量
    Heigth[i] : 表示Suffix[SA[i]]和Suffix[SA[i - 1]]的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀 。

    H[i] : 等于Height[Rank[i]],也就是后缀Suffix[i]和它前一名的后缀的最长公共前缀 。
    而两个排名不相邻的最长公共前缀定义为排名在它们之间的Height的最小值。

  2. 如何高效的计算height数组
    可以知道h[i]>=h[i-1]-1;
    可以思考一下。
    然后按照hi的顺序来算heighti,暴力算

  3. 简单应用
    (1. 一个串中两个串的最大公共前缀是多少?
    (2. 一个串中可重叠的重复最长子串是多长?
    (3. 一个串中不可重叠的重复最长子串是多长? (poj1743)

     1,这就是height啊,用rmq处理即可。
     2,ON扫过去,看height的最大值即可。
     3,二分答案,把所有的height按照k分组,保证每组间的height大于k,然后查看每组最大的sa值和最小的sa值相差是不是超过了k,有一组超过答案就合法
    
  4. 进阶应用

    一个字符串不相等的子串的个数是多少?

     每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。
     可以发现每一个后缀Suffix[SA[i]]的贡献是Len - SA[i] + 1,但是有子串算重复。
     重复的就是Heigh[i]个与前面相同的前缀,那么减去就可以了。最后,一个后缀Suffix[SA[i]]的贡献就是Len - SA[i] + 1 - Height[i]。
    

4、例题:poj 3261 : Milk Patterns

命运石之传送

题意:可重叠的k次最长重复子串

容易想到二分长度(重叠>k次的也一定重叠了k次,满足单调性),然后分组查看每组中有没有足够多数量的字符串

题解就先咕了,下周可能会在机房打

5、后记

后缀数组感觉。。其实理解了也没那么难
好像下节课讲SAM和回文数组。。祝我好运

posted @ 2019-07-27 22:32  lcyfrog  阅读(471)  评论(0编辑  收藏  举报