后缀数组
noco
2018-05-24 19:11:05
## 后缀数组代码及注释
```cpp
// 后缀数组
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+1e3+7;
int n;
int wa[N],wb[N],wv[N];
int sa[N],height[N],rank[N];
/* 后缀数组 SA 是一个一维数组, 它保存 1..n 的某个排列 SA[1] ,
SA[2],……,SA[n],并且保证 Suffix(SA[i]) < Suffix(SA[i+1]),1≤i<n 。
也就是将 S 的 n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺
次放入 SA 中 SA从0开始记录
名次数组 Rank[i]保存的是 Suffix(i)在所有后缀中从小到大排列的“名次”。
height 定义 height[i]=suffix(sa[i-1])和 suffix(sa[i])的最长公
共前缀,也就是排名相邻的两个后缀的最长公共前缀*/
int tong[N];//桶容器 按字符种类建桶 排序使用
char s[N];
bool cmp(int *s,int a,int b,int l){
return s[a]==s[b]&&s[a+l]==s[b+l];
}// 通过二元组的两个下标的比较 来确定两个子串是否相同
void da(char *r,int *sa,int n,int m){
int i,j,p,*x=wa,*y=wb,*t;
for(i=0;i<m;i++)
tong[i]=0; //清空
for(i=0;i<n;i++)
tong[x[i]=r[i]]++;
//不同字符新建一个桶 相同字符放进同一个桶中 并扩展大小 此处x相当于将母串复制一遍
for(i=1;i<m;i++)
tong[i]+=tong[i-1];
// 当前桶内字符的排名都维护一个前面所有点的前缀和
for(i=n-1;i>=0;i--)//倒着扫 可以|手动模拟 !!|
sa[--tong[x[i]]]=i;
// 在满足第一关键字的同时 再满足第二关键字的要求
/*这里 x 数组保存的值相当于是 rank 值。下面的操作只是用 x 数组来比较字
符的大小,所以没有必要求出当前真实的 rank 值*/
for(j=1,p=1;p<n;j<<=1,m=p)//倍增法进行基数排序 j为当前字符串长度 p为当前字符集大小
{
/*基数排序要分两次,第一次是对第二关键字排序,第二次是对第一关键字排序。
对第二关键字排序的结果实际上可以利用上一次求得的 sa 直接算出,没有必要再算一次 */
// y[]的下标就是对应的第二关键字排名 y[]的内容就是第一关键字所在位置
for(i=n-j,p=0;i<n;i++)//因为是后缀排序 因此从n-j+1开始
y[p++]=i;
/*这一步就是在n-j+1到n这个范围内进行排序 根据基数排序的性质 长度不足j的子串一定排在前边
y记录在排到第p位时 其后缀排名*/
for(i=0;i<n;i++)
if(sa[i]>=j)//i这一位是否可以作为第二关键字
y[p++]=sa[i]-j;
/*这里的p起始时已为j 而对与排序来说 这里应减去j才可得到在原串中位置 */
// 第一次:第二关键字排序 y保存对第二关键字排序的结果
/*x[]的内容就是对应的第一关键字排名
根据x[]的内容和y[]的下标进行合并,得到新的排名作为sa[]的下标*/
for(i=0;i<m;i++)
tong[i]=0;//清空桶容器
for(i=0;i<n;i++)
tong[wv[i]=x[y[i]]]++;
/*x为对应的第一关键字的排名 类似于两位数的基数排序大小比较
在满足第一关键字的前提下 再满足第二关键字的条件
并在wv数组中复制此排名 以此排名作为桶的下标然后扩展容器大小*/
for(i=1;i<m;i++)
tong[i]+=tong[i-1];//当前桶的排名即维护前面所有桶大小的前缀和
for(i=n-1;i>=0;i--)//同样倒着扫
sa[--tong[wv[i]]]=y[i];
/*wv数组中也就得到了x的内容和y的下标进行合并后新的排名
并把这个排名和桶的大小排名一起作为sa的下标 并且根据y的下标为第二关键字排名
进行倒搜 因为桶是倒着退的 然后将sa的内容赋为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++;
/*
1.可能有多个字符串的 rank 值是相同的,所以必须比较两个字符串是否完全相同
也就是cmp函数的作用 y 数组的值已经没有必要保存 为了节省空间 这里用 y 数组保存 rank值
这里又有一个小优化,将 x 和 y 定义为指针类型,复制整个数组的操作可以用交换指针的值代替
2.规定 r[n-1]=0 的好处:如果 r[a]=r[b] 说明以 r[a]或 r[b]开头的长度为l的字符串肯定不包括字符r[n-1]
所以调用变量 r[a+l]和r[b+l]不会导致数组下标越界,这样就不需要做特殊判断
3.p在这里储存的也就是不同字符串的大小
*/
//最后 求取rank数组 这里x的内容也就是rank值 下标为位置
//在第一次排序后 rank数组中最大值小于p 因此在循环起始时使m=p
}
}
//下一步进行对height数组的求解
/*设rank[j]<rank[k],则有以下性质:suffix(j)suffix(k)的最长公共前缀height[rank[j]+1],
, height[rank[j]+2], , height[rank[j]+3], … … ,height[rank[k]] 中的最小值。
原因:可以考虑如果一直向下推 相当于在相邻最长公共前缀前逐一添加限制 因此排名越靠后 限制越多
因此 在其中取最小值 即限制最多的最长公共前缀 */
void calheight(char *r,int *sa,int n){
int i,j,k=0;
//首先提取rank数组
for(i=1;i<=n;i++)
rank[sa[i]]=i;
/*根据上一步中所求出的sa的值来获取rank的值 用sa的内容即排好序后后缀的位置作为rank数组的下标
根据sa数组的性质rank数组的内容也就是i 因为已经排好序了啊*/
//然后计算height数组
/*定义 h[i]=height[rank[i]],也就是 suffix(i)和在它前一名的后缀的最长公共前缀。
h数组有以下性质:h[i]≥h[i-1]-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(i)和在它前一名的后缀的最长公共前缀至少是 h[i-1]-1。
按照 h[1],h[2],……,h[n]的顺序计算,并利用 h 数组的性质,时间复杂度可以降为 O(n)。
具体实现:实现的时候其实没有必要保存 h 数组,只须按照 h[1],h[2],……,h[n]的顺序计算即可。*/
for(i=0;i<n;height[rank[i++]]=k)
for(k?k--:0,j=sa[rank[i]-1];r[i+k]==r[j+k];k++);
/* 另一实现版本(只是将论文中的拆解开而已 便于理解):
int i,j,k=0;
for(i=1;i<=n;i++)rank[sa[i]]=i; //同上
for(i=0;i<n;height[rank[i++]]=k){
if(k>0)k--; //由最基本的暴力算法 可知height会随向下比较减小
j=sa[rank[i]-1]; // 利用h[i]>=h[i-1]-1的性质 利用后缀顺序进行比较
for(;r[i+k]==r[j+k];k++); //即此步
}
*/
}
int main(){
scanf("%s",s);
n=strlen(s);
da(s,sa,n+1,257);
calheight(s,sa,n);
for(int i=0;i<n;i++)
printf("%d ",sa[i+1]+1);
printf("\n");
for(int i=2;i<=n;i++)
printf("%d ",height[i]);
}
```