后缀数组
一个字符串处理的神器,并且代码简短精悍
但是自我感觉极其难懂,二十行代码研究数晚,综合多位大佬博客才搞懂
下文代码注释中的字母被和谐了。。请选中查看
基数排序
后缀数组的重要一部分,必须彻底理解,不然就会卡壳!!!
想法也不难,就是从最低位到最高位
(
L
S
D
)
(LSD)
(LSD)或最高到最低
(
M
S
D
)
(MSD)
(MSD)按位脱离,不通过元素比较,而根据“收集”来确定元素大小关系
举例看这里
void RadixSort(){//LSD
for(int D=1;Max;D*=10,Max/=B){
for(int i=0;i<B;i++) cnt[i]=0;
for(int i=1;i<=n;i++) cnt[a[i]/D%B]++;
for(int i=0;i<B;i++) cnt[i]+=cnt[i-1];
//作前缀和,便于定位到前面有几个比我小的,确定排名(位置)
for(int i=n;i;i--) tp[cnt[a[i]/D%B]--]=a[i];//临时数组存放
for(int i=1;i<=n;i++) a[i]=tp[i];
}
}
复杂度 O ( N ∗ D ( 位 数 ) ∗ k ( 小 常 数 ) ) O(N*D(位数)*k(小常数)) O(N∗D(位数)∗k(小常数)),能使后缀数组的构造少掉一个基于元素比较排序的 l o g log log,这个下文就知道了
正题
-
首先,定义一坨变量:
(接下来以后缀的起始位置作为后缀的位置,第 i i i号后缀表示 s [ i s[i s[i~~ N ] N] N])
S a [ i ] : 排 名 为 i 的 后 缀 在 串 中 的 位 置 Sa[i]:排名为i的后缀在串中的位置 Sa[i]:排名为i的后缀在串中的位置
R a k [ i ] : 第 i 号 后 缀 的 排 名 Rak[i]:第i号后缀的排名 Rak[i]:第i号后缀的排名
t p [ i ] : 第 二 关 键 字 排 名 为 i 的 位 置 , 类 似 S a [ i ] ( 下 面 会 解 释 ) tp[i]:第二关键字排名为i的位置,类似Sa[i](下面会解释) tp[i]:第二关键字排名为i的位置,类似Sa[i](下面会解释)
H e i g h t [ i ] : 排 名 为 i 与 i − 1 的 后 缀 的 最 长 公 共 前 缀 Height[i]:排名为i与i-1的后缀的最长公共前缀 Height[i]:排名为i与i−1的后缀的最长公共前缀通俗地说,Sa[i]表示第i是谁,Rak[i]表示i是第几,两者是互逆的
- Sa[Rak[i]]=Rak[Sa[i]]=i
-
核心想法:倍增(本蒟蒻太弱了,不会O(N)的DC3
虽然好像一般不太需要)假设已经求出每个后缀往后 2 k 2^k 2k的排名,然后考虑往后扩展 2 k 2^k 2k到 2 k + 1 2^{k+1} 2k+1
那么显然我们可以看成是双关键字排序:- S [ i ] = { A ( 前 2 k 的 排 名 , 已 求 ) , B ( 后 2 k 的 排 名 ) } S[i]=\{A(前2^k的排名,已求),B(后2^k的排名)\} S[i]={A(前2k的排名,已求),B(后2k的排名)}
而后面可以看到,由于后缀的特殊关系,我们可以线性求得B,在利用基数排序,我们就可以 O ( N ∗ l o g N ) O(N*log_N) O(N∗logN)完成后缀数组的构造(Height先扔一边,最后再搞)
-
代码细节解释
等
等
等
等
先再看一下上面数组定义,理解记忆,不然绝对蒙圈!
磨刀不误砍柴工!!!
- 首先对所有后缀以开头字母为关键字排序
M=127;for(int i=1;i<=N;i++) Rak[i]=s[i],tp[i]=i;//M为基数排序的上界(优化)
RadixSort();
- 接下来是倍增板块
for(int w=1,p=0;p<N;M=p,w<<=1){
//w表示当前已对w位排序,接下来要给2w位排序
//p表示离散后的Rak数,若排名各不相同即可停止
}
- 核心:每个后缀的第二关键字取得位置(构造
t
p
[
]
tp[]
tp[]):
看图:
显然,黄色部分是相同的,那么就可以O(1)求得 t p [ i ] tp[i] tp[i]了
p=0;//这里p仅仅是个计数器
for(int i=1;i<=w;i++) tp[++p]=N-w+i;//对于后w个后缀是没有第二关键字的,我们把它塞前面
for(int i=1;i<=N;i++) if(Sa[i]>w) tp[++p]=Sa[i]-w;
//基数排序,按第一关键字的顺序
//原理就是上图,即第Sa[i]-w号后缀的第二关键字的排名为i
- 然后就很简单了,基数排序后,通过 S a [ ] Sa[] Sa[]与 R a k [ ] Rak[] Rak[]的互逆运算,对 R a k [ ] Rak[] Rak[]就行构造与离散(此时很有可能会出现相同名次,但最后必定不同,毕竟后缀长度都不同)
RadixSort(),memcpy(tp,Rak,sizeof tp),Rak[Sa[1]]=p=1;//现在tp没用了,我们把Rak存过来,用于离散
for(int i=2;i<=N;i++)
Rak[Sa[i]]=(tp[Sa[i]]==tp[Sa[i-1]]&&tp[Sa[i]+w]==tp[Sa[i-1]+w])?p:++p;//简单的双关键字离散
这里有个小问题: S a [ i ] + w 不 会 爆 出 N 吗 ? Sa[i]+w不会爆出N吗? Sa[i]+w不会爆出N吗?
我自己WW了一下,应该不会超过N+1
首 先 , 要 比 较 S a [ i − 1 ] + w 与 S a [ i ] + w , 必 定 满 足 t p [ S a [ i ] ] = = t p [ S a [ i − 1 ] ] 首先,要比较Sa[i-1]+w与Sa[i]+w,必定满足tp[Sa[i]]==tp[Sa[i-1]] 首先,要比较Sa[i−1]+w与Sa[i]+w,必定满足tp[Sa[i]]==tp[Sa[i−1]]
那 么 既 然 有 t p [ S a [ i ] ] = = t p [ S a [ i − 1 ] ] , 显 然 他 们 往 后 w 个 得 到 的 字 符 串 都 得 真 实 存 在 那么既然有tp[Sa[i]]==tp[Sa[i-1]],显然他们往后w个得到的字符串都得真实存在 那么既然有tp[Sa[i]]==tp[Sa[i−1]],显然他们往后w个得到的字符串都得真实存在
所 以 最 坏 情 况 下 往 后 延 伸 w 个 到 达 串 的 末 尾 , 即 为 N + 1 所以最坏情况下往后延伸w个到达串的末尾,即为N+1 所以最坏情况下往后延伸w个到达串的末尾,即为N+1
所以可以加一道判段或在面对多组数据时记得清空数组到N+1!!!
- 再来看一眼基数排序 ~~
实际上没什么好看的。。
就是以 R a k [ t p [ i ] ] Rak[tp[i]] Rak[tp[i]]为关键字排一排就好了(和上面没什么区别)void RadixSort(){ for(int i=0;i<=M;i++) cnt[i]=0; for(int i=1;i<=N;i++) cnt[Rak[tp[i]]]++; for(int i=1;i<=M;i++) cnt[i]+=cnt[i-1]; for(int i=N;i;i--) Sa[cnt[Rak[tp[i]]]--]=tp[i]; }
- 完整过程代码
void RadixSort(){
for(int i=0;i<=M;i++) cnt[i]=0;
for(int i=1;i<=N;i++) cnt[Rak[tp[i]]]++;
for(int i=1;i<=M;i++) cnt[i]+=cnt[i-1];
for(int i=N;i;i--) Sa[cnt[Rak[tp[i]]]--]=tp[i];
}
void SuffixSort(){
M=127;for(int i=1;i<=N;i++) Rak[i]=s[i],tp[i]=i;//M为基数排序的上界(优化)
RadixSort();
for(int w=1,p=0;p<N;M=p,w<<=1){
//w表示当前已对w位排序,接下来要给2w位排序
//p表示离散后的Rak数,若排名各不相同即可停止
p=0;//这里p仅仅是个计数器
for(int i=1;i<=w;i++) tp[++p]=N-w+i;//对于后w个后缀是没有第二关键字的,我们把它塞前面
for(int i=1;i<=N;i++) if(Sa[i]>w) tp[++p]=Sa[i]-w;
//基数排序,按第一关键字的顺序
//原理就是上图,即第Sa[i]-w号后缀的第二关键字的排名为i
RadixSort(),memcpy(tp,Rak,sizeof tp),Rak[Sa[1]]=p=1;//现在tp没用了,我们把Rak存过来,用于离散
for(int i=2;i<=N;i++)
Rak[Sa[i]]=(tp[Sa[i]]==tp[Sa[i-1]]&&tp[Sa[i]+w]==tp[Sa[i-1]+w])?p:++p;//简单的双关键字离散
}
}
Height
这个才是后缀数组的真正神器!!!
H e i g h t [ i ] : 第 i 名 后 缀 与 第 i − 1 名 后 缀 的 最 长 公 共 前 缀 Height[i]:第i名后缀与第i-1名后缀的最长公共前缀 Height[i]:第i名后缀与第i−1名后缀的最长公共前缀
(有点绕)
H [ i ] : 第 i 号 后 缀 与 它 前 一 名 的 后 缀 的 最 长 公 共 前 缀 ( H e i g h t [ R a k [ i ] ] ) H[i]:第i号后缀与它前一名的后缀的最长公共前缀(Height[Rak[i]]) H[i]:第i号后缀与它前一名的后缀的最长公共前缀(Height[Rak[i]])
为了线性构造,要用到一个神奇的性质: H [ i ] > = H [ i − 1 ] + 1 H[i]>=H[i-1]+1 H[i]>=H[i−1]+1
设 第 k 号 后 缀 是 第 i − 1 号 后 缀 前 一 名 的 后 缀 , 它 们 的 最 长 公 共 前 缀 是 H [ i − 1 ] 设第k号后缀是第i-1号后缀前一名的后缀,它们的最长公共前缀是H[i-1] 设第k号后缀是第i−1号后缀前一名的后缀,它们的最长公共前缀是H[i−1]
如 果 H [ i − 1 ] < = 1 , 则 原 命 题 显 然 成 立 如果H[i-1]<=1,则原命题显然成立 如果H[i−1]<=1,则原命题显然成立
否 则 第 k + 1 号 后 缀 将 排 在 第 i 号 后 缀 的 前 面 , 并 且 第 k + 1 号 后 缀 与 第 i 号 后 缀 的 最 长 公 共 前 缀 至 少 是 H [ i − 1 ] − 1 ( 两 个 后 缀 各 去 掉 一 个 首 字 母 , 接 下 来 的 H [ i − 1 ] − 1 个 必 定 相 同 ) 否则第k+1号后缀将排在第i号后缀的前面,并且第k+1号后缀与第i号后缀的最长公共前缀至少是H[i-1]-1(两个后缀各去掉一个首字母,接下来的H[i-1]-1个必定相同) 否则第k+1号后缀将排在第i号后缀的前面,并且第k+1号后缀与第i号后缀的最长公共前缀至少是H[i−1]−1(两个后缀各去掉一个首字母,接下来的H[i−1]−1个必定相同)
所以我们就可以按照 R a k [ 1 Rak[1 Rak[1~~ N ] N] N]的顺序O(N)计算了
void GetHeight(){
for(int i=1,j,k=0;i<=N;i++)if(Rak[i]>1){
j=Sa[Rak[i]-1],k-=(bool)k;
while(i+k<=N&&j+k<=N&&s[i+k]==s[j+k]) k++;
Height[Rak[i]]=k;
}
}
神器在手,天下我有,接下来就可以做很多套路了
-
本质不同的字串个数
∑ N − S a [ i ] + 1 − H e i g h t [ i ] \sum N-Sa[i]+1-Height[i] ∑N−Sa[i]+1−Height[i]
第 i i i名后缀的长度减去与前面重复的长度 -
两个后缀的最长公共前缀
M i n ( H e i g h t [ R a k [ x ] + 1 Min(Height[Rak[x]+1 Min(Height[Rak[x]+1~~ R a k [ y ] ] ) Rak[y]]) Rak[y]]),RMQ预处理,O(1)询问
-
可重叠的最长重复字串
M a x ( H e i g h t [ i ] ) Max(Height[i]) Max(Height[i])
-
不可重叠的最长重复字串
先二分答案,把题目变成判定性问题:判断是否存在两个长度为 K K K的子串是相同的且不重叠
先不考虑重叠,则重复子串的长度要大于等于 k k k,就是一个区间内 H e i g h t [ i ] > = K Height[i]>=K Height[i]>=K
所以我们对 H e i g h t Height Height分组,满足每一组的 H e i g h t [ i ] > = K Height[i]>=K Height[i]>=K
再来考虑重叠:
我们知道了一个区间的 H e i g h t [ i ] > = K Height[i]>=K Height[i]>=K,那么如果存在两个后缀距离大于 K K K,那么可以肯定存在两个长度为 K K K的子串是相同的,且不重叠