后缀数组的正确性证明
后缀数组的正确性证明
后缀数组的一些定义
定义1:一个从下标1开始长度为n字符串,从第i位到第n位组成的字串称为此字符串的第i个后缀。suf(i)函数表示该字符串的第i个后缀。
定义2:将字符串所有后缀字串从小到大排序,排名第i的字串被称为排名第i的后缀。lev(i)函数表示该字符串的排名第i的后缀。
定义3:lcp(i,j)函数表示lev(i)与lev(j)最长公共前缀的长度。
定义4:数组sa,有:排名第i的后缀是第sa[i]个后缀。即lev(i)=suf(sa[i])。
定义5:数组rk,有:第i个后缀是是排名rk[i]个后缀。即lev(rk[i])=suf(i)。
定义6:数组height,有:lcp(i,i-1) = height[i] (i>=2)。
sa数组的建立
类桶排序
给定一些范围在[1,n]的数字,使用cnt数组记录每个数字出现的次数,求cnt前缀和数组s。可知s[i]表示小于等于i的数的个数。若无重复数字,s[i]即i的排名;若有重复数字,每次遇到i使s[i]-1即可。为保证稳定排序,需要从后往前排序。
基数排序
将整数按位数切割成不同的数字,然后按每个位数分别比较:
① 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
② 从最低位开始,依次进行一次类桶排序。
③ 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
依照key排序
定义7:长度为n的字符串,从前往后每k个字符一组,分割成n/k个字串(长度不够,用空字符拼凑)。从前往后,第i个子串的称为长度为k的第i关键字(key)。
使用倍增的思想,k从1开始,每轮依照长度为2*k的第1关键字把字符串所有后缀基数排序,具体而言:先依照长度为k的第2关键字类桶排序,再依照长度为k的第1关键字类桶排序,最后k翻倍,进行下一轮直到k大于n。
一些细节:
void get_sa(void)
{
n = strlen(s+1), m = 'z';
// 依照首字母预处理k1和sa数组
for(int i=1;i<=n;i++) cnt[k1[i] = s[i]] ++;
for(int i=2;i<=m;i++) cnt[i] += cnt[i-1];
for (int i = n; i; i -- ) sa[cnt[k1[i]] -- ] = i;
for(int k=1; k<=n; k<<=1)
{
int num = 0;
// n-k+1之后的后缀无第2关键字,认为第2关键字最小
for(int i=n-k+1;i<=n;i++) k2[++num] = i;
// 上一轮先后依据长为k/2的第1第2关键字排序,即依据长为k的第1关键字排序
// sa存储其结果,sa[r] = i,表示第i个后缀依据长为k的第1关键字排名为r
// 即第sa[r]-k个后缀依据长为k的第2关键字排名为r
for(int r=1;r<=n;r++)
if(sa[r] > k) // sa[r]之前要有长为k的第1关键字
k2[++num] = sa[r] - k;
// 依照第一关键词排序
// 上一轮先后依据长为k/2的第1第2关键字排序,即依据长为k的第1关键字排序
for(int i=1;i<=m;i++) cnt[i] = 0;
for(int i=1;i<=n;i++) cnt[k1[i]] ++;// k1[i]存储的是上一轮suf(i)离散化后的值
for(int i=2;i<=m;i++) cnt[i] += cnt[i-1];
// k2数组从后向前扫,即先依照第2关键字排序
for(int r=n;r;r--) sa[cnt[k1[k2[r]]] --] = k2[r], k2[r] = 0;
// 更新k1数组,并离散化
swap(k1, k2);
k1[sa[1]] = 1, num = 1;
// 如果sa[r]与sa[r-1]前2*k个字符相等
//则suf[sa[r]]与suf[sa[r-1]] 和 suf[sa[r]+k]与suf[sa[r-1]+k]的长为k的第1关键字分别相等
for(int r=2;r<=n;r++)
k1[sa[r]] = (k2[sa[r]] == k2[sa[r-1]] && k2[sa[r]+k] == k2[sa[r-1]+k]) ? num : ++ num;
// 如果所有后缀依照某个前缀排序互不相等,则顺序不会再改变了
if(num == n) break;
m = num;
}
}
rk与height数组的建立
rk数组使sa数组的反数组,故可快速求出:
for(int r=1;r<=n;r++) rk[sa[r]] = r;
height[i] = lcp(i,i-1),若暴力求解效率低下,这里有个技巧。
lcp(i,j)
性质:lcp(i,j) = min(lcp(i,k), lcp(k, j)) (i < k < j)
证明:
把lev(i),lev(k),lev(j)分别分成两大部分,前一部分为三者的最长公共前缀。
可以证明lev(i)与lev(j)的后一部分没有相同的前缀。若lev(i)与lev(j)的后一部分有相同的前缀,又lev(k)的后一部分大于lev(i),则lev(k)的后一部分大于lev(j),这与k < j矛盾。
故三者的前一部分,即三者的最长公共前缀长度为lcp(i,j)。
推广:lcp(i,j) = min( lcp(i,i+1), lcp(i+1, i+2) , ... , lcp(j-1, j)) (i < j)
h(i)
定义8:h()函数,有:h(i) = height[ rk[i] ]。
性质: h(i) >= h(i-1) - 1
证明:
不妨设sa[i] > sa[i-1]
①:rk[i-1] =1 或 h(i-1) = 0,h(i) >= 0 成立
②:h(i-1) >= 1,令sa[rk[i-1]-1] = x, 则h(i-1) = height[rk(i-1)] = lcp(rk[i-1], rk[x])。
由于suf(i-1)与suf(x)有相同的前缀,且rk(i-1) > rk(x), 则各去掉一个首字符后,suf(i) >= suf(x+1), 即rk[i] >= rk[x+1]。
而lcp(rk[i], rk[x+1]) = min(lcp(rk[x+1], rk[x+1]+1), ..., lcp(rk[i]-1), lcp(rk[i])), 可知lcp(rk[i], rk[x+1]) >= lcp(rk[i]-1), lcp(rk[i])。
此时lcp(i, x+1) = h(i-1) - 1;h(i) = lcp(rk[i], rk[i]-1),可知h(i) >= h(i-1) - 1 。
综上h(i) >= h(i-1) - 1。
故,考虑从前往后枚举每个后缀,如枚举到suf(i), 可知h(i) = height[rk[i]] = lcp(rk[i], rk[i] - 1 )。令sa[rk[i]-1] = x。而h(i) >= h(i-1) - 1,所以lcp(rk[i], rk[x]) >= h(i-1) - 1。故求h(i)时,可以从h(i-1)开始枚举比较suf[i]和suf[x]是否相同。
代码:
void get_height(void)
{
for(int r=1;r<=n;r++) rk[sa[r]] = r;
for(int i=1, h=0;i<=n;i++)
{
// height[1] = 0
if(rk[i] == 1) continue;
if(h) h --;
int x = sa[rk[i]-1];
// 枚举h位之后
while(i+h <= n && x+h <= n && s[i+h] == s[x+h]) h ++;
height[rk[i]] = h;
}
}