• 后缀数组详解


    参照博客

    后缀数组

    定义: 后缀就是从字符串的某个位置i到字符串末尾的子串,我们定义以s的第i个字符为第一个元素的后缀为(suff(i))

    辅助数组:

    (sa_i):表示排名为(i)的后缀的起始位置的下标

    (rk_i):表示起始位置的下标为(i)的后缀的排名

    (x_i):表示起始位置的下标为(i)的后缀的第一关键字

    (y_i):表示起始位置的下标为(i)的后缀的第二关键字

    后缀数组的思想:

    先说最暴力的情况,快排(O(nlogn))每个后缀,但是这是字符串,所以比较任意两个后缀的复杂度其实是(O(n)),这样一来就是接近(O(n^2logn))的复杂度,数据大了肯定是不行的,所以我们这里有两个优化。

    倍增: 每次以前(2^k)为第一关键字,后(2^k)为第二关键字(没有的补零)进行合并,预处理出按第一个字符排序的情况,当最后每个后缀的排名都不同时,排序完成

    基数排序: 简单来说,就是个桶

    代码具体流程:

    1. 按第一个字符排序
    for (register int i = 1;i <= n;i++) ++c[x[i] = s[i]];
    for (register int i = 1;i <= m;i++) c[i] += c[i-1];
    for (register int i = n;i >= 1;i--) sa[c[x[i]]--] = i;
    

    举个例子:

    [abbac ]

    • 第一个循环结束后,(c)数组的情况是2 2 1

    • 第二个循环结束后,(c)数组的情况是2 4 5

    • 第三个循环从后往前循环,使相同字符排在后面的依旧排在后面,得到的(sa)数组为0 2 3 1 4

    1. 按第二关键字的排名求y
    • 最后的n-k+1~n部分没有第二关键字,排在最前面

    • 枚举串的排名,如果排名为(i)的串可以作为第二关键字,就入队,排名在前的先入后出

    int num = 0;
    for (register int i = n-k+1;i <= n;i++) y[++num] = i;
    for (register int i = 1;i <= n;i++) if (sa[i] > k) y[++num] = sa[i]-k;
    
    1. 求解sa

    因为(y)数组已经是排好序的,基数排序又是按照(x)排好序的,所以正常进行排序就好

    memset(c,0,sizeof(c));
    for (register int i = 1;i <= n;i++) ++c[x[i]];
    for (register int i = 1;i <= m;i++) c[i] += c[i-1];
    for (register int i = n;i >= 1;i--) sa[c[x[y[i]]]--] = y[i],y[i] = 0;
    
    1. 更新x

    根据更新好的(sa),如果第一第二关键字相同,排名相同,否则就顺序排

    x[sa[1]] = 1,num = 1;
    for (register int i = 2;i <= n;i++)  x[sa[i]] = (y[sa[i]] == y[sa[i-1]]&&y[sa[i]+k] == y[sa[i-1]+k]) ? num : ++num;
    

    最长公共前缀

    定义: (LCP(i,j))(suff(sa_i))(suff(sa_j))的最长公共前缀

    性质:

    1. (LCP(i,j) ext{=} LCP(j,i))

    2. (LCP(i,i) ext{=} len(sa_i) ext{=} n ext{-} sa_i ext{+} 1)

    3. (LCP(i,k) ext{=} min(LCP(i,j),LCP(j,k))),其中(1leq ileq jleq kleq n)

    4. (LCP(i,k) ext{=} min(LCP(j,j-1))),其中(i<jleq k)

    前两条性质的正确性应该不难想吧,最主要的就是第三条,第四条根据第三条也不难想,下面给出证明:

    (LCP(i,k) ext{=} q,p ext{=} min(LCP(i,j),LCP(j,k)),u ext{=} stuff(sa_i),v ext{=} stuff(sa_j),w = stuff(sa_k))

    不难想到(q geq p),如果假设(q > p),则有(u_{p+1} eq v_{p+1})(v_{p+1} eq w_{p+1})至少一条满足,且(u_{p+1} = w_{p+1})

    但是别忘了现在的(u,v,w)三者是有顺序关系的,则(u_{p+1} leq v_{p+1} leq w_{p+1}),等号不同时成立,与上述(u_{p+1} = w_{p+1})矛盾

    假设假了,得出(q leq p),所以(q = p)

    求解LCP:

    (height_i)(LCP(i,i-1)),则(LCP(i,k) ext{=}min(height_j)),那么如何求解height???

    因为下面嵌套的东西太多了,所以就不用LaTeX了,复制粘贴了

    设h[i]=height[rk[i]],同样的,height[i]=h[sa[i]];

    首先我们不妨设第i-1个字符串按排名来的前面的那个字符串是第k个字符串

    这时,依据height的定义,第k个字符串和第i-1个字符串的公共前缀自然是height[rk[i-1]],现在先讨论一下第k+1个字符串和第i个字符串的关系。

    第一种情况,第k个字符串和第i-1个字符串的首字符不同,那么第k+1个字符串的排名既可能在i的前面,也可能在i的后面,但没有关系,因为height[rk[i-1]]就是0了呀,那么无论height[rk[i]]是多少都会有height[rk[i]]>=height[rk[i-1]]-1,也就是h[i]>=h[i-1]-1。

    第二种情况,第k个字符串和第i-1个字符串的首字符相同,那么由于第k+1个字符串就是第k个字符串去掉首字符得到的,第i个字符串也是第i-1个字符串去掉首字符得到的,那么显然第k+1个字符串要排在第i个字符串前面。同时,第k个字符串和第i-1个字符串的最长公共前缀是height[rk[i-1]],

    那么自然第k+1个字符串和第i个字符串的最长公共前缀就是height[rk[i-1]]-1。

    到此为止,第二种情况的证明还没有完,我们可以试想一下,对于比第i个字符串的排名更靠前的那些字符串,谁和第i个字符串的相似度最高(这里说的相似度是指最长公共前缀的长度)?显然是排名紧邻第i个字符串的那个字符串了呀,即sa[rank[i]-1]。但是我们前面求得,有一个排在i前面的字符串k+1,LCP(rk[i],rk[k+1])=height[rk[i-1]]-1;

    又因为height[rk[i]]=LCP(i,i-1)>=LCP(i,k+1)

    所以height[rk[i]]>=height[rk[i-1]]-1,也即h[i]>=h[i-1]-1。

    完整代码:

    #include <cstdio>
    #include <algorithm>
    #include <iostream>
    #include <cstring>
    using namespace std;
    const int maxn = 1e6+10;
    int n,m;
    char s[maxn];
    int sa[maxn],rk[maxn],x[maxn],y[maxn],c[maxn],height[maxn];
    inline void getsa(){
    	// 第一次排序 
    	for (register int i = 1;i <= n;i++) ++c[x[i] = s[i]];
    	for (register int i = 1;i <= m;i++) c[i] += c[i-1];
    	for (register int i = n;i >= 1;i--) sa[c[x[i]]--] = i;
    	
    	for (register int k = 1;k <= n;k <<= 1){
    		int num = 0;
    		for (register int i = n-k+1;i <= n;i++) y[++num] = i;
    		for (register int i = 1;i <= n;i++) if (sa[i] > k) y[++num] = sa[i]-k;
    		memset(c,0,sizeof(c));
    		for (register int i = 1;i <= n;i++) ++c[x[i]];
    		for (register int i = 1;i <= m;i++) c[i] += c[i-1];
    		for (register int i = n;i >= 1;i--) sa[c[x[y[i]]]--] = y[i],y[i] = 0;
    		swap(x,y);
    		x[sa[1]] = 1,num = 1;
    		for (register int i = 2;i <= n;i++)  x[sa[i]] = (y[sa[i]] == y[sa[i-1]]&&y[sa[i]+k] == y[sa[i-1]+k]) ? num : ++num;
    		if (num == n) break;
    		m = num;
    	}
    	for (int i = 1;i <= n;i++) rk[sa[i]] = i; 
    	for (register int i = 1;i <= n;i++) printf("%d ",sa[i]);
    }
    void gethigh(){
    	int k = 0;
     	for (int i = 1;i <= n;i++){
            if (rk[i] == 1) continue;
            if (k) --k;
            int j = sa[rk[i]-1];
            while (j+k <= n&&i+k <= n&&s[i+k] == s[j+k]) ++k;
            height[rk[i]] = k;
        }
        for (int i = 1;i <= n;i++) printf("%d ",height[i]);
    }
    int main(){
    	scanf ("%s",s+1);
    	n = strlen(s+1),m = 122;
    	getsa();gethigh();
    	return 0;
    }
    

    LCP的应用

    1. 求本质不同的子串个数

    因为子串一定是后缀的前缀,我们用所有的子串个数,减掉重复的前缀即可。所有子串个数为(dfrac{n(n+1)}{2}) ,重复的前缀个数为(sumlimits_{i=1}^{n}height_i),其实就是按从小到大枚举后缀,减去这个后缀和前面一个重复的前缀,这样子做是对的是因为(lcp(i,i-1))一定是(lcp(j,i) (1leq j<i))里面最大的。

    1. 求至少出现k次的子串最大长度

    最后要求的子串一定是(k)个后缀的lcp。然后贪心地想,要让这个lcp最大,这(k)个后缀一定是连续的(指字典序)。然后我们现在就是求([i,i+k-1])的所有后缀的lcp,这个其实就是(lcp(i,i+k-1)),又等价于(min(height_j) i<jleq i+k-1)。用个单调队列就可以了。

    1. 是否有子串不重叠地出现了至少两次

    二分子串长度(|s|),然后对每个i,求出最大的j,使得(lcp(i,j)ge s)。然后用(RMQ) 查询([i,j])的最大下标和最小下标,判断一下即可,总复杂度还是(O(nlogn))的。

  • 相关阅读:
    [CodeIgniter4]-将控制器放入子目录中
    [CodeIgniter4]-控制器
    [CodeIgniter4]-处理多环境
    [CodeIgniter4]-管理多个应用
    [CodeIgniter4]-代码模块
    [CodeIgniter4]-网页缓存
    [CodeIgniter4]-错误处理
    netfilter分析
    使用 SELinux 和 Smack 增强轻量级容器
    如何增强 Linux 系统的安全性,第一部分: Linux 安全模块(LSM)简介
  • 原文地址:https://www.cnblogs.com/little-uu/p/14403846.html
Copyright © 2020-2023  润新知