文章目录

  • 前言
  • 解析
    • 后缀排序
      • 优化1:基数排序
      • 优化2:简化第一次排序
      • 优化3:提前break
      • 完整代码
    • LCP与height

所谓后缀数组,就是存储后缀的数组

(逃)

前言

为什么一个算法,如此难以理解却依然是成为一个成熟OIer不可回避的必修课?
足以可见后缀家族功能的强大

首先,由于其本身的性质,后缀数组对字典序相关的问题十分擅长
同时,由于 heightheightheight 数组的众多优秀性质,它在处理公共串问题和 LCP 问题上也十分强大
(我目前SA的题加起来也没做上十道,所以这样的“总结”请选择性阅读)

解析

后缀排序

P3809 【模板】后缀排序

给出一个字符串,把所有后缀按照字典序排序
n≤106n\le10^6n≤106

考虑倍增
一开始子串长度为 111,每个位置的排名 rkirk_irki​ 就是自己位置的字符
然后在已知长度为 www 的所有子串的排名的情况下,以 rki+wrk_{i+w}rki+w​ 为第二关键字,rkirk_irki​ 为第一关键字排序,可以得到长度为 2w2w2w 的所有子串的排名(空串的排名视为负无穷)
每次用 sort 的话,时间复杂度 O(nlog2n)O(nlog^2n^)O(nlog2n)

优化1:基数排序

注意到这里的排序是关于大小的排序,且值域(排名)只有 O(n)O(n)O(n)
所以我们可以使用基数排序代替 sort,时间复杂度变成 O(nlogn)O(nlogn)O(nlogn)

注意! 基数排序重新排列的循环必须倒序枚举,这样才能保证排序的稳定性

memset(cnt,0,sizeof(cnt));
memcpy(oldrk,rk,sizeof(rk));
for(int i=1;i<=n;i++) ++cnt[rk[id[i]]];
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[id[i]]]--]=id[i];
p=0;
for(int i=1;i<=n;i++){if(oldrk[sa[i]]==oldrk[sa[i-1]]&&oldrk[sa[i]+w]==oldrk[sa[i-1]+w]) rk[sa[i]]=p;else rk[sa[i]]=++p;
}
m=p;

优化2:简化第一次排序

第一次是关于rkiwrk_{i_w}rkiw​​ 排序
并不需要基数排序,只需要:

p=0;
for(int i=n;i>n-w;i--) id[++p]=i;
for(int i=1;i<=n;i++){if(sa[i]>w) id[++p]=sa[i]-w;
}

即可

优化3:提前break

玄学优化
大概就是,不必真的倍增到总长度,只需要让所有字符串的排名互相分开即可
这东西在全是 a 这样的串中可以说是等于没有,但在不少时候优化巨大(比如本题 2.2s→0.8s2.2s\to 0.8s2.2s→0.8s)

完整代码

saisa_isai​:排名为 iii 的后缀的编号
rkirk_irki​:后缀 iii 的排名

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define debug(...) fprintf(stderr,__VA_ARGS__)
const int N=1e6+100;
inline ll read(){ll x(0),f(1);char c=getchar();while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}while(isdigit(c)){x=(x<<1)+(x<<3)+c-'0';c=getchar();}return x*f;
}
int n,m,k;
char s[N];
int rk[N<<1],oldrk[N<<1],id[N],sa[N],cnt[N],p;
void write(int x){if(x>9) write(x/10);putchar('0'+x%10);return;
}
signed main() {#ifndef ONLINE_JUDGE//freopen("a.in","r",stdin);//freopen("a.out","w",stdout);
#endifscanf(" %s",s+1);n=strlen(s+1);m=122;for(int i=1;i<=n;i++) cnt[rk[i]=s[i]]++;for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;for(int w=1;;w<<=1){p=0;for(int i=n;i>n-w;i--) id[++p]=i;for(int i=1;i<=n;i++){if(sa[i]>w) id[++p]=sa[i]-w;}memset(cnt,0,sizeof(cnt));memcpy(oldrk,rk,sizeof(rk));for(int i=1;i<=n;i++) ++cnt[rk[id[i]]];for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];for(int i=n;i>=1;i--) sa[cnt[rk[id[i]]]--]=id[i];p=0;for(int i=1;i<=n;i++){if(oldrk[sa[i]]==oldrk[sa[i-1]]&&oldrk[sa[i]+w]==oldrk[sa[i-1]+w]) rk[sa[i]]=p;else rk[sa[i]]=++p;}m=p;if(m==n) break;//优化3}for(int i=1;i<=n;i++) write(sa[i]),putchar(' ');return 0;
}
/**/

