zoukankan      html  css  js  c++  java
  • 后缀数组

    后缀数组主要是两个数组:(sa)(rk)

    其中,(sa[i])表示将所有后缀排序后第(i)小的后缀的编号,(rk[i])表示后缀(i)的排名

    这两个数组满足性质:(sa[rk[i]] = ra[sa[i]] = i)

    后缀数组示例:

    后缀数组的求法

    (O(n^2logn))做法:

    这个做法大家都会,暴力嘛,用(string + sort)就可以了

    (O(nlog^2n))做法:

    这个做法用到了倍增的思想

    先对每个字符(长度为1的子串)进行排序

    假设我们已经知道了长度为(w)的子串的排名(rk_w[1…n])(即,(rk_w[i])表示(s[i…min(i + w - 1,n)])({s[x…min(x +w - 1,n)|xin[1,n]})中的排名),那么以(rk_w[i])为第一关键字,(rk_w[i+w])为第二关键字(若(i+w>n)则令(rk_w[i+w])为无穷小)进行排序,就可以求出(rk_2w[1…n])

    倍增排序示意图:

    如果用(sort)进行排序,复杂度就是(O(nlog^2n))

    #define B cout << "BreakPoint" << endl;
    #define O(x) cout << #x << " " << x << endl;
    #define O_(x) cout << #x << " " << x << " ";
    #define Msz(x) cout << "Sizeof " << #x << " " << sizeof(x)/1024/1024 << " MB" << endl;
    #include<cstdio>
    #include<cmath>
    #include<iostream>
    #include<cstring>
    #include<algorithm>
    #include<queue>
    #include<set>
    #define LL long long
    const int inf = 1e9 + 9;
    const int N = 2e5 + 5;
    using namespace std;
    inline int read() {
    	int s = 0,w = 1;
    	char ch = getchar();
    	while(ch < '0' || ch > '9') {
    		if(ch == '-')
    			w = -1;
    		ch = getchar();
    	}
    	while(ch >= '0' && ch <= '9') {
    		s = s * 10 + ch - '0';
    		ch = getchar();
    	}
    	return s * w;
    }
    char s[N];
    int n,w,p,sa[N],rk[N << 1],oldrk[N << 1];
    // 为了防止访问 rk[i+w] 导致数组越界,开两倍数组。
    // 当然也可以在访问前判断是否越界,但直接开两倍数组方便一些。
    int main() {
    	scanf("%s", s + 1);
      	n = strlen(s + 1);
      	for(int i = 1; i <= n; i++) rk[i] = s[i];
      	for(w = 1; w < n; w <<= 1) {
        	for(int i = 1; i <= n; i++) sa[i] = i;
        	sort(sa + 1, sa + n + 1,[](int x, int y) {return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];});
        	memcpy(oldrk, rk, sizeof(rk));
        	// 由于计算 rk 的时候原来的 rk 会被覆盖,要先复制一份
        	for(int i = 1;i <= n;i++) {
        		p = 0;
          		if(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w])  rk[sa[i]] = p; 
    	  		else rk[sa[i]] = ++p;  // 若两个子串相同,它们对应的 rk 也需要相同,所以要去重
        	}
      	}
      	for(int i = 1; i <= n; i++) printf("%d ", sa[i]);
      	return 0;
    }
    
    

    (O(nlogn))做法

    (O(nlog^2n))做法中,单次排序是(O(nlogn))的,如果能(O(n))排序即可

    前置芝士:基数排序,计数排序

    然而我也不会这个做法。

    就这地吧

    后缀数组

    寻找最小的循环移动位置

    将字符串(s)复制一份变成(ss)就转化成了后缀排序问题

    例题: JSOI2007」字符加密

    在字符串中找子串

    任务 在线地在主串(T)中寻找模式串(S)。在线的意思是,我们已经预先知道主串(T),但是当且晋档询问时才知道模式串(S)。我们可以先构造出(T)的后缀数组,然后查找子串(S)。若子串(S)(T)中出现,它必定是(T)的一些后缀的前缀。因为我们已将所有后缀排序了,我们额可以在通过(p)数组中二分(S)来实现。比较子串(S)和当前后缀的时间复杂度为(O(|S|)),因此找子串的时间复杂度为(O(|s|log|T|))。注意,如果该子串在(T)中国出现了多次,每次出现都是在(p)数组中相邻的。因此出现次数可以通过再次二分找到,输出每次出现的位置也很轻松。

    从字符串首位取字符最小化字典序

    例题: 「USACO07DEC」Best Cow Line

    题意:给你一个字符串,每次从首或尾取一个字符组成字符串,问所有能够组成的字符串中字典序最小的一个。

    暴力做法就是每次最坏(O(n))地判断当前应该取首还是尾(即比较取首得到的字符串与取尾得到的反串的大小),只需优化这一判断过程即可。

    由于需要在原串后缀与反串后缀构成的集合内比较大小,可以将反串拼接在原串后,并在中间加上一个没出现过的字符(如 #,代码中可以直接使用空字符),求后缀数组,即可(O(1))完成这一判断。

    height数组

    LCP(最长公共前缀)

    两个字符串(S)(T)的LCP就是最大的(x)(x leq min(|S|,|T|)))使得(S_i = T_i(forall1leq ileq x))

    (lcp(i,j))表示后缀(i)和后缀(j)的最长公共前缀(的长度)

    height数组的定义

    (height[i] = lcp(sa[i],sa[i-1])),即第(i)名的后缀与它前一名的后缀的最长公共前缀。

    (height[1])可以视作0

    (O(n))(height)数组需要一个引理

    (height[rk[i]] ge height[rk[i - 1]] - 1)

    证明:

    (height[rk[i-1]]leq 1)时,上式显然成立(右边小于等于0)

    (height[rk[i - 1]]>1)时:

    设后缀(i-1)(aAD)(A)是一个长度为(height[rk[i-1]]-1)的字符串),那么后缀(i)就是(AD)。设后缀(sa[rk[i-1]-1])(aAB),那么(lcp(i - 1,sa[rk[i-1]-1])=aA)。由于后缀(sa[rk[i-1]-1]+1)(AB),一定飘在后缀(i)的前面,所后缀数组(sa[rk[i]-1])一定含有前缀(A),所以(lcp(i,sa[rk[i]-1]))至少是(height[rk[i-1]]-1)

    [i-1:AD\i:AD\sa[rk[i-1]-1]:aAb\sa[rk[i]-1]:A[B/C]\lcp(i,sa[rk[i]-1]):AX(X可能为空) ]

    (O(n))求height数组的代码实现

    利用上面这个引理暴力求:

    for(int i = 1, k = 0; i <= n; ++i) {
      if (k) --k;
      while(s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
      ht[rk[i]] = k;  // height太长了缩写为ht
    }
    

    (k)不会超过(n),最多减(n)次,所以最多加(2n)次,总复杂度就是(O(n))

    height数组的应用

    两子串最长公共前缀

    (lcp(sa[i],sa[j]) = min{ height[i+1…j]})

    感性理解:如果(height)一直大于某个数,前这么多为一直就没变过;反之,由于后缀已经排好序了,不可能变了之后变回来。

    有了这个定理,求两子串最长公共前缀就转化为了RMQ问题

    比较一个字符串的两个子串的大小关系

    假设需要比较的是(A=S[a..b])(B=S[c..d])的大小关系。

    (lcp(a,c) ge min(|A|,|B|),A<B iff |A| < |B|)

    否则,(A<B iff rk[a] < rk[b])

    不同子串的数目

    子串就是后缀的前缀,所以可以每个后缀,计算前缀总数,再减掉重复。

    “前缀总数”其实就是子串个数,为(n(n+1)/2)

    如果按后缀排序的顺序枚举后缀,每次新增的子串就是除了与上一个后缀的LCP剩下的前缀。这些前缀一定是新增的,否则会破坏(lcp(sa[i],s[j])=min{ height[i+1..j]})的性质。只有这些前缀是新增的,因为LCP部分在枚举上一个前缀时计算过了

    所以答案为

    [frac{n(n+1)}{2}-sum_{i=2}^nheight[i] ]

  • 相关阅读:
    css中的背景、渐变 文本格式化和表格的常用属性
    HTML5中常见的英文单词
    matlab文件处理
    优先级队列
    编程珠玑(一)
    排序算法之希尔排序
    自己写的矩阵类Matrix
    排序算法之快速排序
    Thoughtworks公司面试题——MARS ROVERS问题
    matlab画图
  • 原文地址:https://www.cnblogs.com/excellent-zzy/p/12354689.html
Copyright © 2011-2022 走看看