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

    后缀数组

    参考:https://www.bilibili.com/video/av92589768?from=search&seid=11036159274843024348

    符号

    子串

    从原串中选取连续的一段即为子串,空串也是子串

    后缀

    我们用(suf(k))表示(s(k…n))构成的子串

    任何子串都是某个后缀的前缀

    最长公共前缀lcp

    (lcp(suf(i), suf(j))) 表示两个串(suf(i))(suf(j))最长的一样的前缀

    问题

    将所有后缀(suf(1),suf(2),…,suf(N))按照字典序从小到大排序

    方法1

    首先看到题目想到的就是直接用暴力,建一个(cmp)数组,用(string)可以比较大小的性质去暴力(sort)

    因为(sort)(nlog n)的,每次(cmp)函数都是(O(n))的,所以总的时间复杂度就是 (n^2log n)

    方法2

    想一想更好的做法,我们可以用二分+hash

    复杂度:(n log^2n)

    (cmp)函数中二分(suf(i))(suf(j))(lcp)

    (return s[i + |lcp|] < s[j +|lcp|])

    方法3

    (SA)算法

    $SA[l] = $ 排名第(l)的后缀的开始位置

    $Rank[i] = $ 后缀(suf(i))的排名

    Rank[SA[l]] = l;
    SA[Rank[i]] = i;
    

    求出其中一个就能(O(n))求出另一个

    有什么求其中一个数组的好的方法呢?

    答案是倍增

    方法三实现优化

    倍增

    (sub[i][k] = s)(i)开始长度(=s^k)的子串

    (sub[i][k]=s[i…i+(1 << k) - 1]),超过(n)的部分都视为''(字典序最小的字符)

    (rank[i][k] = sub[i][k])在长度(=2^k)的所有子串中的排名

    $sa[l][k] = (在长度)=2^k(的所有子串中排名第)l$的子串的开始位置

    过程

    1. 求出(sub[1][0], sub[2][0], …,sub[n][0])的字典排序
    2. 求出(sub[1][1], sub[2][1], …,sub[n][1])的字典排序
    3. ……
    4. 求出(sub[1][k], sub[2][k],…,sub[n][k])的字典排序

    当子串长度(2^k>=n)时,子串排序就是后缀排序

    利用(rank[1…n][k]),如何求出(rank[1…n][k+1])

    对于两个子串(sub[i][k+1])(sub[j][k+1])

    先比较(rank[i][k]<rank[j][k])

    若相等,再比较(rank[i+2^k][k]<rank[j+2^k][k])

    其实就相当于对二元组((rank[i][k], rank[i+2^k][k]))排序

    (pair)排序时,先按(first)比较,若相等再按(second)比较

    但如果建(pair)数组直接(sort)的话,复杂度还是(nlog^2n),还不如写二分+hash

    于是这个时候就出现了一个神奇的东西:基数排序

    为什么可以优化呢?我们注意到(rank)这个数组,他的值域是多少?

    没错,值域就是不超过(n)的正整数,所以我们就可以用基数排序,换句话说就是桶排序

    关于基数排序的相关,看可以去看一下洛谷日报第十五期,这里给出链接:基数排序

    (SA)时的基数排序用(cnt)实现

    如何将(a[i])数组基数排序,然后将结果放在(SA)数组中呢?

    下面的代码就实现了输入一个(a)数组,得到(sa)数组

    for (int i = 1; i <= n; i++) ++cnt[a[i]];
    for (int i = 1; i <= n; i++) cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; i--) sa[cnt[a[i]]--] = i;
    

    比如一个(a)数组为

    (a=[2,1,2,4,2])

    若用(sa[l])表示排名第(l)的数在(a)中的下标

    (sa=[2,1,3,5,4])

    就可以根据

    Rank[SA[l]] = l;
    SA[Rank[i]] = i;
    

    得出(rank)数组

    (rank=[2,1,2,3,2])

    到这里我们就能回到一开始的问题,实现用(rank[1…n][k]),如何求出(rank[1…n][k+1]),步骤如下:

    (large for(k = 1 sim log n))

    • (rank[i+2^k][k])(第二关键字)基数排序
    • (rank[i][k])(第一关键字)基数排序,得到(sa[i][k+1])数组
    • (sa[i][k+1])求出(rank[i][k+1])

    如果你细心的话可能会发现,(k)是从(1)开始的而不是从(0)开始的,那么(k)(0)时候怎么来的呢?

    因为(2^0)就是(1),所以我们可以直接把(rank)数组(也就是排名)先设成当前字符的( ext{ASCII})码,这样就可以啦~

    sa->rank

    如果(rk[i])中有并列

    for (int p = 0, i = 1; i <= n; i++) {
    	if(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + k] == oldrk[sa[i - 1] + k])
    		rk[sa[i]] = p;
    	else rk[sa[i]] = ++p;
    }
    

    代码

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    using namespace std;
    
    const int A = 1e6 + 11;
    
    inline int read() {
    	char c = getchar();
    	int x = 0, f = 1;
    	for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
    	for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
    	return x * f;
    }
    
    char s[A];
    int n, m, sa[A], rank[A], tp[A], tax[A];
    
    void cntsort() {
    	for (int i = 0; i <= m; i++) tax[i] = 0;
    	for (int i = 1; i <= n; i++) tax[rank[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];
    }
    
    void Sort() {
    	m = 75;
    	for (int i = 1; i <= n; i++) rank[i] = s[i] - '0' + 1, tp[i] = i;
    	cntsort();
    	for (int w = 1, p = 0; p < n; m = p, w <<= 1) {
    		p = 0;
    		for (int i = 1; i <= w; i++) tp[++p] = n - w + i;
    		for (int i = 1; i <= n; i++) if(sa[i] > w) tp[++p] = sa[i] - w;
    		cntsort();
    		swap(tp, rank);
    		rank[sa[1]] = p = 1;
    		for (int i = 2; i <= n; i++) {
    			rank[sa[i]] = (tp[sa[i - 1]] == tp[sa[i]] && tp[sa[i - 1] + w] == tp[sa[i] + w]) ? p : ++p;
    		}
    	}
    }
    
    int main() {
    	scanf("%s", s + 1);
    	n = strlen(s + 1);
    	Sort();
    	for(int i = 1; i <= n; i++) cout << sa[i] << ' ';
    	return 0;
    }
    

    Height数组

    我们通过求(SA)数组可以把所有后缀排序,那么排序之后有啥用呢??

    其实是为了快速的求出任意两个后缀的(lcp)长度

    我们记$Height[l] = (排名第)l-1(的后缀和排名第)l(的后缀的)lcp$长度

    (Height[l] = lcp(suf(SA[l-1], suf(SA[l]))))

    (Height[1])可以视作(0)

    假设(l=)后缀(suf(i))的排名,$r = (后缀)suf(j)(的排名(在此)l(不一定小于)r$,只是举例)

    那么有结论:

    • (lcp(suf(i),duf(j))=min(Height[l+1]…Height[r]))
    • 即两个后缀的(lcp=)它们排名区间中(Height)的最小值

    可以用数据结构维护(rmp)

    为什么可以这么理解呢?

    假设有三个字符串(s_1,s_2,s_3),且(s_1<s_2<s_3)(按(rank)排名得出)

    那么(lcp(s_1,s_3))就等于(min(lcp(s_1,s_2), lcp(s_2,s_3)))

    (详细证明需要画图……我真的懒)

    (lcp(s_1,s_3) >= min(lcp(s_1,s_2), lcp(s_2,s_3))=1)

    又有(s_1[l+1]!= s_3[l+1])

    求法

    那么如何快速求出(Height)数组呢?

    纯暴力(O(n^2))
    for i = 1 - N
    	l = rank[i]
    	j = sa[l - 1]
    	k = 0
    	while (s[i + k] ==s [j + k]): ++k
    	Height[l] = k
    

    (l = rank[i], r = rank[i-1])
    (Height[l] = lcp(suf(SA[l-1]), suf(i)))
    (Height[r] = 1cp(suf(SA[r-1]),suf(i-1)))

    有重要结论:
    (Height[l] >= Height[r] - 1)

    • (Height[r]>1),有(suf(SA[r-1]) < suf(SA[i-1]))
    • 去掉首个字符 (lcp(suf(SA[r-1]+1), suf(SA[i])) = Height[r] - 1)
    • (suf(SA[r-1]+1) < suf(SA[i]))
    • 由于$Height[1] (是)suf(i)(与排名紧挨着自己的后缀的)lcp$,有
    • (suf(SA[r-1]+1) <= suf(SA[1-1]) < suf(SA[i]))

    相近的(Height)会比较相似,比较远的会差别很大

    不恰当的例子:

    优化(O(n))

    利用(Height[rank[i]] >= Height[rank[i-1] ] - 1)
    优化暴力即可,复杂度(0(N))

    for i = 1 - N
    	j = sa[l - 1]
    	k = max(0, Height[rank[i - 1]] - 1)
    	while (s[i + k] == S[j+k]): ++k
    	Height[rank[i]] = k
    

    之后再用(st)表之类的维护(Height)(rmq)信息即可

  • 相关阅读:
    flare3d_Material3D_shader3D
    判断2个数组是否相等
    js文件下载几种请求方式,普通请求方式封装
    echarts上下柱形图
    已知数组中的一个元素,求其下标
    判断一个数组是否另一个数组的子集
    js中如何判断一个数组是另一个数组的子集
    高德地图
    php过滤和转义函数
    SQLServer表字段默认值相关信息的获取方法
  • 原文地址:https://www.cnblogs.com/loceaner/p/12400528.html
Copyright © 2011-2022 走看看