zoukankan      html  css  js  c++  java
  • 后缀数组(SA)总结

    后缀数组(SA)总结

    这个东西鸽了好久了,今天补一下

    概念

    后缀数组(SA)是什么东西?

    它是记录一个字符串每个后缀的字典序的数组

    (sa[i]):表示排名为(i)的后缀是哪一个。

    (rnk[i]):可以理解为(SA)数组的逆,记录后缀(i)的排名是多少,(rnk[SA[i]]=i)

    (lcp[i]):别人一般叫(height),表示后缀(SA[i])(SA[i-1])的最长公共前缀的长度。

    后缀排序

    求出后缀数组的算法,模板题

    代码

    先上代码,便于理解

    #define cmp(i, j, k) (y[i] == y[j] && y[i + k] == y[j + k])
    void Get_SA() {
        static int x[MAX_N], y[MAX_N], bln[MAX_N];
    	int M = 122; 
    	for (int i = 1; i <= N; i++) bln[x[i] = a[i]]++; 
    	for (int i = 1; i <= M; i++) bln[i] += bln[i - 1]; 
    	for (int i = N; i >= 1; i--) sa[bln[x[i]]--] = i; 
    	for (int k = 1; k <= N; k <<= 1) { 
    		int p = 0; 
    		for (int i = 0; i <= M; i++) y[i] = 0; 
    		for (int i = N - k + 1; i <= N; i++) y[++p] = i; 
    		for (int i = 1; i <= N; i++) if (sa[i] > k) y[++p] = sa[i] - k;
    		for (int i = 0; i <= M; i++) bln[i] = 0; 
    		for (int i = 1; i <= N; i++) bln[x[y[i]]]++; 
    		for (int i = 1; i <= M; i++) bln[i] += bln[i - 1]; 
    		for (int i = N; i >= 1; i--) sa[bln[x[y[i]]]--] = y[i]; 
    	    swap(x, y); x[sa[1]] = p = 1;
    		for (int i = 2; i <= N; i++) x[sa[i]] = cmp(sa[i], sa[i - 1], k) ? p : ++p;
    		if (p >= N) break;
    		M = p; 
    	} 
    } 
    

    算法流程

    (sa)的算法有倍增法和(DC3),因为后者有码量大、常数大、我不会等种种缺点,

    这里只介绍倍增算法。

    我们如果对于每个倍增完的二元组,每个都(sort)一下,复杂度是(O(nlog^2))的。

    那么将基数排序应用到其中去,就可以做到(O(nlogn)),具体做法:

    我们考虑一下普通的基数排序是怎么排二元组

    先将第二位丢进桶里,然后按照第一维的次序取出。

    那么这个字符串怎么排呢?

    首先当(k=0)时,我们直接桶排一下就行了。

    但是我们还要接着排啊,

    还记得吧,基排序是先按照第二维从小往大排

    那么,我们就先把第二维的顺序搞出来

    首先最小的一定就是没有第二维的东西

    所以我们先把这些数直接丢进数组里面

    接下来就是有第二维的东西啦

    (i)位的第二维是啥?(rnk[i+k])

    所以,从小到达枚举(sa),这样保证第二维从小往大

    那么,只要(sa[i]>k)

    就证明它是一个东西的第二维

    所以,把(sa[i]−k)

    丢到数组里面去就好啦

    这样的话,按照第二维就排好啦

    再来依次按照第一维丢到桶里面去

    做一遍基数排序就好啦

    这样就能够求出(sa)

    看起来很简单诶。。

    只是数组不要搞混了

    一定搞清楚每个数组是干啥的

    比如我的代码

    (sa)是后缀数组,(sa[i])表示排名为i的串是哪一个

    (rnk[i])相当于排名,(rnk[i])表示第i个串的排名

    (x,y)两个数组是记录顺序的

    分别记录第一维和第二维的排序的顺序

    (bln)是桶。

    如果实在理解不了,就背吧,反正也没有多长

    那么(lcp)数组怎么求呢?

    (forall i<j),不妨设(rnk[j]<rnk[k]),那么以(j)开头的后缀和(k)开头的后缀的最长公共前缀就是(min _{i=rnk[j]+1}^{rnk[k]} lcp[i])

    有一个引理:

    定义(h[i]=lcp[rnk[i]]),那么,(h[i]geq h[i-1]-1)

    证明:设(s[k...])为排在(s[i-1...])的前一名的后缀,其最长公共前缀为(h[i-1]),则(s[k+1...])(s[i...])的最长公共前缀显然大于等于(h[i-1]-1),原结论得证。

    然后这样求就可以了:

        for (int i = 1; i <= N; i++) rnk[sa[i]] = i; 
        for (int i = 1, j = 0; i <= n; i++) { 
            if (j) j--; 
            while (a[i + j] == a[sa[rnk[i] - 1] + j]) ++j; 
            lcp[rnk[i]] = j; 
        }
    

    一些trick

    总结了一些食用SA时的(trick)

    一、对于可重复的最长重复子串问题(若子串(s)重复出现次数大于等于二,则称重复子串)(Ans=max_{i=1}^nlcp_i)

    二、对于不可重叠的最长重复子串问题,二分,将问题转化为是否有两个长度为(k)的子串是相同的,且不重叠。将(lcp)数组分组,最长公共前缀不小于(k)的为一组其中如果有一组(sa[i])之差大于(k)时,则成
    立。

    三、对于可重叠的重复(k)次最长重复子串,与上一种方法思路相似,二分,问题转化为判断是否存在(k)个长度为(l)的子串是相同的,将最长公共子串大于(l)的后缀分为一组,查看每一组内后缀个数是否大于(k)

    四、对于多个字符串的问题,通常用一个原串中不会出现的字符将两个字符串连接为一个。对于最长公共子串问题,首先将两个字符串用一个未出现过的字符连接起来,然后求出它们的最长公共前缀,解时注意判断是否在间隔符两边。

    五、求取长度不小于(k)的公共子串个数时,将两个字符串按照上述方法连接,中间用一个未曾出现过的字符隔开,计算所有后缀之间最长公共前缀的长度,用单调栈维护最长公共前缀的长度。

    六、对于在多个字符串中,出现不小于(k)个字符串的最长公共子串。按照上述方法连接多个字符串后,使用二分法。对于给定的长度,先分组,判断每组字符串后缀是否出现在不同的(k)个字符串中。

    七、对于在每个字符串中至少出现两次且不重叠的最长公共子串时,按照上述方法连接多个字符串,使用二分法。对于给定的长度,先分组,判断是否有一组包含每个字符串中的两个不重叠答案。

    一些后话

    我还不太熟悉,题目暂未整理出来。

    以后会提供每个(trick)的例题及一些题单。

    如有错漏之处,请联系作者。

    参考文章:
    yyb的博客 https://www.cnblogs.com/cjyyb/p/8335194.html
    清华大学出版社《ACM/ICPC算法基础训练教程》第8章

  • 相关阅读:
    js 符号
    Flexbox的一些东西,再看看这里面的一些文章,基本上能解决相关问题
    使用initial-letter实现首字下沉效果,感兴趣的同学欢迎猛击:
    特效网址2
    特效网址
    CSS Icon 项目地址 小图标-用css写成的
    【SSH进阶之路】一步步重构容器实现Spring框架——配置文件+反射实现IoC容器(十)
    【SSH进阶之路】一步步重构容器实现Spring框架——解决容器对组件的“侵入式”管理的两种方案--主动查找和控制反转(九)
    【SSH进阶之路】一步步重构容器实现Spring框架——从一个简单的容器开始(八)
    从今天起,写一本关于LLVM的书----《深入理解LLVM》
  • 原文地址:https://www.cnblogs.com/heyujun/p/10300582.html
Copyright © 2011-2022 走看看