一、后缀数组 及其对应的名次数组
举例:S=" B A C $ " , 后缀数组长度为n-1= 3 , 假定'$'<字符集Σ中的任意字符
1 2 3 4
1. 后缀数组SA=[4 2 1 3] , 对应所有后缀的一个字典序 从小到大的序列
SA[1]=4 --> 对应 "$"
SA[2]=2 --> 对应 "A C $"
SA[3]=1 --> 对应 "B A C $"
SA[4]=3 --> 对应 "C $"
2. 名次数组Rank=SA^-1, 方便地查询每个后缀在后缀数组中的名次
Rank[4]=1 --> Suffix(4)="$" 在后缀数组中排第1
Rank[2]=2 --> Suffix(2)="A C $" 在后缀数组中排第2
Rank[1]=3 --> Suffix(1)="B A C $" 在后缀数组中排第3
Rank[3]=4 --> Suffix(3)="C $" 在后缀数组中排第4
一、构造后缀数组——“k-前缀比较”的倍增方法(Doubling Algorithm)
1. k-前缀
字符串u的 k-前缀 :
两个字符串的 k-前缀比较关系 :
2. “k-前缀比较”的3个性质
A. 性质1.2、 1.3 使得我可以倍增的重排:
“1-前缀排序”的前缀数组SA_1[]=Suffix(i)...
“2-前缀排序”的前缀数组SA_2[]=Suffix(i)...
“4-前缀排序”的前缀数组SA_4[]=Suffix(i)...
“8-前缀排序”的前缀数组SA_8[]=Suffix(i)...
...
“k-前缀排序”的前缀数组SA_k[]=Suffix(i)...
注: 所有SA_i中的元素都是一样的(字符串的所有可能后缀),只不过排列顺序不同
B. 性质1.1 当k≥n时,“字典顺序” 和 “k-前缀顺序”就是一个东东了!
告诉我们当k≥n时,就可以结束了:) 因为此时SA_k[]中后缀顺序即是“所有后缀按照字典顺序的排列”了。
3. 基于k-前缀的构造后缀数组(及其名次数组)
初始化: 从 “1-前缀 前缀数组SA_1” 开始; 计算出她对应的 “1-前缀 名次数组Rank_1” ; //O(n)时间 (Rank=SA^-1) while(k<n){ //O(nlogn)时间, // A. “2k-前缀比较”Suffix_2k(i)和Suffix_2k(j)用时O(1) // B. 从小到大快排所有Suffix_2k(i)用时O(nlogn)(注:大小比较是=2k, <2k这样的“2k-前缀比较”) 性质2.3,用“k-前缀 前缀数组SA_k” 和 “k-前缀 名次数组Rank_k” 构造 “2k-前缀 前缀数组SA_2k” ; //O(n)时间 (Rank=SA^-1) 计算 “2k-前缀 前缀数组SA_2k” 对应的 “2k-前缀 名次数组Rank_2k” ; }
三、height[]数组 height[i]=lcp( Suffix(SA[i-1]), Suffix(SA[i]) )
区别和明确几个定义: lcp(u,v)和LCP(i,j); height[]和h[]的定义:
(1)任意两个字符串的“最长公共前缀长度 ”: lcp(u,v) = max{i | u =i v}, =i表示“i-前缀等于”关系
int lcp(String u, String v);
(2)后缀数组中两个元素对应字符串的“最长公共前缀长度 ”:LCP(i,j) = lcp( Suffix(SA[i]), Suffix(SA[j]) )
int LCP(int i, int j);
注意:i和j是在后缀数组SA[]中的排名/位置 ,不对应原字符串中后缀的开始位置;
SA[i]和SA[j]才对应了这两个后缀 开始的字符 在原字符串中的位置 。
LCP Theorem :
设i<j,则LCP(i,j) = min{ LCP(k-1,k) | i+1≤k≤j } = min{ height[i+1], height[i+2], ... , height[j] }
(3)后缀数组中“第i-1个元素”和“第i个元素”的“最长公共前缀长度 ”
height[i] = LCP(i-1, i) = lcp( Suffix(SA[i-1]), Suffix(SA[i]) )
(4)原字符串中“从位置i开始的后缀”和“该后缀在SA[]中前一个元素(另一个后缀)”的“ 最长公共前缀长度 ”
h[i] = height[ Rank[i] ] = height[Rank[i]-1, Rank[i]] = lcp( Suffix(Rank[i]-1), Suffix(Rank[i]) )
注意:
(1)
height[i]中i 含义是 “目标后缀是后缀数组SA[]中的第i个元素/SA[]中排名第i的元素/字典序第i小的元素”
h[i]中的i 含义是 “目标后缀是从原字符串中位置i开始的后缀S[i, ..., n]”. 令,原字符串为S[1, ..., n]
(2)数组名称总是代表函数值的含义
sa[排名]=后缀字符串
rank[后缀字符串]=排名
模板代码:我的这个模板改造了罗穗骞的代码,使之更容易理解
/
//Constructing Suffix Array with Doubling Algorithm
//Constructing Height[]
/
#include <algorithm>//sort
#include <cstring>//memset
#include <iostream>
using namespace std;
const int MAX_SFX = 210000;
struct Sfx {
//i --> 后缀在“原数组”中的位置(唯一确定了一个后缀字符串)
int i;
//key[0] --> 后缀S[i,...,n]在 “k-前缀排序”中的位置
//key[1] --> 后缀S[i+k,...,n]在“k-前缀排序”中的位置
int key[2];
bool operator < (const Sfx& s) const //后面的const是什么意思???
{
return key[0] < s.key[0]
|| (key[0] == s.key[0] && key[1] < s.key[1]);
}
};
int g_buf[MAX_SFX + 1];
Sfx g_tempSfx[2][MAX_SFX];
Sfx *g_sa = g_tempSfx[0]; //后缀数组 g_sa[0]~g_sa[len-1]
int rank[MAX_SFX]; //名次数组 rank[0]~rank[len-1]
int height[MAX_SFX]; //height数组 height[0]~height[len-1]. 注:包括 height[0](==0)
/*
CALL: cSort(in, len, key∈{0,1}, out);
EFFECT: in[0]~in[len-1],按照域 in[i].key[key]进行排序,结果放入out中
理解基数排序:
假设有4个字母 in[]={A B B A},即,有2个A;有2个B.
即,有2个≤A ;有4个≤B
现在按照字母顺序放置这4个字母 in[]={A B B A}
方法:
cnt[A]=2
cnt[B]=4
out[--cnt[B]]=B, 即out[--4]=B,即 out[3]=B
out[--cnt[B]]=B, 即out[--3]=B,即 out[2]=B
out[--cnt[A]]=A, 即out[--2]=A,即 out[1]=A
out[--cnt[A]]=A, 即out[--1]=A,即 out[0]=A
*/
void cSort(Sfx* in, int n, int key, Sfx* out) {
//cnt[]: cnt[i]表示 in[i].key[1]的值≤i的 in[i]的个数
int* cnt = g_buf;
memset( cnt, 0, sizeof(int) * (n + 1) );
for (int i = 0; i < n; i++){
cnt[ in[i].key[key] ]++;
}
for (int i = 1; i <= n; i++){
cnt[i] += cnt[i - 1];
}
for (int i = n - 1; i >= 0; i--){
//输入元素数组中的元素 in[i] 应该放到输出数组中的位置 out[...]
out[ --cnt[ in[i].key[key] ] ] = in[i];
}
}
/*
Build a suffix array from string 'text' whose length is 'len'.
write the result into global array 'g_sa'.
*/
void buildSA(char* text, int len) {
Sfx *temp = g_tempSfx[1];
int* rank = g_buf;
//1. g_sa[]中后缀数组按照 1-前缀关系从小到大排列
for (int i = 0; i < len; i++){
g_sa[i].i = i;
g_sa[i].key[0] = text[i];
g_sa[i].key[1] = i; //这句省略后结果相同
}
sort(g_sa, g_sa + len);
for (int i = 0; i < len; i++) {
g_sa[i].key[1] = 0;
}
//2. 每次循环计算 “k-前缀关系 名次数组” 和 “2k-前缀关系 后缀数组”
int k = 1;
while (k < len) {
//2.1 计算 “k-前缀关系 名次数组”
//计算 k-前缀关系名次数组rank[]前,g_sa[]已经按照 k-前缀关系 ↑排序了
rank[ g_sa[0].i ] = 1;
for (int i = 1; i < len; i++){
rank[ g_sa[i].i ] = rank[ g_sa[i - 1].i ];
if( g_sa[i-1] < g_sa[i] ){
rank[ g_sa[i].i ]++;
}
}
//2.2 计算 “2k-前缀关系 后缀数组”
// 2.2.A 设置了g_sa[]中每个元素的 i, key[0], key[1]三个域
for (int i = 0; i < len; i++){
g_sa[i].i = i; //i --> 后缀在“原数组”中的位置(唯一确定了一个后缀字符串)
g_sa[i].key[0] = rank[i]; //key[0] --> 后缀S[i,...,n]在 “k-前缀排序”中的位置
g_sa[i].key[1] = i + k < len? rank[i + k]: 0; //key[1] --> 后缀S[i+k,...,n]在“k-前缀排序”中的位置
}
// 2.2.B 根据 key[0], key[1]两个域,从小到大排列 g_sa[]中的每个元素
cSort(g_sa, len, 1, temp);
cSort(temp, len, 0, g_sa);
k *= 2;
}
}
/*
Build height[]
注:朴素算法计算height[]需要 O(n^2); 这里采用罗穗骞的算法,按照h[i]的顺序计算,时间复杂度为O(n)
*/
void buildHeight(char* str, Sfx* sa, int len){
//构造名次数组
for(int i=0; i<len; i++)
rank[ sa[i].i ] = i;
//按照 h[1], h[2], ... , h[n]的顺序计算,即按照 height[ rank[1] ], height[ rank[2] ],...顺序计算
//这样计算h[]的累积时间为O(n)
// h[]定义:h[i] = height[ rank[i] ]
// h[]性质:h[i] >= h[i-1]-1 有点像单调增:不会大退步
int k=0;
for(int i=0; i<len; i++){
//此刻, k==h[i-1]
if(k>0) k--;
//此刻,k==h[i-1]-1
//j: 后缀数组中排在Suffix(i)前一位的后缀的位置
int j=sa[ rank[i]-1 ].i;
//h[i]≥h[i-1]-1 == k, 只需将 k从 h[i-1]增长到 h[i]即可
while(str[i+k]==str[j+k])
k++;
//相当于计算 h[i],因为height[rank[i]] = h[i]
height[ rank[i] ] = k;
}
}
int main() {
/* 字符串尾部填充'\0',(int)'\0'为0,小于所有字符的值*/
char str[] = "aabbaaababab";
buildSA(str, 13);
buildHeight(str, g_sa, 13);
//The first suffix is useless (empty).
for (int i=0; i<13; i++){
cout
<<g_sa[i].i
<<' '<<str+g_sa[i].i
<<' '<<height[i]
<<endl;
}
system("pause");
return 0;
}
四、后缀数组的应用
1. 最长公共前缀
对于j, k,则有(我起的名)“最长公共前缀性质 ”:
Suffix(j)和Suffix(k)是同一个字符串的两个后缀 。,不妨设rank[j]<rank[k],则Suffix(j)和Suffix(k)的最长公共前缀为height[rank[j]+1], height[rank[j]+2], height[rank[j]+3], ... , height[rank[k]]中的最小值。这样就可以用RMQ问题的几种解法(线段树/ST算法)。
2. 单个字符串相关问题
例一、给定一个字符串,求最长重复子串
解法:O(n): 查找height[]中最大元素值
例二、给定一个字符串,求最长不重叠重复子串
解法:
什么是不重叠最长子串呢,就是一个串中至少出现两次,又不重叠的子串中的最长的,比较绕口。
解决这个问题的关键还是利用height 数组。把排序后的后缀分成若干组,其中每组的后缀之间的height 值都不小于k。然后找出各个组的后缀的sa值的最大最小值max,min,如果存在 max-min >= k,那么就存在长度为k的不重叠子串,因为根据LCP定理,每个组中的height值都不小于k,就是说这个组中这些后缀的最长公共前驱最小是k,然后由于max-min>= k,所以可以判定存在所要求的子串。做题的时候二分答案,把问题变为判定性问题(大牛说的)。那么整个事件复杂度为O(nlogn)。把height数组分组的思想非常常用,可以多看看IOI论文了解了解。
(1)把题目变成判定性问题“判断是否存在两个长度≥k的两个相同子串,且不重叠”。将后缀数组分为若干组。其中后缀之间height值≥k的后缀在同一组,显然只有这样的组才可能出现长度≥k的两个相同子串,且不重叠。
(2)对于满足上述要求的组判断每个后缀的sa值的最大值和最小值之差是否≥k(≥k则不重叠)。
注:基于这个问题的扩展见POJ 1743 Musical Theme 传说中楼教主男人八题之一。
例三、给定一个字符串,求至少出现k次的最长重复子串(这k个子串可以重叠)???
例四、子串的个数——给定一个字符串,求不相同的子串的个数???