后缀数组被称为字符串处理神器,要解决字符串问题,一定要掌握它。(我这里的下标全部都是从1开始)
首先后缀数组要处理出两个数组,一个是sa[],sa[i]表示排名第i为的后缀的起始位置是什么,rank[i]表示第i个字符为起始点的后缀,它的排名是什么。可以知道sa[rank[i]] = i; rank[sa[i]] = i;
由于每个后缀各不相同,至起码长度不同,所以每个后缀是不可能相等的。
解除一个值,就能在O(n)时间内得到另外一个。
定义:suffix(i)表示从[i, lenstr]这个后缀。
普通排序复杂度显然是O(n^2),因为快排最坏情况也是O(n^2)。考虑运用字符串的特点?。这里考虑倍增法。为什么能用倍增呢?因为它充分利用了前面已经得到的信息。因为字符串的比较,都是从头到尾比较的,那么如果已经知道一个字符串前len / 2部分的比较结果,对于后加进来的后len / 2部分,还需要继续比较前len / 2部分吗?答案是不用的。所以,考虑倍增,先得到1个字符自己的rank,然后考虑2个字符的时候,用后一个字符的rank来作为第二关键字,进行排序即可。
对关键字排序选用的是基数排序,因为它可以在O(n + maxnum)的时间内排好序。然后为了预防越界,就是aaaa这样,suffix(1)和suffix(2)比较的话,前3个都是匹配的,然后明显是"aaa",suffix(2)比较小,所以就在末尾加上一个0,来预防越界,也能解决排名问题。
关于后缀数组的字符数组为什么用int str[]这样:
ans:因为有时候解题的时候,需要把n个串连接起来,那么你每两个串之间就要加上一些不会出现的字符,来防止越界。
就是aaa%aaa%aaa这样是没用的,因为两个%会相等,使得LCP变大。aaa$aaa%aaa#才是正确的打开方式。
那么问题来了,n很大,1000个左右,你用char字符无能为力了,所以这个时候只能用int str[]了。
但是一般的题,都是1个或者两个字符串而已,用char str[]是足够的。
题目就是那个POJ 3294了。
book[]大小,用于基数排序,起码要大于lenstr,因为要记录rank[],而rank[]会有lenstr那么大

