zoukankan      html  css  js  c++  java
  • [学习笔记]后缀数组

    [学习笔记]后缀数组

    零.前言与一些规定

    ​ 整了两三天,感觉自己把 09 年的那篇论文看懂了一点,题倒是没做几道,打算写一下以防忘记。

    ​ 在此有以下定义:

    • 字符串 S 为 (s[1...n]) ,其中后缀 i 指 (s[i...n])
    • (sa[i]:) 排名为 i 的后缀在原字符串的位置
    • (rank[i](rk[i]):) 后缀 i 的排名
    • (sa,rk) 形成一个一一对应关系,即 (sa[rk[i]]=rk[sa[i]]=i)
    • (lcp(i,j)) 后缀 i 和后缀 j 的最长公共前缀
    • (height[i](ht[i]):) (lcp(sa[i],sa[i-1]))

    一.求解 sa 与 rk

    ​ 因为有"排名"的概念,所以我们首先应该明确的是"排序"的规则。此处的排序并不等于一般意义上的排序,而是以第一个不相同的字符(若是没有则补空)的相对字典序大小决定。

    ​ 那么我们能想到一个十分朴素的算法,即将每一个后缀列出来,然后直接排序,但是这个算法显然很不行,于是考虑倍增算法。

    ​ 首先要知道什么是计数排序和基数排序。如果不知道建议先去了解一下,很简单的。然后我们设 (rk_w[i]) 为在所有只计算前 (w) 个字符的后缀中的排名,换而言之,每一个后缀的长度被限制到了 (w) ,然后考虑如何去求出 (rk_{2w}[i]) 。很好想的,只需要(rk_w[i+w]) 作为第二关键字,(rk_w[i]) 作为第一关键字,跑一遍基数排序就能得到新的顺序。

    ​ 然后这里有几个小细节。

    ​ 第一,若是长度不够,则以空(最小值补齐),所以数组应当开两倍。

    ​ 第二,虽然最后的所有后缀肯定互不相同,但是在中间是极有可能出现相同的,出于一些非常必要且特殊的原因(比如减小复杂度),中途的(rk_w) 需要去重,或者说是同样的字符串对应的 (rk) 须得是相同的。并且一开始为了方便 (w=1) 的时候 (rk_1[]) 是并不连续的,或者说并不严格,会出现“断层”,意思就是会有 (rk_1[a]=1,rk_2[c]=3) 但是并没有 (b) 这个元素的情况。但是在之后的倍增中就会严格。

    ​ 第三,最后的终止条件是 (w>n) ,十分显然,因为比较的原理是没有就补空。

    ​ 第四,基数排序中,第二关键字的排序并不需要运用到计数排序,因为这一次的第二关键字也是上一次的排序结果,是有序的,直接利用就行,代码中有着十分良好的体现。

    ​ 第五,我们始终希望原本位置相对靠后的,虽然相同,但是对应到它的 (sa) 会相对大一些。

    CODE

    using namespace std;
    #define fe(i,a,b) for(int i=a;i<=b;++i)
    #define ef(i,b,a) for(int i=b;i>=a;--i)
    const int MAXN=2e6+5;
    char s[MAXN];
    int rk[MAXN],id[MAXN],oldrk[MAXN],cnt[MAXN],sa[MAXN],n,m;
    inline bool cmp_SA(int i,int w){
    	return oldrk[sa[i]]==oldrk[sa[i-1]]&&oldrk[sa[i]+w]==oldrk[sa[i-1]+w];//相同的充要条件
    }
    int main(){
    	scanf("%s",s+1);
    	n=strlen(s+1),m=300;
    	fe(i,1,n)++cnt[rk[i]=s[i]];//一开始的rk并不很严格,第二点
    	fe(i,1,m)cnt[i]+=cnt[i-1];
    	ef(i,n,1)sa[cnt[rk[i]]--]=i;//相对靠后,第五点
    	for(int w=1,p=0;w<n;w<<=1,m=p,p=0){//p用来优化值域
    		ef(i,n,n-w+1)id[++p]=i;//先将第二关键字含有无限小(空)的放在前面,这里的循环顺序没有关系
    		fe(i,1,n)if(sa[i]>w)id[++p]=sa[i]-w;//第二关键字重复利用,第四点
    		memset(cnt,0,sizeof(cnt));
    		fe(i,1,n)++cnt[rk[id[i]]];
    		fe(i,1,m)cnt[i]+=cnt[i-1];
    		ef(i,n,1)sa[cnt[rk[id[i]]]--]=id[i];//计数排序与相对靠后
    		memcpy(oldrk,rk,sizeof(rk));
    		p=0;
    		fe(i,1,n)rk[sa[i]]=cmp_SA(i,w)?p:++p;//重新计算值域
    	}
    	fe(i,1,n)printf("%d ",sa[i]);
    	return 0;
    }
    

    二.Height 数组

    ​ 知道了前面两个基础数组之后,接下来是最难懂但是最有用的 (Height) 数组了。从其定义可以看出 (ht[i]=lcp(sa[i],sa[i-1])),即第 i 小的后缀和 i-1 小之间的最长公共前缀。然后又去思考“排序”排出来的意义,实际上就是将前缀差异最小的放在一起,忽略了开头在原串中的位置,差异越小越近,而 (ht) 反映了这个差异有多小,或者说有多么相似,重复的开头有多么长。

    ​ 所以可以写出一个定义式。

    [height[i]=lcp(sa[i],sa[i-1])=max{lcp(sa[i],j),rank[j]<rank[i]} ]

    1.ht的性质

    ​ 多用于求 (lcp(i,j)),式子为 (lcp(i,j)=min{height[rank[i]+1...rank[j]]},rank[i+1]leq rank[j]),或者是带入恒等式为 (lcp(sa[i],sa[j])=min{height[i+1...j]}),可以很直观的感受到它的意义,由于排序相隔最近的总是差异最小,可以理解为用最小的变化去靠近,或者说是这个最小值代表了这么长的前缀一直重复没有变过,可以手玩体会。

    ​ 由于子串相当于是某一个后缀的前缀,所以多用于解决子串的出现与大小问题。

    2.ht的求解及其引理

    ​ 首先我们有一个及其重要的引理

    [height[rank[i]]geq height[rank[i-1]]-1 ]

    我大概会口胡证明:

    ​ 首先明确以下几个显而易见的结论:

    • 在空串也视为后缀的意义下,所有的非空后缀删去第一个字符都是一个合法的后缀

    • 后缀开头位置的+1等同于删去一个字符,-1等同于在前面补充一个合法字符

    • (lcp(i,j)=k Rightarrow lcp(i+1,j+1)=k-1(k>0))(1)

    • 若有 (lcp(i,j)=k,rank[i]<rank[j]),则 (forall xin[0,k],rank[i+x]<rank[j+x])(2)

      接下来开始证明,记后缀 i 为 (A) ,后缀 (i-1)(aA) ,那么当 (ht[rk[i-1]]=0) 时,式子显然成立,不等于 0 时,有 (lcp(sa[rk[i-1]],sa[rk[i-1]-1])=k > 0),带入恒等式 (lcp(i-1,sa[rk[i-1]-1])=k>0),于是使用结论(1) (lcp(i,sa[rk[i-1]-1]+1)=k-1),

      可以知道 (sa[rk[i-1]-1]+1) 是一个合法的后缀,换句话说,存在一个合法的后缀 (j=sa[rk[i-1]-1]+1) 使得 (lcp(i,j)=k-1=height[rank[i-1]]-1) ,由 (rk[i-1]-1<rank[i-1]) 使用结论(2)知道 $rank[j]<rank[i] $然后由于定义式,得证。

      此处也可以用 (lcp) 的求解来理解,(k-1)(min{height[rank[j]+1...rank[i]]})

    这个引理近似于一个单调不降的性质,于是可以非常暴力的求解 height 数组,给出代码。

    CODE

    for(int p=0,i=1;i<=n;++i){
    	if(p)p--;
    	while(s[sa[rk[i]-1]+p]==s[sa[rk[i]]+p])++p;//这里也可以写成 s[i+p]
    	ht[rk[i]]=p;
    }
    

    三.常用范围

    0.序

    主要是照搬09年的那篇论文,以后做了题会补充吧cy

    1. 一个字符串

    最长重复子串(可重叠)

    ​ 等价于两个后缀的最长公共前缀,有很多种方式可以证明一定是来源于 (height) 数组,且为其中的最大值。

    最长重复子串(不可重叠)

    ​ 由于很明显满足单调性,先二分长度,然后将连续的大于长度 (height) 分组,看其中 (sa) 的最大最小值之差是否大于长度。

    出现 k 次的最长子串(可重叠)

    ​ 二分长度,分组。看组内的size是否大于等于 k。

    ​ 或者在 height 上面整 size 为 k-1 的滑动窗口,取最小值的最大值。

    不相同的子串个数

    ​ 对于每一个后缀 i ,会产生 (height[i]) 个重复的前缀,实际上所有的重复为 (lcp(i,k)) ,但是两两对应和不重复相减,取一个最大的 (lcp(i,k)) ,k的rank在他之前 ,即为 height。

    ​ 整合就是 (Ans=sum n-sa[i]+1-height[i]=sum i-height[i] =frac{n*(n+1)}{2}-sum height[i])

    回文子串

    ​ 将字符串反过来拼在后面,中间用一个特殊字符隔开,然后找对应的两个后缀的 (lcp) ,时间复杂度在 (nlogn) (用DC3和RMQ结合可以做到 (O(n)) 但是我不会hhhhh)

    连续重复子串

    S 由一个字符串 k 重复而成。枚举长度看 (lcp(1,len+1)) 是否等于 (n-k) 即可,预处理一手做到 (O(n))

    连续重复出现次数最多的子串

    枚举长度。重复若干次肯定包含了重复两次,也就是说在(s[1],s[1+L],s[1+2*L]...s[1+k*L]) 中肯定有两个位置相同,从这两个位置向左右匹配,看最长的 (lcp),然后稍加计算即可。

    2.两个字符串

    最长公共子串

    暴力拼在一起,中间隔开。然后看相邻 sa 所属不同的 height 的最大值。

    可以简单证明答案一定在 height 处产生,因为原题相当于求一个最大的 (lcp(i,j)) ,那么 (rank[i]...rank[j]) 之间肯定有一个相邻的 sa 相异,否则不产生答案。且这个 (lcp) 一定小于等于那个位置的 height

    长度不小于 k 的公共子串的个数

    拼接。分组。在每个组内对于 sa 相异的组合数学算一算。

    3.多个字符串

    出现在不小于 k 个字符串中的最长公共子串

    二分长度。分组。看每组来自几个不同的字符串。
    [upd21.2.17] 疑似可以尺取然后写一个伪单调对列做到线性,不过瓶颈卡在构造SA上了哈哈哈哈

    每个字符串中出现若干次且不重叠的最长子串

    二分长度。分组。看来源于相同字符串的 sa 最大最小值之差。

    在每个字符串中出现或者反转后出现

    先将所有字符串反写。所有正反拼接。二分长度,分组。组内判断是否在对应的两个字符串中任一出现。

  • 相关阅读:
    HUST 1584 摆放餐桌
    HUST 1585 排队
    HUST 1583 长度单位
    树状数组 poj2352 Stars
    Visual Studio2013应用笔记---WinForm事件中的Object sender和EventArgs e参数
    倒置输入的数 Exercise07_02
    指定等级 Exercise07_01
    检测密码 Exercise06_18
    一年的天数 Exercise06_16
    数列求和 Exercise06_13
  • 原文地址:https://www.cnblogs.com/clockwhite/p/14361172.html
Copyright © 2011-2022 走看看