您的位置:首页 > 其它

【转载&总结】后缀数组及广泛应用

2017-08-03 22:05 357 查看
转自:http://blog.csdn.net/yxuanwkeith/article/details/50636898 五分钟搞懂后缀数组!后缀数组解析以及应用(附详解代码) 作者:YxuanwKeith

为什么学后缀数组

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

有什么SAM做不了的例子?
比如果求一个串后缀的lcp方面的应用,这是SA可以很方便的用rmq来维护,但是SAM还要求lca,比较麻烦,还有就是字符集比较大的时候SA也有优势。

现在这里放道题,看完这个blog可能就会做了!:
你可想想这道题:你有一个01串S,然后定义一个前缀最右边的位置就是这个前缀的结束位置。现在有q多个询问,每个询问结束位置在l~r中不同前缀的最长公共后缀是多长?
|S|,q≤100000
时限4s

而下面是我对后缀数组的一些理解

构造后缀数组——SA

先定义一些变量的含义

Str :需要处理的字符串(长度为Len)
Suffix[i] :Str下标为i ~ Len的连续子串(即后缀)
Rank[i] : Suffix[i]在所有后缀中的排名
SA[i] : 满足Suffix[SA[1]] < Suffix[SA[2]] …… < Suffix[SA[Len]],即排名为i的后缀为Suffix[SA[i]] (与Rank是互逆运算)
好,来形象的理解一下

/*
Problem: JZOJ1598(询问一个字符串中有多少至少出现两次的子串)
Content: SA's Code and Explanation
Author : YxuanwKeith
*/

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int MAXN = 100005;

char ch[MAXN], All[MAXN];
int SA[MAXN], rank[MAXN], Height[MAXN], tax[MAXN], tp[MAXN], a[MAXN], n, m;
char str[MAXN];
//rank[i] 第i个后缀的排名; SA[i] 排名为i的后缀位置; Height[i] 排名为i的后缀与排名为(i-1)的后缀的LCP
//tax[i] 计数排序辅助数组; tp[i] rank的辅助数组(计数排序中的第二关键字),与SA意义一样!
//a为原串
void RSort() {
//rank第一关键字,tp第二关键字。
for (int i = 0; i <= m; i ++) tax[i] = 0;//计数初始化
for (int i = 1; i <= n; i ++) tax[rank[tp[i]]] ++;
for (int i = 1; i <= m; i ++) tax[i] += tax[i-1];//前缀和求出排名
for (int i = n; i >= 1; i --) SA[tax[rank[tp[i]]] --] = tp[i]; //确保满足第一关键字的同时,再满足第二关键字的要求
} //计数排序,把新的二元组排序。

int cmp(int *f, int x, int y, int w) { return f[x] == f[y] && f[x + w] == f[y + w]; }
//通过二元组两个下标的比较,确定两个子串是否相同

void Suffix() {
//SA
for (int i = 1; i <= n; i ++) rank[i] = a[i], tp[i] = i;
m = 127 ,RSort(); //一开始是以单个字符为单位,所以(m = 127)

for (int w = 1, p = 1, i; p < n; w += w, m = p) { //把子串长度翻倍,更新rank

//w 当前一个子串的长度; m 当前离散后的排名种类数
//当前的tp(第二关键字)可直接由上一次的SA的得到
for (p = 0, i = n - w + 1; i <= n; i ++) tp[++ p] = i; //长度越界,第二关键字为0
for (i = 1; i <= n; i ++) if (SA[i] > w) tp[++ p] = SA[i] - w;

//更新SA值,并用tp暂时存下上一轮的rank(用于cmp比较)
RSort(), swap(rank, tp), rank[SA[1]] = p = 1;

//用已经完成的SA来更新与它互逆的rank,并离散rank
for (i = 2; i <= n; i ++) rank[SA[i]] = cmp(tp, SA[i], SA[i - 1], w) ? p : ++ p;
}
//离散:把相等的字符串的rank设为相同。
//LCP
int j, k = 0;
for(int i = 1; i <= n; Height[rank[i ++]] = k)
for( k = k ? k - 1 : k, j = SA[rank[i] - 1]; a[i + k] == a[j + k]; ++ k);
//这个知道原理后就比较好理解程序
}

void Init() {
scanf("%s", str);
n = strlen(str);
for (int i = 0; i < n; i ++) a[i + 1] = str[i];
}

int main() {
Init();
Suffix();

int ans = Height[2];
for (int i = 3; i <= n; i ++) ans += max(Height[i] - Height[i - 1], 0);
printf("%d\n", ans);
}


神奇的代码

4个比较基础的应用

Q1:一个串中两个子串的最大公共前缀是多少?
A1:这不就是Height吗?用rmq预处理,再O(1)查询。

Q2:一个串中可重叠的重复最长子串是多长?
A2:就是求任意两个后缀的最长公共前缀,而任意两个后缀的最长公共前缀都是Height 数组里某一段的最小值,那最长的就是Height中的最大值

Q3:一个串中不可重叠的重复最长子串是多长?
A3:先二分答案转化成判别式的问题比较好处理。假设当前需要判别长度为k是否符合要求,只需把排序后的后缀分成若干组,其中每组的后缀之间的Height 值都不小于k,再判断其中有没有不重复的后缀,具体就是看最大的SA值和最小的SA值相差超不超过k,有一组超过的话k就是合法答案。