LCP与height

定义:

height(i)height(i)height(i) 表示后缀 saisa_isai​ 和后缀 sai−1sa_{i-1}sai−1​ 的最长公共前缀(lcp(sai,sai−1)lcp(sa_i,sa_{i-1})lcp(sai​,sai−1​))。特别的,lcp(1)=0lcp(1)=0lcp(1)=0

感性理解来说,把所有后缀按照字典序排序后,height(i)height(i)height(i) 就是相邻两个后缀的相同部分的长度。

引理1:lcp(i,j)=min⁡(lcpi,k,lcpk,j)lcp(i,j)=\min (lcp_{i,k},lcp_{k,j})lcp(i,j)=min(lcpi,k​,lcpk,j​),对于任意的 i≤k≤ji\le k\le ji≤k≤j 均成立.

证明:
首先,min⁡(lcpi,k,lcpk,j)\min (lcp_{i,k},lcp_{k,j})min(lcpi,k​,lcpk,j​) 是 kkk 与 i,ji,ji,j 共同的公共前缀,所以也必然是 i,ji,ji,j 的公共前缀,lcp(i,j)≥min⁡(lcpi,k,lcpk,j)lcp(i,j)\ge\min (lcp_{i,k},lcp_{k,j})lcp(i,j)≥min(lcpi,k​,lcpk,j​)。
同时,由于字典序单调的性质,iii 变到 kkk 变化的前缀在 kkk 变化到 jjj 时必然不可能再变回来,否则 jjj 的字典序就比 kkk 小了,所以有 lcp(i,j)≤min⁡(lcpi,k,lcpk,j)lcp(i,j)\le\min (lcp_{i,k},lcp_{k,j})lcp(i,j)≤min(lcpi,k​,lcpk,j​)。
综上,lcp(i,j)=min⁡(lcpi,k,lcpk,j)lcp(i,j)=\min (lcp_{i,k},lcp_{k,j})lcp(i,j)=min(lcpi,k​,lcpk,j​),证毕。

引理2:heightrki≥heightrki−1−1height_{rk_i}\ge height_{rk_{i-1}}-1heightrki​​≥heightrki−1​​−1

证明:
rki−1≤1rk_{i-1}\le1rki−1​≤1 时,显然成立
rki−1>1rk_{i-1}>1rki−1​>1 时,设 rki−1−1=krk_{i-1}-1=krki−1​−1=k(kkk 就是 i−1i-1i−1 按字典序排序后的前一个),那么:
若 i−1i-1i−1 和 kkk 的首字母不同, hi−1=0h_{i-1}=0hi−1​=0 ,显然成立
若 i−1i-1i−1 和 kkk 的首字母相同,那么考虑字符串 k+1k+1k+1,由于k 去掉首字符变成 k+1,i-1 去掉首字母变成 i,所以 k+1k+1k+1 也一定在 iii 的前面,同时 lcp(k+1,i)=lcp(k,i−1)−1=heightrki−1−1lcp(k+1,i)=lcp(k,i-1)-1=height_{rk{i-1}}-1lcp(k+1,i)=lcp(k,i−1)−1=heightrki−1​−1,由引理1,有 lcp(k+1,i)=min⁡(lcp(k+1,rki−1),lcp(i−1,i))lcp(k+1,i)=\min (lcp(k+1,rk_i-1),lcp(i-1,i))lcp(k+1,i)=min(lcp(k+1,rki​−1),lcp(i−1,i)),故 lcp(i−1,i)≥heightrki−1−1lcp(i-1,i)\ge height_{rk{i-1}}-1lcp(i−1,i)≥heightrki−1​−1,即 heightrki≥heightrki−1−1height_{rk_i}\ge height_{rk_{i-1}}-1heightrki​​≥heightrki−1​​−1
得证。
得出这个性质后,线性求 heightheightheight 的代码就不难写出了:

for(int i=1,k=0;i<=n;i++){if(k) --k;while(s[i+k]==s[sa[rk[i]-1]+k]) ++k;ht[rk[i]]=k;
}

Thanks for reading!

