又是一个学了n遍还没学会的算法……

后缀数组是一种常用的处理字符串问题的数据结构,主要由 \(\mathrm{sa}\) 和 \(\mathrm{rank}\) 两个数组组成。以下给出一些定义:

\(\mathrm{str}\) 表示处理的字符串,长度为 \(\mathrm{len}\) 。(下标从\(0\)开始)

\([i,j)\)表示 \(\mathrm{str}\) 从\(i\)到\(j - 1\)的字串。

后缀\(i\)表示子串\([i,len)\),以字典序排序。

\(sa[i]\)表示排名为\(i\)的后缀的起始位置(即后缀\(sa[i]\)是第\(i\)名)

\(rank[i]\)表示后缀\(i\)的排名(从\(0\)开始)。显然\(rank[sa[i]]=i\)。

一、基数排序

先简单介绍一下后缀数组的前置技能:基数排序。

以对整数数组 \(\mathrm{arr}\) 排序为例。从低到高遍历每一个十进制位,对于每个位:

\(1.\) \(\mathrm{arr}\) 数组已经按照前\(i-1\)位排好序,(\(i=0\)时忽略这句),现在我们将把它变为按前\(i\)位排好序。脑补以下整数的比较方式,现在应该把第\(i\)位作为第一关键字,前\(i-1\)位作为第二关键字。

\(2.\)统计第\(i\)位为数字\(a\)的数的数量,存入\(count[a]\)。

\(3.\)对 \(\mathrm{count}\) 数组求前缀和,算出最后一个第\(i\)位为\(a\)的数在按照前\(i\)位排序后数组中的位置的下一个。这句表达比较鬼畜,看下面的例子。

比如,\(i\)位为\(0\)的有\(2\)个,为\(1\)的有\(1\)个,为\(2\)的有\(3\)个,第\(3\)步以后 \(\mathrm{count}\) 位\(\{2,3,6\}\),那么排序后\(arr[0]\)和\(arr[1]\)的第\(i\)位为\(0\),\(arr[2]\)的第\(i\)位为\(1\),\(arr[3]\)到\(arr[5]\)的第\(i\)位为\(2\)。

\(4.\)逆序遍历 \(\mathrm{arr}\) ,按照上一步中算出的第\(i\)位为\(a\)的数排序后的位置逆序填充临时数组。两个均逆序保证了对于第\(i\)位相同的数按照最初在 \(\mathrm{arr}\) 中的位置排序。

\(5.\)最后,把临时数组复制给 \(\mathrm{arr}\) ,此时 \(\mathrm{arr}\) 按照前\(i\)位有序。

int count[10];
for(int i = 1; i <= 10; i++, ra *= 10)
{
memset(count, 0, sizeof(count));
for (int j = 1; j <= n; j++)
++count[arr[j] / ra % 10];//step 2
for (int j = 1; j < 10; j++)
count[j] += count[j - 1];//step 3
for(int j = n - 1; j >= 0; j--)
buc[--count[arr[j] / ra % 10]] = arr[j];
memcpy(arr, buc, sizeof(int[n]));
}

二、倍增构造后缀数组

考虑我们现在有了对所有形如\([i,min(i+tmp,len))\)的子串排序的数组 \(\mathrm{sa}\) 和 \(\mathrm{rank}\) (对于相同的子串,它们的 \(\mathrm{rank}\) 值相同,在 \(\mathrm{sa}\) 中顺序任意),我们现在要构造对所有形如\([i,min(i+2tmp,len))\)的子串排序。最坏情况下,当\(2tmp\geq len\)时就得到了答案。

可以发现此时很类似于基数排序时排到某一位时的情况。此时,第一关键字是\([i,i+tmp)\),第二关键字是\([i+tmp, i+2tmp)\)。并且,现在已经按照第二关键字排好序了。

于是我们先看看此处的基数排序。其中 \(\mathrm{kind}\) 是 \(\mathrm{rank}\) 中不同值的种数(由于 \(\mathrm{rank}\) 从\(0\)开始,也可以看成 \(\mathrm{rank}\) 中最大值加\(1\)),\(tp[i]\)表示哪个串的第二关键字在所有第二关键字中的排名是\(i\)。