const int maxn = 20000 + 20; int a[maxn]; int sa[maxn]; int x[maxn]; int y[maxn]; int book[10000000 + 20]; bool cmp(int r[], int a, int b, int len) { return r[a] == r[b] && r[a + len] == r[b + len]; } void da(int str[], int sa[], int lenstr, int mx) { int *fir = x, *sec = y, *ToChange; for (int i = 0; i <= mx; ++i) book[i] = 0; //清0 for (int i = 1; i <= lenstr; ++i) { fir[i] = str[i]; // 开始的rank数组,只保留相对大小即可,开始就是str[] book[str[i]]++; //统计不同字母的个数 } for (int i = 1; i <= mx; ++i) book[i] += book[i - 1]; //统计 <= 这个字母的有多少个元素 for (int i = lenstr; i >= 1; --i) sa[book[fir[i]]--] = i; // <=str[i]这个字母的有x个,那么,排第x的就应该是这个i的位置了。 //倒过来排序,是为了确保相同字符的时候,前面的就先在前面出现。 //倍增法求sa[],复杂度O(nlogn),p是第二个关键字0的个数 for (int j = 1, p = 1; p <= lenstr; j <<= 1, mx = p) { //字符串长度为j的比较 //上面已经求出了第一个关键字了,现在求第二个关键字,然后合并(合并的时候按第一关键字优先合并) p = 0; for (int i = lenstr - j + 1; i <= lenstr; ++i) sec[++p] = i; //这些位置,再跳j格就是越界了的,所以第二关键字是0,排在前面 for (int i = 1; i <= lenstr; ++i) if (sa[i] > j) //如果排名第i的起始位置在长度j之后 sec[++p] = sa[i] - j; //减去这个长度j,表明第sa[i] - j这个位置的第二个关键字是从sa[i]处拿的,排名靠前也正常,因为sa[i]排名是递增的 //sec[]保存的是下标,现在对第一个关键字排序 for (int i = 0; i <= mx; ++i) book[i] = 0; //清0 for (int i = 1; i <= lenstr; ++i) book[fir[sec[i]]]++; for (int i = 1; i <= mx; ++i) book[i] += book[i - 1]; for (int i = lenstr; i >= 1; --i) sa[book[fir[sec[i]]]--] = sec[i]; //因为sec[i]才是对应str[]的下标 //现在要把第二关键字的结果,合并到第一关键字那里。同时我需要用到第一关键字保存的记录 //所以用指针交换的方式达到快速交换数组中的值 ToChange = fir; fir = sec; sec = ToChange; fir[sa[1]] = 0; //固定的是0 因为sa[1]固定是lenstr那个0 p = 2; for (int i = 2; i <= lenstr; ++i) //fir是当前的rank值,sec是前一次的rank值 fir[sa[i]] = cmp(sec, sa[i - 1], sa[i], j) ? p - 1 : p++; } return ; } int rank[maxn]; int height[maxn]; //height[i]:表示suffix(sa[i - 1]) 和 suffix(sa[i]) 的LCP //就是两个排名紧挨着的后缀的LCP //sa[rank[i]] = i; rank[sa[i]] = i; void CalcHight(int str[], int sa[], int lenstr) { for (int i = 1; i <= lenstr; ++i) rank[sa[i]] = i; int k = 0; for (int i = 1; i <= lenstr - 1; ++i) { //最后一位不用算,最后一位排名一定是1,然后sa[0]就尴尬了 k -= k > 0; int j = sa[rank[i] - 1]; //排名在i前一位的那个串,相似度最高 while (str[j + k] == str[i + k]) ++k; height[rank[i]] = k; } return ; }
height[],height[i]表示suffix(sa[i])和suffix(sa[i - 1])的LCP
可知道,sa[1]是最后末尾那个0(因为字典序总是最小的),而它没有前一个后缀,所以height[1] = 0是一定的。同理,sa[2]和sa[1],也是没有交集的,因为sa[1]的开头就是那个0,所以height[2] = 0也是一定的。而相反,height[lenstr + 1]是有定义的,因为被0占据了sa[1],所以其他的后移一位。

int rank[maxn]; int height[maxn]; //height[i]:表示suffix(sa[i - 1]) 和 suffix(sa[i]) 的LCP //就是两个排名紧挨着的后缀的LCP //sa[rank[i]] = i; rank[sa[i]] = i; void CalcHight(int str[], int sa[], int lenstr) { for (int i = 1; i <= lenstr; ++i) rank[sa[i]] = i; int k = 0; for (int i = 1; i <= lenstr - 1; ++i) { //最后一位不用算,最后一位排名一定是1,然后sa[0]就尴尬了 k -= k > 0; int j = sa[rank[i] - 1]; //排名在i前一位的那个串,相似度最高 while (str[j + k] == str[i + k]) ++k; height[rank[i]] = k; } return ; }
更多详细的,请看这篇文章,我忘记了是谁写的了,找不到原文,真的真的不好意思。sorry

#include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <algorithm> using namespace std; #define inf (0x3f3f3f3f) typedef long long int LL; #include <iostream> #include <sstream> #include <vector> #include <set> #include <map> #include <queue> #include <string> const int maxn = 5000 + 20; int wa[maxn],wb[maxn],wv[maxn],WS[maxn]; int sa[maxn]; char r[maxn]; int cmp(int *r,int a,int b,int l) { return r[a]==r[b]&&r[a+l]==r[b+l]; //就像论文所说,由于末尾填了0,所以如果r[a]==r[b]( //实际是y[a]==y[b]), //说明待合并的两个长为j的字符串,前面那个一定不包含末尾0, //因而后面这个的起始位置至多在0的位置,不会再靠后了,因而不会产生 //数组 //越界。 } //da函数的参数n代表字符串中字符的个数,这里的n里面是包括人为在 //字符串末尾添加的那个0的,但论文的图示上并没有画出字符串末尾的0。 //da函数的参数m代表字符串中字符的取值范围,是基数排序的一个参数, //如果原序列都是字母可以直接取128,如果原序列本身都是整数的话, //则m可以取比最大的整数大1的值。 void da(char *r,int *sa,int n,int m) { int i,j,p,*x=wa,*y=wb,*t; //以下四行代码是把各个字符(也即长度为1的字符串)进行基数排序, //如果不理解为什么这样可以达到基数排序的效果, //不妨自己实际用纸笔模拟一下,我最初也是这样才理解的。 for(i=0; i<m; i++) WS[i]=0; for(i=0; i<n; i++) WS[x[i]=r[i]]++; //x[]里面本意是保存各个后缀的rank值的,但是这里并没有去存储rank值, //因为后续只是涉及x[]的比较工作,因而这一步可以不用存储真实的rank值 //,能够反映相对的大小即可。 for(i=1; i<m; i++) WS[i]+=WS[i-1]; for(i=n-1; i>=0; i--) sa[--WS[x[i]]]=i; //i之所以从n-1开始循环,是为了保证在当字符串中有相等的字符串时, //默认靠前的字符串更小一些。 // for (int i = 0; i < n; ++i) { // cout << sa[i] << " "; // } //下面这层循环中p代表rank值不用的字符串的数量,如果p达到n,那么各个字符串的大小关系就已经明了了。 //j代表当前待合并的字符串的长度,每次将两个长度为j的字符串合并成一个长度为2*j的字符串, //当然如果包含字符串末尾具体则数值应另当别论,但思想是一样的。 //m同样代表基数排序的元素的取值范围 for(j=1,p=1; p<n; j*=2,m=p) { //以下两行代码实现了对第二关键字的排序 for(p=0,i=n-j; i<n; i++) y[p++]=i; //结合论文的插图,我们可以看到位置在第n-j至n的元素的第二关键字 //都为0,因此如果按第二关键字排序,必然这些元素都是排在前面的。 for(i=0; i<n; i++) if(sa[i]>=j) y[p++]=sa[i]-j; //结合论文的插图,我们可以看到,下面一行的第二关键字不为0的 //部分都是根据上面一行的排序结果得到的,且上一行中只有sa[i]>=j //的第sa[i]个字符串(这里以及后面指的“第?个字符串”不是按字典序 //排名来的,是按照首字符在字符串中的位置来的)的rank才会作为下 //一行的第sa[i]-j个字符串的第二关键字,而且显然按sa[i]的 //顺序rank[sa[i]]是递增的,因此完成了对剩余的元素的第二关键字的 //排序。 // for (int i = 0; i < p; ++i) { // cout << y[i] << " "; // } // cout << endl; //第二关键字基数排序完成后,y[]里存放的是按第二关键字排序的字符串下标 for(i=0; i<n; i++) wv[i]=x[y[i]]; //这里相当于提取出每个字符串的第一关键字(前面说过了x[]是 //保存rank值的,也就是字符串的第一关键字),放到wv[]里面是 //方便后面的使用 // for (int i = 0; i < n; ++i) { // cout << wv[i] << " "; // } // cout << endl; //以下四行代码是按第一关键字进行的基数排序 for(i=0; i<m; i++) WS[i]=0; for(i=0; i<n; i++) WS[wv[i]]++; for(i=1; i<m; i++) WS[i]+=WS[i-1]; for(i=n-1; i>=0; i--) sa[--WS[wv[i]]]=y[i]; //i之所以从n-1开始循环,含义同上,同时注意这里是y[i], //因为y[i]里面才存着字符串的下标 //下面两行就是计算合并之后的rank值了,而合并之后的rank值应该存 // 在x[]里面,但我们计算的时候又必须用到上一层的rank值, //也就是现在x[]里面放的东西,如果我既要从x[]里面拿, //又要向x[]里面放,怎么办?当然是先把x[]的东西放到另外一个 //数组里面,省得乱了。这里就是用交换指针的方式, //高效实现了将x[]的东西“复制”到了y[]中。 for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1; i<n; i++) x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++; //这里就是用x[]存储计算出的各字符串rank的值了, //记得我们前面说过,计算sa[]值的时候如果字符串相同是 //默认前面的更小的,但这里计算rank的时候必须将相同 //的字符串看作有相同的rank,要不然p==n之后就不会再循环啦。 // cout << p << endl; } return; } //能够线性计算height[]的值的关键在于h[](height[rank[]])的性质, //即h[i]>=h[i-1]-1,下面具体分析一下这个不等式的由来。 //论文里面证明的部分一开始看得我云里雾里,后来画了一下终于搞明白了, //我们先把要证什么放在这:对于第i个后缀,设j=sa[rank[i] - 1], //也就是说j是i的按排名来的上一个字符串,按定义来i和j的最长公共前缀就 //是height[rank[i]],我们现在就是想知道height[rank[i]]至少是多少, //而我们要证明的就是至少是height[rank[i-1]]-1。 //好啦,现在开始证吧。 //首先我们不妨设第i-1个字符串(这里以及后面指的“第?个字符串” //不是按字典序排名来的,是按照首字符在字符串中的位置来的) //按字典序排名来的前面的那个字符串是第k个字符串,注意k不一定是i-2, //因为第k个字符串是按字典序排名来的i-1前面那个,并不是指在原字符串中 //位置在i-1前面的那个第i-2个字符串。 //这时,依据height[]的定义,第k个字符串和第i-1个字符串的公共前缀 //自然是height[rank[i-1]],现在先讨论一下第k+1个字符串和第i个字符串 //的关系。 //第一种情况,第k个字符串和第i-1个字符串的首字符不同, //那么第k+1个字符串的排名既可能在i的前面,也可能在i的后面, //但没有关系,因为height[rank[i-1]]就是0了呀,那么无论height[rank[i]] //是多少都会有height[rank[i]]>=height[rank[i-1]]-1, //也就是h[i]>=h[i-1]-1。 //第二种情况,第k个字符串和第i-1个字符串的首字符相同, //那么由于第k+1个字符串就是第k个字符串去掉首字符得到的, //第i个字符串也是第i-1个字符串去掉首字符得到的,那么显然 //第k+1个字符串要排在第i个字符串前面,要么就产生矛盾了。同时, //第k个字符串和第i-1个字符串的最长公共前缀是height[rank[i-1]], //那么自然第k+1个字符串和第i个字符串的最长公共前缀就是 //height[rank[i-1]]-1。 //到此为止,第二种情况的证明还没有完,我们可以试想一下, //对于比第i个字符串的字典序排名更靠前的那些字符串, //谁和第i个字符串的相似度最高(这里说的相似度是指 //最长公共前缀的长度)?显然是排名紧邻第i个字符串的那个字符串了呀, //即sa[rank[i]-1]。也就是说sa[rank[i]]和sa[rank[i]-1]的最长公共 //前缀至少是height[rank[i-1]]-1,那么就有 //height[rank[i]]>=height[rank[i-1]]-1,也即h[i]>=h[i-1]-1。 //证明完这些之后,下面的代码也就比较容易看懂了。 int rank[maxn],height[maxn]; void calheight(char *r,int *sa,int n) { int i,j,k=0; for(i=1; i<=n; i++) rank[sa[i]]=i; //计算每个字符串的字典序排名 for(i=0; i<n; height[rank[i++]]=k) //将计算出来的height[rank[i]]的值,也就是k,赋给height[rank[i]]。 //i是由0循环到n-1,但实际上height[]计算的顺序是由height[rank[0]] //计算到height[rank[n-1]]。 for(k?k--:0,j=sa[rank[i]-1]; r[i+k]==r[j+k]; k++); //上一次的计算结果是k,首先判断一下如果k是0的话, //那么k就不用动了,从首字符开始看第i个字符串和第j个字符串前 //面有多少是相同的,如果k不为0,按我们前面证明的,最长公共 //前缀的长度至少是k-1,于是从首字符后面k-1个字符开始检查起即 //可。 return; } //最后再说明一点,就是关于da和calheight的调用问题,实际上在“小罗”写的源程序里面是如下调用的, //这样我们也能清晰的看到da和calheight中的int n不是一个概念, //同时height数组的值的有效范围是height[1]~height[n]其中height[1]=0, //原因就是sa[0]实际上就是我们补的那个0,所以sa[1]和sa[0]的最长公共前 //缀自然是0。 void work () { strcpy(r,"1111111111"); int n = strlen(r); r[n]=0; da(r,sa,n+1,128); for (int i = 0; i < n + 1; ++i ) { cout << sa[i] << " "; } cout << endl; calheight(r,sa,n); for (int i = 0; i < n; ++i) { cout << rank[i] << " "; } cout << endl; for (int i = 0; i < n; ++i) { cout << height[i] << " "; } cout << endl; } int main () { work (); return 0; }
Question 1:
如果求任意给定后缀suffix(i)和suffix(j)的LCP?
ansLCP = min (height[rank[i] + 1],....height[j]);
例如:aabaaaab

#include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <algorithm> using namespace std; #define inf (0x3f3f3f3f) typedef long long int LL; #include <iostream> #include <sstream> #include <vector> #include <set> #include <map> #include <queue> #include <string> const int maxn = 4000 + 20; char str[maxn]; int sa[maxn], x[maxn], y[maxn], book[300]; bool cmp(int r[], int a, int b, int len) { return r[a] == r[b] && r[a + len] == r[b + len]; } void da(char str[], int sa[], int lenstr, int mx) { int *fir = x, *sec = y, *ToChange; for (int i = 0; i <= mx; ++i) book[i] = 0; //清0 for (int i = 1; i <= lenstr; ++i) { fir[i] = str[i]; //开始的rank数组,只保留相对大小即可,开始就是str[] book[str[i]]++; //统计不同字母的个数 } for (int i = 1; i <= mx; ++i) book[i] += book[i - 1]; //统计 <= 这个字母的有多少个元素 for (int i = lenstr; i >= 1; --i) sa[book[fir[i]]--] = i; // <=str[i]这个字母的有x个,那么,排第x的就应该是这个i的位置了。 //倒过来排序,是为了确保相同字符的时候,前面的就先在前面出现。 //p是第二个关键字0的个数 for (int j = 1, p = 1; p <= lenstr; j <<= 1, mx = p) { //字符串长度为j的比较 //现在求第二个关键字,然后合并(合并的时候按第一关键字优先合并) p = 0; for (int i = lenstr - j + 1; i <= lenstr; ++i) sec[++p] = i; //这些位置,再跳j格就是越界了的,所以第二关键字是0,排在前面 for (int i = 1; i <= lenstr; ++i) if (sa[i] > j) //如果排名第i的起始位置在长度j之后 sec[++p] = sa[i] - j; //减去这个长度j,表明第sa[i] - j这个位置的第二个是从sa[i]处拿的,排名靠前也//正常,因为sa[i]排名是递增的 //sec[]保存的是下标,现在对第一个关键字排序 for (int i = 0; i <= mx; ++i) book[i] = 0; //清0 for (int i = 1; i <= lenstr; ++i) book[fir[sec[i]]]++; for (int i = 1; i <= mx; ++i) book[i] += book[i - 1]; for (int i = lenstr; i >= 1; --i) sa[book[fir[sec[i]]]--] = sec[i]; //因为sec[i]才是对应str[]的下标 //现在要把第二关键字的结果,合并到第一关键字那里。同时我需要用到第一关键//字保存的记录,所以用指针交换的方式达到快速交换数组中的值 ToChange = fir; fir = sec; sec = ToChange; fir[sa[1]] = 0; //固定的是0 因为sa[1]固定是lenstr那个0 p = 2; for (int i = 2; i <= lenstr; ++i) //fir是当前的rank值,sec是前一次的rank值 fir[sa[i]] = cmp(sec, sa[i - 1], sa[i], j) ? p - 1 : p++; } return ; } int RANK[maxn], height[maxn]; void CalcHight(char str[], int sa[], int lenstr) { for (int i = 1; i <= lenstr; ++i) RANK[sa[i]] = i; //O(n)处理出rank[] int k = 0; for (int i = 1; i <= lenstr - 1; ++i) { //最后一位不用算,最后一位排名一定是1,然后sa[0]就尴尬了 k -= k > 0; int j = sa[RANK[i] - 1]; //排名在i前一位的那个串,相似度最高 while (str[j + k] == str[i + k]) ++k; height[RANK[i]] = k; } return ; } void work() { scanf("%s", str + 1); int lenstr = strlen(str + 1); str[lenstr + 1] = '$'; str[lenstr + 2] = '