A4:一个字符串不相等的子串的个数是多少?
Q4:每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。而且可以发现每一个后缀Suffix[SA[i]]的贡献是Len - SA[i] + 1,但是有子串算重复,重复的就是Heigh[i]个与前面相同的前缀,那么减去就可以了。最后,一个后缀Suffix[SA[i]]的贡献就是Len - SA[k] + 1 - Height[k]
对于后缀数组更多的应用这里就不详细阐述,经过思考后每个人都会发现它的一些不同的用途,它的功能也许比你想象中的更强大!

最开始的那道题

先搬下来。。。

你可想想这道题:你有一个01串S,然后定义一个前缀最右边的位置就是这个前缀的结束位置。现在有很多个询问,每q个询问结束位置在l~r中不同前缀的最长公共后缀是多长?
|S|,q≤100000
时限4s

简单思路:首先可以把字符串反过来就是求后缀的最长公共前缀了,可以用SA求出height数组,然后用rmq预处理之后就是求两个位置间的最小值。然后对于一个区间,显然只有在SA数组中相邻的两个串可以贡献答案。
对于区间询问的问题可以用莫队处理,然后考虑加入一个后缀应该怎么处理,我们可以维护一个按SA数组排序的链表。假设我们先把所有位置的SA全部加入,然后按顺序删除,重新按顺序加入时就可以O(1)完成修改。那么按照这个思路我们可以用固定左端点的并查集,做到只加入,不删除,然后用O(nn√+nlogn)的复杂度完成这道题。

*可能后面的处理方式比较麻烦,如果直接用splay维护区间中的后缀的话可以做到O(nn√logn),这个方法就比较直观,而SAM在个问题上还是有点无力的。这题只是为了说明SA相比于SAM还是有他的独到之处,特别是在处理后缀的lcp之类的问题上。

结束

以上就是我对后缀数组的理解 ——YxuanwKeith

简要总结

后缀数据充分利用了先前的信息,使得效率有可观性地提高。

suddix[i]表示后缀开始位置为i的后缀字符串

rank[i]表示第i个后缀的排名

sa[i]表示排名第i的后缀位置

height[i]表示排名为i和i-1的后缀字符串的最长公共前缀长度

以下是个人关于几个应用的理解

1.求字符串中可重叠的最长公共子串

根据height定义很显然这个就是height中的最大值了。

2.求字符串中不可重叠的最长公共子串

这个我们需要二分答案再验证,我们要二分可能的公共子串长度k,然后按sa的顺序对height进行分组,使组内的height值都不小于k,然后对于某个组内我们只要考察该组内sa的最大值和最小值的差是否大于等于k(实际上就是这两个后缀的开头是否相差k从而避免重叠),有则k成立。

3.求字符串中可重叠K次的最长公共子串

这个我们跟2差不多,二分公共长度k分组,然后我们考察每个组内的后缀个数是否大于等于K,有则K成立。

4.求字符串中不相同的子串个数

每个子串必定是某个后缀的前缀,那问题就是求所有后缀中不相同的前缀的个数,我们从顺序sa[1],sa[2],sa[3],不难发现每加入一个suffix[sa[i]],它有n-sa[i]+1个前缀(就是这个后缀的长度),其中有height[i]是和前面的字符串相同(最长公共前缀嘛),所以这个字符串会贡献出n-sa[i]+1-height[i]不同的子串,累加后就可以了。

5.求字符串中最长的回文子串

所谓回文就是一个字符串满足中心对称,某个字符为对称中心,从这个字符向左和向右对应位置的字母都相等,如12345678987654321,我们设这个中心对称的字符为a[i],则我们就要判断 a[i-k]与a[i+k]是否相等,我们可以把整个字符串倒过来写在这个字符串后面(我们就得到了逆过来的那个12345678的字符串),其中加个特殊符号,这样我们可以简化判断,只用判断这新的字符串的某两个字符串的最长公共前缀

6.求字符串中连续的重复子串

已知一个字符串L是由某个字符串S重复R次得到的求最大值。

我们假设S的长度为k,首先L%k=0,然后判断suffix[1],和suffix[k+1]的最长公共子串是否为n-k。因此在查找最长公共子串的时候就是求height[rank[k+1]]到height[rank[1]]之间的最小值。因此我们的做法就是求height数组中每一个数到height[rank[1]]之间的数的最小值k,R=L的长度/K

7.求字符串中重复次数最多的连续重复子串

8.求两个字符串的最长公共子串

将这两个字符串连接起来,其中用一个特殊符号分开,然后再求出不在同一个字符串中的最大的height值即可。

9.求长度不小于K的最长公共子串

将两个字符串A、B连接起来,其中用一个特殊符号分开,然后用k对height数组分组,再统计每组的最长公共前缀和。每遇到一个B子串,就统计与前面A子串产生多少个长度不小于K的公共子串,这里A需要用栈来维护。然后对A也一样的处理。

10.求n个字符串的最长公共子串

这个用KMP可以处理,也可以将这n个字符串连成一个字符串,然后用不同的特殊符号分开,二分长度k对height数组进行分组判定是否该组中所有字符串的子串都出现在里面即可。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: