后缀数组

    后缀数组是处理字符串的一种常用算法,是后缀树的一种精巧的替代品,它比后缀树更容易编程实现,且效率和后缀树相当。

后缀数组定义

 
    如上图所示,为一个后缀数组和名次数组示例。其中下标从1开始。

            设字符串的长度为n。为了方便比较大小,可以在字符串的后面添加一个字符,这个字符在前面的字符中没有出现过,且比前面的字符都要小。在求出Rank数组之后,可以用O(1)的时间比较任意两个后缀的大小,在求出后缀数组或名次数组中的人任意一个之后,都可以用O(n)的时间求出另一个。

后缀数组的求法——倍增算法

n次排序,每次排序使用基数排序,复杂度为O(n),总的时间复杂度为O(nlogn).

后缀数组的求法——DC3算法

 
    求出起始位置不是3的倍数的后缀的rank之后,可以很容易的求出起始位置为3的倍数的后缀的rank,类似倍增算法进行两个关键字基数排序即可。

 
    第二部分的后缀为起始位置为3的倍数的后缀,可以看成是一个字符加上一个起始位置为3k+1的后缀,起始位置为3k+1的后缀的rank已经求出,因此只需要一次基数排序即可求出剩下的后缀的SA。

 
情形一是Suffix(3*i)和Suffix(3*j+1)的比较

Suffix(3*i) = r[3*i] + Suffix(3*i+1) 
Suffix(3*j+1) = r[3*j+1] + Suffix(3*j+2)

 
情形二是Suffix(3*i)和Suffix(3*j+2)的比较

Suffix(3*i) = r[3*i] + r[3*i+1] + Suffix(3*i+2) 
Suffix(3*j+2) = r[3*j+2] + r[3*j+3] + Suffix(3*(j+1)+1)

其中,Suffix(3*i+2)和Suffix(3*(j+1)+1)的比较结果可以通过步骤(2)的结果快速得到。

 
    步骤(1)的排序时间为O(n),新的字符串的长度不超过2n/3,求新串的SA的时间为f(2n/3), 步骤(2)和步骤(3)的时间为O(n).因此 f(n) = f(2n/3) + O(n).可以得到 f(n) = O(n).

后缀树组的height数组

    后缀数组解决字符串问题的时候,经常用到后缀数组的一个附属——height数组。height数组定义为:

height[i]表示后缀 Suffix(SA(i))和Suffix(SA(i-1))的最长公共前缀长度。即第i大的后缀和第i-1大的后缀的最长公共前缀长度。

 
    设Suffix(k)是排在Suffix(i-1)前一名的后缀,他们的最长公共前缀是h[i-1]。那么Suffix(k+1)将排在Suffix(i)的前面(这里要求h[i-1] > 1,若h[i-1] <=1, 原式显然成立),并且Suffix(k+1)和Suffix(i)的最长公共前缀是h[i-1]-1(相比Suffix(k)和Suffix(i-1)分别去掉首字符)。所以Suffix(i)和它的前一名后缀的最长公共前缀至少是h[i-1]-1.

实现(c++)

 
    需要注意,基数排序时候,按照先排低位后排高位的顺序,多次使用计数排序

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#define LETTERS 30
#define MAX_ARRAY_SIZE 200005 
int gStrLen;
int gStr[MAX_ARRAY_SIZE];			//将字符串转换为整数数组,且在末尾加一个最小的之前未出现的数字


int gCount[MAX_ARRAY_SIZE];			//用于计数排序和基数排序
int gRank[MAX_ARRAY_SIZE];			//将序列中每个元素都给一个rank,用于排序
int gSuffixArray[MAX_ARRAY_SIZE];	//后缀数组
int gOrderBySecondKey[MAX_ARRAY_SIZE];//倍增算法基数排序时,将gRank按照第二关键字排序后的顺序
int gFirstKeyArray[MAX_ARRAY_SIZE]; //将gRank按照gOrderBySecondKey排列的结果,用于对第一关键字进行排序 
//gFirstKeyArray[i] = gRank[gOrderBySecondKey[i]]
int gHeight[MAX_ARRAY_SIZE];


//比较两个元素是否相等,分别比较第一关键字和第二关键字
bool Compare(int* arr, int a, int b, int step){
	return arr[a] == arr[b] && arr[a + step] == arr[b + step];
}

//将字符串转换为初始整数数组
void GetStr(char* str){
	memset(gStr, 0, sizeof(gStr));
	gStrLen = strlen(str);
	for (int i = 0; i < gStrLen; i++){
		gStr[i] = str[i] - 'a' + 1;
	}
	gStr[gStrLen] = 0;
	gStrLen++;
}

