后缀数组学习笔记
我们用 \(sa[i]\) 代表排名为 \(i\) 的是哪个字符串,\(h[i]\) 为第 \(i\) 个字符串的排名。
普通的排序是 \(O(n^2\log n)\) 的,我们考虑先按照第一个和第二个字符为关键字排序。
我们先对第一个字符排序,然后就得到了几个串的初始,然后再按照第一个+第二个排序。
比如说:
.......
......^
.....^^
....^^^
...^^^^
..^^^^^
.^^^^^^
void SA()
{
for(int i=1;i<=n;i++) x[i]=s[i];
for(int i=1;i<=n;i++) ++c[x[i]]; // 转成数字,然后对应放进桶里
for(int i=2;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[x[i]]--]=i; // 按照第一个字母排序,正序倒序都没关系,因为没有第二关键字
for(int k=1;k<=n;k=k<<1){
int num=0;
// y[i]表示第二关键字排名为i的数,第一关键字的位置
// 第n-k+1到第n位是没有第二关键字的,所以排名在最前面
// 这里y从前往后第二关键字是不断增大的
for(int i=n-k+1;i<=n;++i) y[++num]=i; // 后面的第二关键字最小
for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
// 能作为第二关键字的,因为我们的第二关键字的下标是>k的,然后排名从小到大
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) c[x[i]]++; // 第一关键字加上去
for(int i=2;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) { sa[c[x[y[i]]]--]=y[i]; y[i]=0; }
// 和开头一样,我们发现开头就相当于把下标当作了第二关键字
swap(x,y);
num=1; x[sa[1]]=1;
for(int i=2;i<=n;i++){
if(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=num;
// 如果一样那么排名也一样
else x[sa[i]]=++num; // 排名为i大于排名为i-1
}
if(num==n) break;
m=num;
}
for(int i=1;i<=n;i++)
printf("%d ",sa[i]);
printf("\n");
}
妙啊!
LCP
貌似这个很重要,学一下。
\(lcp(i,j)\) 为 \(suff(sa[i])\) 和 \(suff(sa[j])\) 的最长公共前缀。
显而易见 \(lcp(i,j)=lcp(j,i)\),\(lcp(i,i)=n-sa[i]+1\)。
1. \(lcp(i,k)=\min(lcp(i,j),lcp(j,k))\)
设 \(p=\min(lcp(i,j),lcp(j,k))\)。
则 \(lcp(i,j)\),\(lcp(j,k) \ge p\)。
则 \(i,j,k\)前\(p\)个字符相等。
也就是 \(lcp(i,k) \ge p\)。
设 \(lcp(i,k)=q>p\)。
则 \(q \ge p+1\)。
那么 $i,k $前 \(q\) 个字符相等,矛盾。
所以 \(lcp(i,k)=p\)。
\(lcp(i,k)=\min(lcp(i,i+1),lcp(i+1,i+2),\dots)\)
显然。
设 \(height[i]\) 为 \(lcp(i,i-1)\),那么 \(height[1]=0\),\(lcp(i,k)=\min(height[j])\) \((i+1 \le j \le k)\)。
设 \(h[i]=height[rk[i]]\),\(h[sa[i]]=height[i]\)。
\(h\) 用中文说就是第 \(i\) 个字符串和比它字典序小字符串的 \(LCP\)。
\(height\) 就是排名。
要证明 \(h[i] \ge h[i-1] - 1\)。
设 \(i\) 为第 \(i\) 个字符串,\(j\) 为第 \(i-1\) 个字符串,\(k\) 为排名比 \(j\) 小的第一个字符串。
所以 \(lcp(j,k)=height[rk[i-1]]\)。
然后讨论第 \(k+1\) 个字符串和 \(i\) 的关系。
分类讨论:
-
\(k\) 和 \(j\)的首字符不同,那么 \(height[rk[i-1]]=0\),所以 \(height[rk[i]] \ge height[rk[i-1]] - 1\),也就是 \(h[i] \ge h[i-1] - 1\)。
-
第二种情况,\(k\) 和 \(j\) 的首字符相同,那么由于第 \(k+1\) 个字符串就是第 \(k\) 个字符串去掉首字符得到的,第 \(i\) 个字符串也是第 \(j\) 个字符串去掉首字符得到的。因为 \(k < j\),所以 \(k+1 < i\)。那么 \(k+1\) 就是为排名比 \(i\) 小的第一个字符串。所以第 \(k+1\) 个字符串和第 \(i\) 个字符串的最长公共前缀就是 \(height[rk[i-1]] - 1\)。(感性理解,要利用后缀这一性质)
到此为止,第二种情况的证明还没有完,我们可以试想一下,对于比第 \(i\) 个字符串的排名更靠前的那些字符串,\(sa[rank[i]-1]\) 与 \(i\) 的 \(LCP\) 最长。但是我们前面求得,有一个排在 \(i\) 前面的字符串 \(k+1\),\(LCP(rk[i],rk[k+1])=height[rk[i-1]] - 1\);
又因为 \(height[rk[i]]=LCP(i,i-1) \ge LCP(i,k+1)\)
所以 \(height[rk[i]] \ge height[rk[i-1]] - 1\),也即 \(h[i] \ge h[i-1] - 1\)。
for1(i,1,n) rk[sa[i]]=i;
int hi=0;
for1(i,1,n){
if(rk[i]==1){
hi=0;
continue;
}
if(hi) hi--;
int j=sa[rk[i]-1];
while(i+k<=n && j+k<=n && s[i+k]==s[j+k]) k++;
height[rk[i]]=k;
}
代码比较好理解。
多个串间的最长公共子串。
建出 \(SA\),利用双指针,保证区间内有 \(n\) 种颜色然后 \(st\) 表求 \(height\)。
检查字符串是否出现。
两个串扔到 \(SA\) 里,然后找到原串,然后向四边找。
不同子串个数 P2408。
建 \(SA\),然后,就是给答案加上 \(len[i] - height[i]\)。
所有不同子串的总长度。
一样,只不过变成了等差数列求和。
字典序第 \(k\) 大子串。
通过计算不同子串个数,我们从 '\(a\)'~'\(z\)' 枚举,可以用 \(SA\) 吧。
最小表示法。
将 \(s+s\) 插进去,然后就是路径长度为 \(s\) 的子串,然后贪心走,可以用 \(SA\)。