模板:后缀数组(SA)相关推荐

  1. F - Anti-Rhyme Pairs(rmq算法模板)(后缀数组算法模板)

    点击打开链接 题目大意:通常押韵的两个词以相同的字符结尾.我们运用这个特性来规定反押韵的概念.反押韵是一对拥有近似开头的单词.一对单词的反押韵的复杂度被定义为两者都以之开头且最长的字符串S的长度.因此 ...

  2. [SDOI2016] 生成魔咒(后缀数组SA + st表 + set)动态不同子串个数

    problem luogu-P4070 魔咒串由许多魔咒字符组成,魔咒字符可以用数字表示.例如可以将魔咒字符 1,21,21,2 拼凑起来形成一个魔咒串 [1,2][1,2][1,2]. 一个魔咒串 ...

  3. 后缀数组(SA)备忘

    两个让我真正理解代码的资料: 2009集训队论文 网上的经典应用都是从里面抄的,还把解释给去掉了...真事屑 这篇博客 代码注释特别好 桶排换成快排的代码,便于理解算法思想 ,这里面要减去k的原因是 ...

  4. 洛谷P2463 [SDOI2008]Sandy的卡片(后缀数组SA + 差分 + 二分答案)

    题目链接:https://www.luogu.org/problem/P2463 [题意] 求出N个串中都出现的相同子串的最长长度,相同子串的定义如题:所有元素加上一个数变成另一个,则这两个串相同,可 ...

  5. 后缀数组(未完待续)

    后缀数组 简介 后缀数组(Suffix Array, SA)是一种在字符串问题中很实用的工具,其主要作用是求多模板匹配和最长公共前缀(LCP).与 AC自动机 预先处理模板串不同,后缀数组在进行多模板 ...

  6. 求两个字符串的LCS(最长公共子串)后缀数组

    题意: 给两个字符串,求出它们的最长公共子串的长度. 比如 yeshowmuchiloveyoumydearmotherreallyicannotbelieveit yeaphowmuchilovey ...

  7. 字符串-后缀树和后缀数组详解

    文章目录 后缀树 后缀数组 概念 sa[] rk[] height[] 例题 HDU-1403最长公共子串 洛谷P2408 不同子串个数 HDU-5769Substring 后缀树 建议先了解一下字典 ...

  8. 后缀数组 + Hash + 二分 or Hash + 二分 + 双指针 求 LCP ---- 2017icpc 青岛 J Suffix (假题!!)

    题目链接 题目大意: 就是给你n个串每个串取一个后缀,要求把串拼起来要求字典序最小!! sum_length_of_n≤5e5sum\_length\_of\_n\leq 5e5sum_length_ ...

  9. 1402 后缀数组 (hash+二分)

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

  10. 后缀数组总结(转载)

    后缀数组--处理字符串的有力工具 作者:罗穗骞 2009年1月 [摘要] 后缀数组是处理字符串的有力工具.后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间 ...

最新文章

  1. UIPopoverController在ARC环境下用法注意
  2. centos make 升级_CentOS更改yum源与更新系统
  3. 是vans_你知道VANS有哪些好鞋不贵系列?
  4. Linux 2.6内核配置说明(File systems文件系统)
  5. H264解码器源码(Android 1.6 版和QT都可以调用)
  6. 网站升级到新服务器,第一次折腾站点升级HTTPS 虽胜尤败
  7. LINQ 中的 select
  8. 软件工程进度条-第十五周
  9. php 空模块,tp5.1配置空模块,空方法
  10. asterisk概述和代码分析
  11. linux中fopen和open的区别,Linux下open与fopen的区别
  12. Sun公司开源游戏服务器Project Darkstar Server——(Sun game server , 简称 sgs)学习笔记(二):多人游戏...
  13. 太难了,斯坦福AI报告曝光!全球190万会AI,中国有5万
  14. 对话英特尔高级副总裁 Raja:软件将为硬件释放无限潜力
  15. 数据存储与访问之——初见SQLite数据库
  16. IEEE Access的模板的问题
  17. Windows窗口程序
  18. 《关键对话——何谓关键对话》读书笔记(一)
  19. 有信号但是无法连接到移动网络连接服务器,手机打电话显示无法连接到移动网络怎么回事?...
  20. 如何从购物数据中挖掘出啤酒与尿布的关联关系?

热门文章

  1. 不想再被鄙视?那就看进来! 一文搞懂 Python 2 字符编码
  2. 光通信调制方式MATLAB仿真,基于LED的紫外光通信调制方式研究
  3. java中string 和stringbuffer的区别_Java中的String,StringBuilder,StringBuffer三者的区别...
  4. 苹果手机怎么拍星空_手机拍星空,看这篇教程就够了!
  5. mysql root密码忘记2018_2018-03-28设置及修改mysql用户密码学习笔记
  6. 国内linux内核镜像仓库,国内较快的maven仓库镜像
  7. ros发布节点信息python_vscode开发ROS1(13)-python实现话题通信(msg)
  8. armv7的linux系统,CentOS 7(1611) for ARM(armhfp)发布
  9. gif android 点击 加载,android 加载显示gif图片的解决方案
  10. 吴恩达DeepLearningCourse4-卷积神经网络