void radix_sort()
{
static int count[N];
memset(count, 0, sizeof(int[kind]);
for (int i = 0; i < len; i++)
count[rank[tp[i]]]++;
for (int i = 1; i < kind; i++)
count[i] += count[i - 1];
for (int i = len - 1; i >= 0; i--)
sa[--count[rank[tp[i]]]] = tp[i];
}

然后我们来构造 \(\mathrm{tp}\) 数组。首先,对于起点在\([len-tmp,len)\)中的串,它们的第二关键字都是空串,排名是最低的。所以它们应当在 \(\mathrm{tp}\) 的开头:

for (int i = len - tmp; i < len; i++)
tp[cnt++] = i;

然后,按照 \(\mathrm{sa}\) 加入剩下的串。注意只有起点在\(tmp\)及以后的串才能作为第二关键字。

for(int i=0;i<len;i++)
if(sa[i]>=tmp)
tp[cnt++]=sa[i]-tmp;

至此, \(\mathrm{tp}\) 数组构造完毕,可以进行基数排序。排序后,我们要按照新的 \(\mathrm{sa}\) 和旧的 \(\mathrm{rank}\) 构造新的 \(\mathrm{rank}\) 。首先,把旧的 \(\mathrm{rank}\) 进行拷贝:

memcpy(tp, rank, sizeof(int[n]));

记住,此后 \(\mathrm{tp}\) 就只是旧的 \(\mathrm{rank}\) 的一份拷贝了,没有更多实际意义。更新 \(\mathrm{rank}\) 的过程比较显然。

rank[sa[0]] = 0;
kind = 1;
for (int i = 1; i < len; i++)
{
if (tp[sa[i]] == tp[sa[i - 1]] &&
(sa[i] + tmp < len && sa[i - 1] + tmp < len) &&
(tp[sa[i] + tmp] == tp[sa[i - 1] + tmp]))
rank[sa[i]] = rank[sa[i - 1]];
else
rank[sa[i]] = kind++;
}

最后,如果\(kind=len\),即 \(\mathrm{rank}\) 已经两两不同,则说明已经得出了答案。

三、应用:构造 \(\mathrm{height}\) 数组

我不会,你开心不qwq

更新时间:2019年12月7日,距离这篇博客最初发表(2018年12月13日)已经过去了将近一年。(在此期间我的博客写作风格发生了一些变化)

博主肯定不是学了一年才学会了求 height ,只是又懒又咕……

\(\mathrm{height}\) 太长了,以下暂且缩写为 \(h\) 。

首先是定义:\(h_i\) 表示 \(\mathrm{sa}_i\) 和 \(\mathrm{sa}_{i-1}\) 的最长公共前缀(LCP)。注意这是两个 字典序排名 连续的后缀,而不是两个 位置 (这里的「位置」指的是在原字符串中的位置,下同)连续的后缀。

定理:\(h_{\mathrm{rank}_i}+1\geq h_{\mathrm{rank}_{i-1}}\)

翻译成人话就是:对于一个位置 \(i\) ,从 \(i\) 开始的后缀与排名在它前一位的后缀的 LCP (即 \(h_{\mathrm{rank}_i}\) ),一定不小于从 \(i-1\) 开始的后缀与排名在它前一位的后缀的 LCP (即 \(h_{\mathrm{rank}_{i-1}}\) )减去 \(1\) 。

为什么呢?如果 \(h_{\mathrm{rank}_{i-1}}=0\) 显然成立,所以我们只讨论 \(h_{\mathrm{rank}_{i-1}}>0\) 的情况。设 \(j\) 是排名在 \(i-1\) 前面的那个后缀(即 \(j=\mathrm{sa}_{\mathrm{rank}_{i-1}-1}\) ),那么根据定义,有 \([j,j+h_{\mathrm{rank}_{i-1}})=[i-1,i-1+h_{\mathrm{rank}_{i-1}})\) 。那么就有 \([j+1,j+h_{\mathrm{rank}_{i-1}})=[i,i-1+h_{\mathrm{rank}_{i-1}})\) 。也就是说 \(i\) 和 \(j+1\) 有长为 \(h_{\mathrm{rank}_{i-1}}-1\) 的 LCP。并且,由于 \(j\) 的字典序比 \(i-1\) 小,而它们的首字母是相同的,所以 \(j+1\) 的字典序一定比 \(i\) 小。因此,\(i\) 与它排名前一位的那个后缀的 LCP 一定不小于 \(h_{\mathrm{rank}_{i-1}}-1\) 。

具体实现:

for (int i = 0; i < n; i++)
{
if (rank[i] == 0)
continue;
int j = sa[rank[i] - 1], k = height[rank[i - 1]];
if (k)
--k;
while (i + k < n && j + k < n && str[i + k] == str[j + k])
++k;
height[rank[i]] = k;
}

\(k\) (也就是 \(h_{\mathrm{rank}_{i-1}}\))不可能加到超过 \(n\) ,而 \(k\) 最多也只会减 \(n\) 次,因此时间复杂度是 \(O(n)\) 。

四、完整代码

namespace Suffix_Array
{
int height[N], sa[N], rank[N], tp[N], kind, n;
void radix_sort()
{
static int count[N];
memset(count, 0, sizeof(int[kind]));
for (int i = 0; i < n; i++)
++count[rank[i]];
for (int i = 1; i < kind; i++)
count[i] += count[i - 1];
for (int i = n - 1; i >= 0; i--)
sa[--count[rank[tp[i]]]] = tp[i];
}
void build(const char *const s)
{
n = strlen(s);
kind = CH;
for (int i = 0; i < n; i++)
tp[i] = i, rank[i] = ctoi(s[i]);
radix_sort();
for (int len = 1; len < n; len <<= 1)
{
int cnt = 0;
for (int i = n - len; i < n; i++)
tp[cnt++] = i;
for (int i = 0; i < n; i++)
if (sa[i] >= len)
tp[cnt++] = sa[i] - len;
radix_sort();
memcpy(tp, rank, sizeof(int[n]));
kind = 0;
rank[sa[0]] = kind++;
for (int i = 1; i < n; i++)
{
if (tp[sa[i]] == tp[sa[i - 1]] &&
sa[i] + len < n && sa[i - 1] + len < n &&
tp[sa[i] + len] == tp[sa[i - 1] + len])
rank[sa[i]] = kind - 1;
else
rank[sa[i]] = kind++;
}
if (kind == n)
break;
}
for (int i = 0; i < n; i++)
{
if (rank[i] == 0)
continue;
int j = sa[rank[i] - 1], k = height[rank[i - 1]];
if (k)
--k;
while (i + k < n && j + k < n && str[i + k] == str[j + k])
++k;
height[rank[i]] = k;
}
}
}

【知识总结】后缀数组(Suffix_Array)的更多相关文章

  1. 【后缀数组之SA数组】【真难懂啊】

    基本上一搜后缀数组网上的模板都是<后缀数组——处理字符串的有力工具>这一篇的注释,O(nlogn)的复杂度确实很强大,但对于初次接触(比如窝)的人来说理解起来也着实有些困难(比如窝就活活好 ...

  2. 【洛谷1117_BZOJ4650】[NOI2016] 优秀的拆分(哈希_后缀数组_RMQ)

    题目: 洛谷1117 分析: 定义把我校某兔姓神犇Tzz和他的妹子拆分,为"优秀的拆分" 随便写个哈希就能有\(95\)分的好成绩-- 我的\(95\)分做法比fei较chang奇 ...

  3. SPOJ DISUBSTR ——后缀数组

    [题目分析] 后缀数组模板题. 由于height数组存在RMQ的性质. 那么对于一个后缀,与前面相同的串总共有h[i]+sa[i]个.然后求和即可. [代码](模板来自Claris,这个板子太漂亮了) ...

  4. 后缀数组 --- WOj 1564 Problem 1564 - A - Circle

    Problem 1564 - A - Circle Problem's Link:   http://acm.whu.edu.cn/land/problem/detail?problem_id=156 ...

  5. BZOJ 3172([Tjoi2013]单词-后缀数组第一题+RMQ)

    3172: [Tjoi2013]单词 Time Limit: 10 Sec   Memory Limit: 512 MB Submit: 268   Solved: 145 [ Submit][ St ...

  6. BZOJ.4199.[NOI2015]品酒大会(后缀数组 单调栈)

    BZOJ 洛谷 后缀自动机做法. 洛谷上SAM比SA慢...BZOJ SAM却能快近一倍... 显然只需要考虑极长的相同子串的贡献,然后求后缀和/后缀\(\max\)就可以了. 对于相同子串,我们能想 ...

  7. BZOJ.4453.cys就是要拿英魂!(后缀数组 单调栈)

    BZOJ 求字典序最大,容易想到对原串建后缀数组求\(rk\). 假设当前区间是\([l,r]\),对于在\([l,r]\)中的两个后缀\(i,j\)(\(i<j\)),显然我们不能直接比较\( ...

  8. BZOJ 4556: [Tjoi2016&Heoi2016]字符串(后缀数组 + 二分答案 + 主席树 + ST表 or 后缀数组 + 暴力)

    题意 一个长为 \(n\) 的字符串 \(s\),和 \(m\) 个询问.每次询问有 \(4\) 个参数分别为 \(a,b,c,d\). 要你告诉它 \(s[a...b]\) 中的所有子串 和 \(s ...

  9. BZOJ 1031 [JSOI2007]字符加密Cipher 后缀数组教程

    1031: [JSOI2007]字符加密Cipher Description 喜欢钻研问题的JS同学,最近又迷上了对加密方法的思考.一天,他突然想出了一种他认为是终极的加密办法:把需要加密的信息排成一 ...

随机推荐

  1. python 配置文件 ConfigParser模块

    ConfigParser模块 用于生成和修改常见配置文档,当前模块的名称在 python 3.x 版本中变更为 configparser. 来看一个好多软件的常见文档格式如下 [DEFAULT] Se ...

  2. pxc增量备份

    ###增备数据库,如果后续还需要再次增备,则可以再次指定--extra-lsndir,如果与上次备份指定相同的位置,该文件被覆盖# innobackupex --compress --incremen ...

  3. [bzoj4726][POI2017][Sabota?] (树形dp)

    Description 某个公司有n个人, 上下级关系构成了一个有根树.其中有个人是叛徒(这个人不知道是谁).对于一个人, 如果他 下属(直接或者间接, 不包括他自己)中叛徒占的比例超过x,那么这个人 ...

  4. 【Codeforces 264B】Good Sequences

    [链接] 我是链接,点我呀:) [题意] 让你在一个递增数组中选择一个最长子序列使得gcd(a[i],a[i+1])>1 [题解] 设f[i]表示以一个"含有素因子i的数字" ...

  5. nyoj 1112 求次数(map, set)

    求次数 时间限制:1000 ms  |  内存限制:65535 KB 难度:2   描述 题意很简单,给一个数n 以及一个字符串str,区间[i,i+n-1] 为一个新的字符串,i 属于[0,strl ...

  6. POJ 1026 置换群的k次幂问题

    题目大意: 给定了一组对应关系,经过k次幂后,得到新的对应关系b[i],然后将给定的字符串上的第i位字符放置到b[i]的位置上, 如果字符串长度不足n就用空格补足,这里的是空格,也就是str[i] = ...

  7. 洛谷—— P2196 挖地雷

    https://www.luogu.org/problem/show?pid=2196 题目背景 NOIp1996提高组第三题 题目描述 在一个地图上有N个地窖(N<=20),每个地窖中埋有一定 ...

  8. CentosOS 7: 创建Nginx+Https网站

    参考文章: 1. https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E 2. http://songchenwen.com/tech/ ...

  9. cisco路由器上的DHCP

    一.实验拓扑 二.具体配置 Router(config)#do sh run Building configuration...   Current configuration : 604 bytes ...

  10. win7下安装SQLSERVER2000

    来自为知笔记(Wiz)