//求后缀数组
void GetSuffixArray(){
	int n = gStrLen;

	//求step = 0的后缀数组
	memset(gCount, 0, sizeof(gCount));
	for (int i = 0; i < n; i++){
		gRank[i] = gStr[i];//根据字符串的内容,将gRank初始化,gRank[i]相当于位置i处的元素值
		gCount[gRank[i]] ++; 
	}
	int m = LETTERS;
	for (int i = 1; i < m; i++){
		gCount[i] += gCount[i - 1];
	}
	for (int i = n - 1; i >= 0; i--){
		gSuffixArray[--gCount[gRank[i]]] = i;  //step = 0的后缀数组
	}

	int step = 1;
	int *rank = gRank, *order_by_second_key = gOrderBySecondKey;
	while (step < n){  //倍增算法
		int p = 0;
		//根据上次求得的 gSuffixArray 求gRank按照第二关键字排序后的位置,
		//gOrderBySecondKey[i] 存放的是gRank按照第二关键字排序,第i大的位于 gRank数组的位置(用作后续的第一关键字的位置)
		for (int i = n - step; i < n; i++){ 
			order_by_second_key[p++] = i;   //i >n-step时,第二关键字的位置 > n-1,因此直接视为0,放到gOrderBySecondKey最开始的位置
		}
		for (int i = 0; i < n; i++){//上次的gSuffixArray[i],如果位于 [step, n-1]区间内,则可以直接得到其 第一关键字的位置
									//且按照 i从小到大的顺序,可以保证基数排序的正确性
			if (gSuffixArray[i] >= step){
				order_by_second_key[p++] = gSuffixArray[i] - step;
			}
		}
		for (int i = 0; i < n; i++){ //按照第二关键字大小得到的第一关键字的序列
			gFirstKeyArray[i] = rank[order_by_second_key[i]];
		}

		//基数排序
		for (int i = 0; i < m; i++){
			gCount[i] = 0;
		}
		for (int i = 0; i < n; i++){
			gCount[gFirstKeyArray[i]] ++;
		}
		for (int i = 1; i < m; i++){
			gCount[i] += gCount[i - 1];
		}
		for (int i = n - 1; i >= 0; i--){
			gSuffixArray[--gCount[gFirstKeyArray[i]]] = order_by_second_key[i];
		}

		//此时,更新 rank的值(需要根据第二关键字和第一关键字)
		int* tmp = rank; rank = order_by_second_key; order_by_second_key = tmp;
		rank[gSuffixArray[0]] = p = 0;
		for (int i = 1; i < n; i++){ //已知 新的step下的子串的次序,直接比较相邻的rank即可
			if (Compare(order_by_second_key, gSuffixArray[i], gSuffixArray[i - 1], step)){
				rank[gSuffixArray[i]] = p;
			}
			else{
				rank[gSuffixArray[i]] = ++p;
			}
		}
		m = p + 1;
		step *= 2;
	}
}
//求height数组
void GetHeight(){
	int n = gStrLen;
	for (int i = 0; i < n; i++){
		gRank[gSuffixArray[i]] = i; //由于在构造str数组的时候,手动添加了0在最末尾,使得gSuffixArray[0]肯定为n
	}
	int k = 0, j;
	for (int i = 0; i < n; i++){
		if (k){
			k--;
		}
		j = gSuffixArray[gRank[i] - 1];
		while (j + k < n && i + k < n&& gStr[i + k] == gStr[j + k]){
			k++;
		}
		gHeight[gRank[i]] = k;  // 计算得到 h[i]
	}
}

//for debug
/*
void printstr(int n){
	printf("string = 
");
	for (int i = 0; i < n; i++){
		printf("%d ", gStr[i]);
	}
	printf("
");
}
void printsuffix(int n){
	printf("suffix = 
");
	for (int i = 0; i < n; i++){
		printf("%d ", gSuffixArray[i]);
	}
	printf("
");
}
void printheight(int n){
	printf("height = 
");
	for (int i = 0; i < n; i++){
		printf("%d ", gHeight[i]);
	}
	printf("
");
}
void printSuffix(char* str, int n){
	for (int i = 0; i < n; i++){
		printf("%d ", gSuffixArray[i]);
		printf("%s
", str + gSuffixArray[i]);
	}
}
void printrank(int n){
	printf("rank = 
");
	for (int i = 0; i < n; i++){
		printf("%d ", gRank[i]);
	}
	printf("
");
}
*/