目录

1.后缀数组sa[]的求法O(nlogn)

2.rank数组的求法O(n)

3.height数组的求法O(n+)


可以先看文章末的参考博客!!

字符串:aabaaaab,读入时下标从0开始

三个基本数组:

(1)sa[i]=j:第i名的后缀是[j,n)(i从1开始,j从0开始)

(2)rankk[i]=j:[i,n)的后缀排第j名(i从0开始,j从1开始)

(3)height[i]:排第i名的后缀和排第i-1名的后缀的最长公共前缀长度

1.后缀数组sa[]的求法O(nlogn)


倍增算法(基数排序) (还有一种比倍增算法更优的DC3算法,暂时不学)

数组说明:

y[i] = j: 相当于第二关键字的sa数组,表示从下标j开始选取的第二关键字排第i名,同时y[i]也记录了字符串的开始下标,注意每次都把上一次排序处理的多个字符看成一个整体

x[i] =j :相当于第一关键字的rankk,表示从下标i开始的第一关键字排名为j

wv[i] = j:排名为i的第二关键字对应的第一关键字排名为j

(求前缀和的)wss[i] = j: 统计排名[0,i]区间所有排名出现的次数,处理的时候倒序处理

sa[i] = j: 表示从下标j开始选定的制定长度的字符字典序排名为i

倍增算法求sa数组:


bool cmp(int *r, int a, int b, int l)
{return r[a] == r[b] && r[a + l] == r[b + l];
}
void get_sa(int *r, int *sa, int n, int m)
{int *x=wa, *y=wb;int p =0, i, j;for(i = 0; i < m; i++) wss[i] = 0;for(i = 0; i < n; i++) wss[ x[i]=r[i] ]++;for(i = 1; i <= m; i++) wss[i] += wss[i - 1];for(i = n - 1; i >= 0; i--) sa[--wss[x[i]]] = i;for(j = 1, p = 1; p < n; j *= 2, m = p){//对第二关键字排序for(p = 0, i = n - j; i < n; i++) // [n-j,n)没有内容y[p++] = i;for(i = 0; i < n; i++)if(sa[i] >= j) y[p++] = sa[i] - j;//对字符串排序for(i = 0; i < n; i++) wv[i] = x[y[i]];for(i = 0; i < m; i++) wss[i] = 0;for(i = 0; i < n; i++) wss[wv[i]]++;for(i = 1; i <= m; i++) wss[i] += wss[i - 1];for(i = n - 1; i >= 0; i--) sa[--wss[wv[i]]] = y[i];//相同的字符串排名相同swap(x,y);for(i = 1, p = 1, x[sa[0]] = 0; i < n; i++)x[sa[i]] = cmp(y, sa[i-1], sa[i], j) ? p - 1 : p++;}
}

dc3算法求sa数组:(存个板子)O(n),但是常数较大,又是也会超时

const int max = 3*1e6+5
#define F(x) ((x)/3+((x)%3==1?0:tb))
#define G(x) ((x)<tb?(x)*3+1:((x)-tb)*3+2)
int wa[maxn],wb[maxn],wv[maxn],ws[maxn];
int r[maxn],sa[maxn];
int c0(int *r,int a,int b)
{return r[a]==r[b]&&r[a+1]==r[b+1]&&r[a+2]==r[b+2];}
int c12(int k,int *r,int a,int b)
{if(k==2) return r[a]<r[b]||r[a]==r[b]&&c12(1,r,a+1,b+1);else return r[a]<r[b]||r[a]==r[b]&&wv[a+1]<wv[b+1];}
void sort(int *r,int *a,int *b,int n,int m)
{int i;for(i=0;i<n;i++) wv[i]=r[a[i]];for(i=0;i<m;i++) ws[i]=0;for(i=0;i<n;i++) ws[wv[i]]++;for(i=1;i<m;i++) ws[i]+=ws[i-1];for(i=n-1;i>=0;i--) b[--ws[wv[i]]]=a[i];return;
}
void dc3(int *r,int *sa,int n,int m)
{int i,j,*rn=r+n,*san=sa+n,ta=0,tb=(n+1)/3,tbc=0,p;r[n]=r[n+1]=0;for(i=0;i<n;i++) if(i%3!=0) wa[tbc++]=i;sort(r+2,wa,wb,tbc,m);sort(r+1,wb,wa,tbc,m);sort(r,wa,wb,tbc,m);for(p=1,rn[F(wb[0])]=0,i=1;i<tbc;i++)rn[F(wb[i])]=c0(r,wb[i-1],wb[i])?p-1:p++;if(p<tbc) dc3(rn,san,tbc,p);else for(i=0;i<tbc;i++) san[rn[i]]=i;for(i=0;i<tbc;i++) if(san[i]<tb) wb[ta++]=san[i]*3;if(n%3==1) wb[ta++]=n-1;sort(r,wb,wa,ta,m);for(i=0;i<tbc;i++) wv[wb[i]=G(san[i])]=i;for(i=0,j=0,p=0;i<ta && j<tbc;p++)sa[p]=c12(wb[j]%3,r,wa[i],wb[j])?wa[i++]:wb[j++];for(;i<ta;p++) sa[p]=wa[i++];for(;j<tbc;p++) sa[p]=wb[j++];return;
}

模拟一遍倍增算法的程序基本上就懂个大概了。


对于字符串aabaaaab,长度是8,但是传入的参数n应该是8+1=9,把字符换成对应的int值,下面的代码执行的结果是:

    for(i = 0; i < m; i++) wss[i] = 0;for(i = 0; i < n; i++) wss[ x[i]=r[i] ]++;
i 0 1 2 3 4 5 6 7 8
r[i] 97 97 98 97 97 97 97 98 0
x[i] 97 97 98 97 97 97 97 98 0
i 0 1 . . 96 97 98 99 . . . 255
wss[i] 1 0 0 0 0 6 2 0 0 0 0 0

    for(i = 1; i <= m; i++) wss[i] += wss[i - 1];

执行结果:求前缀和

i 0 1 2 . . 96 97 98 99 . . 255
wss[i] 1 1 1 1 1 1 7 9 9 9 9 9

    for(i = n - 1; i >= 0; i--) sa[--wss[x[i]]] = i;

执行结果:对单个字符进行初步排序,此时,对于相同的字符,先出现的排名高

i 0 1 2 3 4 5 6 7 8
下标为i的字符的排名 1 2 7 3 4 5 6 8 0
i 0 1 2 3 4 5 6 7 8
sa[i] 8 0 1 3 4 5 6 2 7

for循环内,执行倍增算法,先对第二关键字排序,所谓的第二关键字:当前字符(之后会是字符串参考第一张图)后面j个字符。第一次j=1,那就是每个字符串后面1个字符就是第二关键字。对于aabaaaab的第二次排序的第二关键就是a、b、a、a、a、a、b、0(没有第二关键字后面补0⃣️)

        for(p = 0, i = n - j; i < n; i++) // [n-j,n)没有内容y[p++] = i;for(i = 0; i < n; i++)if(sa[i] >= j) y[p++] = sa[i] - j;

执行结果:这里的y数组是利用了上一次求出的sa[],来获得第二关键字的大小关系,因为上一轮的字符串(已排好序)是这一轮的第一关键字,第一关键字已经有序,按照基数排序的思想,高位已经有序了,再考虑低位,优先排 排名高的高位对应的低位,所以i从0开始遍历,从排序高的第一关键字开始考虑它的第二关键字

i 0 1 2 3 4 5 6 7 8
y[i] 8 7 0 2 3 4 5 1 6

参考第二关键字验证正确性:

i 0 1 2 3 4 5 6 7 8 9
r[i] 97 97 98 97 97 97 97 98 0 0

接下来对整个字符串排序:

for(i = 0; i < n; i++) wv[i] = x[y[i]];

执行结果:

i 0 1 2 3 4 5 6 7 8
wv[i] 0 98 97 98 97 97 97 97 97

        for(i = 0; i < m; i++) wss[i] = 0;for(i = 0; i < n; i++) wss[wv[i]]++;for(i = 1; i <= m; i++) wss[i] += wss[i - 1];

执行结果:

i 0 1 2 . . 96 97 98 99 . . 255
wss[i] 1 1 1 1 1 1 7 9 9 9 9 9

        for(i = n - 1; i >= 0; i--) sa[--wss[wv[i]]] = y[i];

执行结果:参照字符串严重正确性,现在比较的是(每个后缀字符串的前两个字符)

i 0 1 2 3 4 5 6 7 8
sa[i] 8 0 3 4 5 1 6 7 2


x和y数组交换,此时因为可能有的字符串相同但是在对第一关键字排序后排名不同,要让相同的字符串的排名相同,用下面的代码实现:


bool cmp(int *r, int a, int b, int l)
{return r[a] == r[b] && r[a + l] == r[b + l];
}
swap(x,y);
for(i = 1, p = 1, x[sa[0]] = 0; i < n; i++)x[sa[i]] = cmp(y, sa[i-1], sa[i], j) ? p - 1 : p++;

执行结果:对比字符串可验证正确性,x[i]=j此时存的是:从下标j开始的前两个字符排名第i名,相当于rankk数组

i 0 1 2 3 4 5 6 7 8
x[i] 1 2 4 1 1 1 2 3 0

j=1*2,m=5

 //对第二关键字排序for(p = 0, i = n - j; i < n; i++) // [n-j,n)没有内容y[p++] = i;for(i = 0; i < n; i++)if(sa[i] >= j) y[p++] = sa[i] - j;

执行结果:

i 0 1 2 3 4 5 6 7 8
y[i] 7 8 6 1 2 3 4 5 0

举例说明此时y[]的作用(手绘图),在原字符串的基础上:注意在上一轮排完序后已经把每个后缀的前两个字符看成了一个整体

y[2] =6表示在下标6+j-1后面(跳过下标6和下标7,从下标8开始)选择j个字符,这j个字符在这一轮的第二关键字中排第2名

所以y数组相当于第二关键字的sa[]数组


通过rankk[sa[i]]=i得到排名第i的第二关键字对应的第一关键字的排名,wv[i]=m表示第二关键字排第i名,它的第一关键字排第m名

//排名为i的第二关键字对应的第一关键字的排名,x此时相当于rankk,y相当于第二关键字的safor(i = 0; i < n; i++) wv[i] = x[y[i]];

执行结果:

i 0 1 2 3 4 5 6 7 8
wv[i] 3 0 2 2 4 1 1 1 1

        for(i = 0; i < m; i++) wss[i] = 0;for(i = 0; i < n; i++) wss[wv[i]]++;

执行结果:

i 0 1 2 3 4 5 ...
wss[i] 1 4 2 1 1 0 0

        for(i = 1; i <= m; i++) wss[i] += wss[i - 1];

求前缀和: m = 5

i 1 2 3 4 5
wss[i] 5 7 8 9 9

y[i]相当于第二关键字的sa[i],第二关键字排第i名,它的第一关键字排第wv[i]名y[i]记录了这四个字符的开始下标

        for(i = n - 1; i >= 0; i--) sa[--wss[wv[i]]] = y[i];

执行结果:

i 0 1 2 3 4 5 6 7 8
sa[i] 8 3 4 5 0 6 1 7 2

再让相同的字符排序相同

        swap(x,y);for(i = 1, p = 1, x[sa[0]] = 0; i < n; i++)x[sa[i]] = cmp(y, sa[i-1], sa[i], j) ? p - 1 : p++;

至此,p=9 >8,说明给定的aabaaaab的8个后缀字符串已经排好序了,结束循环,最终求得sa数组,x数组相当于rankk数组

总结:搞懂下面这几个数组很重要!!我没有在网上找到有具体讲解下面的数组的博客!(◎_◎;)

r[]:存输出的字符串对应的int值

y[i] = j: 相当于第二关键字的sa数组,表示从下标j开始选取的第二关键字排第i名,同时y[i]也记录了字符串的开始下标,注意每次都把上一次排序处理的多个字符看成一个整体

x[i] =j :相当于第一关键字的rankk,表示从下标i开始的第一关键字排名为j

wv[i] = j:排名为i的第二关键字对应的第一关键字排名为j

(求前缀和的)wss[i] = j: 统计排名[0,i]区间所有排名出现的次数,处理的时候倒序处理

sa[i] = j: 表示从下标j开始选定的制定长度的字符字典序排名为i

在每一轮求得sa数组之后,x记录此轮的rankk值,并让相同的字符串排名相同,最终处理到排名值超过字符串长度结束。

最终求得的排名从1开始才有意义,字符串下标仍是从0开始的。

2.rank数组的求法O(n)


性质:rankk[sa[i]]=i, sa[rankk[i]]=i

    for(int i = 1; i <= n; i++) rankk[sa[i]] = i;

3.height数组的求法O(n+)


(文字来源https://blog.csdn.net/wangushang2/article/details/7827873)

height数组:定义height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀。那么对于j和k,不妨设rank[j]<rank[k],则有以下性质:

suffix(j)和suffix(k)的最长公共前缀为height[rank[j]+1],height[rank[j]+2],height[rank[j]+3],……,height[rank[k]]中的最小值。

例如,字符串为“aabaaaab”,求后缀“abaaaab”和后缀“aaab”的最长公共前缀,如图4所示:

那么应该如何高效的求出height值呢?

如果按height[2],height[3],……,height[n]的顺序计算,最坏情况下时间复杂度为O(n2)。这样做并没有利用字符串的性质。定义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)。

实现代码:

注意for中i的下标是从0开始的,以为字符串的下标是从0开始的,所以定存在rankk[0],否则就错了

小心后缀数组里的for循环左右边界!

void get_height(int n)
{int k = 0;for(int i = 0; i < n; i++){k ? k-- : 0;//根据性质height[rank[i]] ≥ (height[rank[i-1]] -1)int j = sa[rankk[i] - 1];//上一名的开始下标while(r[i + k] == r[j + k]) k++;height[rankk[i]] = k;}
}

【参考博客】:

https://www.cnblogs.com/shanchuan04/p/5324009.html

https://blog.csdn.net/wangushang2/article/details/7827873

https://blog.csdn.net/a1035719430/article/details/80217267

【算法笔记】图文结合彻底搞懂后缀数组相关推荐

  1. 五分钟搞懂后缀数组!

    为什么学后缀数组 后缀数组是一个比较强大的处理字符串的算法,是有关字符串的基础算法,所以必须掌握.  学会后缀自动机(SAM)就不用学后缀数组(SA)了?不,虽然SAM看起来更为强大和全面,但是有些S ...

  2. 五分钟搞懂后缀数组!后缀数组解析以及应用(附详解代码)

    为什么学后缀数组 后缀数组是一个比较强大的处理字符串的算法,是有关字符串的基础算法,所以必须掌握. 学会后缀自动机(SAM)就不用学后缀数组(SA)了?不,虽然SAM看起来更为强大和全面,但是有些SA ...

  3. 最短路算法的证明_彻底搞懂最短路算法

    只想说:温故而知新,可以为师矣.我大二的<数据结构>是由申老师讲的,那时候不怎么明白,估计太理论化了(ps:或许是因为我睡觉了):今天把老王的2011年课件又看了一遍,给大二的孩子们又讲了 ...

  4. 算法竞赛进阶指南:0x14:后缀数组

    原题链接 后缀数组 (SA) 是一种重要的数据结构,通常使用倍增或者 DC3 算法实现,这超出了我们的讨论范围. 在本题中,我们希望使用快排.Hash 与二分实现一个简单的 O(nlog^2n) 的后 ...

  5. 数据结构与算法—一文多图搞懂双链表

    目录 前言 双链表介绍 与单链表区别 结构的设计 具体方法的解析 初始化 增加 空表插入: 头插入: 尾插入: 编号插入: 删除 单节点删除: 头删除: 尾删除: 普通删除: 代码与测试 总结与感悟 ...

  6. 【数据结构与算法】一篇文章彻底搞懂二分查找(思路图解+代码优化)两种实现方式,递归与非递归

    1.二分查找是什么? 二分查找也称折半查找,是一种效率较高的查找方式.但是,二分查找要求线性表为顺序存储结构且表按关键字有序排列. 时间复杂度:O(log2n) 2.二分查找的思路分析 便于叙述,笔者 ...

  7. 10分钟带你彻底搞懂服务限流和服务降级

    文章目录 十分钟搞懂系列 服务限流 计数器法 滑动窗口法 漏桶算法 令牌桶算法 服务降级 十分钟搞懂系列 序号 标题 链接 1 10分钟带你彻底搞懂企业服务总线 https://blog.csdn.n ...

  8. 一文搞懂UART通信协议

    目录 1.UART简介 2.UART特性 3.UART协议帧 3.1.起始位 3.2.数据位 3.3.奇偶校验位 3.4.停止位 4.UART通信步骤 1.UART简介 UART(Universal ...

  9. 一文搞懂I2S通信总线

    目录 1.物理特性 2.常见的I2S数据格式 2.1.I2S Philips标准 2.2.左对齐(MSB)标准 2.3.右对齐(LSB)标准 之前我们讲过I2C通信总线,本篇博文将讲一讲I2C的变种: ...

  10. 使用倍增算法(Prefix Doubling)构造后缀数组

    如果采用对每个后缀排序的方法来生成后缀数组,即使采用快速排序,由于每次比较的对象是字符串(设输入字符串的长度为n),因此每一个比较操作的复杂度不再是常数,而是O(n),快速排序本身的平均情况下时间复杂 ...

最新文章

  1. 一个http的Post请求问题,unable to resolve host 我的域名:no address associated with hostnam...
  2. 2018/11/29 一个64位操作系统的设计与实现 02 (安装nasm)
  3. 如何完整迁移git仓库到另一个远程地址
  4. 浏览器HTTP缓存机制
  5. 人工智能助力资深内容营销人员
  6. 在绘制USB2.O设备接口差分线时,应注意以下几点要求:
  7. 编译安装Apache2.4.10
  8. php dns更新,php_mzdns: 站群,大量域名 通过 dns 服务商 api 批量添加 ip 工具。你懂的~ 基于 mzphp2 框架。...
  9. ini_set(display_errors,On);和error_reporting(E_ALL);
  10. 初中人教版电子课本app_电子课本|2020秋 部编人教版初中历史七年级上册教材电子课本(高清更新可打印)...
  11. 分析windows宿主机Ping不通linux虚拟机的其中一种情况
  12. AUTOSAR从入门到精通100讲(二十五)-AutoSar架构中的Methodology
  13. 都说程序员是吃青春饭!
  14. 洛谷P1238 走迷宫
  15. prototype中的$R函数的用法
  16. 5.Nginx 反向代理
  17. 软中断SOFTIRQ
  18. FFmpeg图片合成视频,实现放大平移
  19. Google账号登录后直接跳转百度首页,登陆不上
  20. Linux下dcm2niix使用

热门文章

  1. el-form-item 如何限制只能输入数字_中教云数字课程教材云平台操作答疑
  2. mysql免安装 配置_MySql免安装版配置方法
  3. 不支持给定路径的格式_【国考那些事】2021国/省考,申论答题格式,建议收藏!...
  4. java块语句_Java™ 教程(表达式、语句和块)
  5. BZOJ3108 [cqoi2013]图的逆变换
  6. code review平台Rietveld应用指南
  7. 操作系统就是虚拟机--主内又主外
  8. CamtasiaStudio如何导出视频上传优酷实现高清
  9. APICloud连接夜神模拟器--博客园老牛大讲堂
  10. OpenCV 使用 FLANN 库实现特征匹配