【后缀数组】学习笔记

看大佬A
看大佬B

后缀数组

一个字符串处理的神器,并且代码简短精悍
但是自我感觉极其难懂,二十行代码研究数晚,综合多位大佬博客才搞懂
下文代码注释中的字母被和谐了。。请选中查看


基数排序

后缀数组的重要一部分,必须彻底理解,不然就会卡壳!!!
想法也不难,就是从最低位到最高位 ( 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(ND()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]:iSa[i]
    H e i g h t [ i ] : 排 名 为 i 与 i − 1 的 后 缀 的 最 长 公 共 前 缀 Height[i]:排名为i与i-1的后缀的最长公共前缀 Height[i]:ii1

    通俗地说,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(NlogN)完成后缀数组的构造(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[]):
    看图: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]+wN

我自己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[i1]+wSa[i]+wtp[Sa[i]]==tp[Sa[i1]]
那 么 既 然 有 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[i1]]w
所 以 最 坏 情 况 下 往 后 延 伸 w 个 到 达 串 的 末 尾 , 即 为 N + 1 所以最坏情况下往后延伸w个到达串的末尾,即为N+1 wN+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]:ii1(有点绕)
H [ i ] : 第 i 号 后 缀 与 它 前 一 名 的 后 缀 的 最 长 公 共 前 缀 ( H e i g h t [ R a k [ i ] ] ) H[i]:第i号后缀与它前一名的后缀的最长公共前缀(Height[Rak[i]]) H[i]:iHeight[Rak[i]]

为了线性构造,要用到一个神奇的性质: H [ i ] &gt; = H [ i − 1 ] + 1 H[i]&gt;=H[i-1]+1 H[i]>=H[i1]+1

设 第 k 号 后 缀 是 第 i − 1 号 后 缀 前 一 名 的 后 缀 , 它 们 的 最 长 公 共 前 缀 是 H [ i − 1 ] 设第k号后缀是第i-1号后缀前一名的后缀,它们的最长公共前缀是H[i-1] ki1H[i1]
如 果 H [ i − 1 ] &lt; = 1 , 则 原 命 题 显 然 成 立 如果H[i-1]&lt;=1,则原命题显然成立 H[i1]<=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+1ik+1iH[i1]1H[i1]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] NSa[i]+1Height[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 ] &gt; = K Height[i]&gt;=K Height[i]>=K
    所以我们对 H e i g h t Height Height分组,满足每一组的 H e i g h t [ i ] &gt; = K Height[i]&gt;=K Height[i]>=K
    再来考虑重叠:
    我们知道了一个区间的 H e i g h t [ i ] &gt; = K Height[i]&gt;=K Height[i]>=K,那么如果存在两个后缀距离大于 K K K,那么可以肯定存在两个长度为 K K K的子串是相同的,且不重叠

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值