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

    后缀数组

    先下几个常见的定义

    (s(i, j))表示([i, j])形成的连续子串

    (suf[i])表示以(i)为开头的后缀

    (rank)数组:(rank[i])表示将(1sim n)的后缀排序后,(suf[i])的排名

    (sa)数组:(sa[i])表示将(1 sim n)的后缀排序后,排第(i)的在哪里

    举个例子:

    sort(a + 1, a + n + 1, cmp);
    

    当我们采用上述代码之后,(a)数组存下的实际就是排第(i)的在哪里


    (sa)数组不会相同,因为后缀的长度互不相同

    在假想状态下,我们考虑在字符串的某尾加上无限个(0)

    这样子,我们可以使得两个后缀具有相同的长度,便于比较

    比如字符串"(abaa)"

    我们实际上是在比较这4个字符串的排名:

    (abaa,baa0,aa00,a000)


    先看一个有趣的事情:
    对于字符串(S = S_1 + S_2, T = T_1 + T_2),且(|S_1| = |S_2|, |T_1| = |T_2|)
    如果(S_1 < T_1),那么(S < T)
    如果(S_1 = T_1;and;S_2 < T_2),那么(S < T)


    这要怎么利用呢?

    也就是说,我们现在把所有的后缀都看做是长度为(n)的字符串

    我们先处理出所有的(s(i, i))的字典序排名,如果不存在(s(i, i) = s(j, j)),那么我们的序排好了

    否则,我们可以利用所有的(s(i, i)),按照上面的排序方式,得出所有的(s(i, i + 2^1 - 1))的字典序排名

    同样,如果不存在(s(i, i + 2^1 - 1) = s(j, j + 2^1 - 1)),那么我们的序就排好了

    否则,合并出(s(i, i + 2^2 - 1)),然后再去判断

    依次类推

    当我们合并到(s(i, i + 2^k - 1);(2^k geq n))时,我们一定能判断出字典序排名

    如果合并的时候,我们的复杂度可以做到(O(n log n)),那么总体而言,就能做到(O(n log ^2 n))

    如果合并的时候,我们能做到(O(n)),那么总体而言,就能做到(O(n log n))

    1542304416806

    可以对着上面这张经典的图理解一下


    在描述(O(n log n))的鬼畜写法之前,我们先来看看(O(n log^2 n))的写法

    inline bool cmp(int x, int y) { return P[x] < P[y]; }
    inline void Suffix_sort() {
        for(int i = 1; i <= n; i ++) sa[i] = i;
        for(int i = 1; i <= n; i ++) rk[i] = s[i];
        //初始化sa和rank
        for(int k = 1; k <= n; k <<= 1) {
            //倍增
            for(int i = 1; i <= n; i ++) 
                P[i] = make_pair(rk[i], rk[i + k]);
            //通过stl的pair+sort来实现双关键字排序
            sort(sa + 1, sa + n + 1, cmp);
            int tmp = 1; rk[sa[1]] = 1;
            for(int i = 2; i <= n; i ++)
                rk[sa[i]] = (P[sa[i]] == P[sa[i - 1]]) ? tmp : ++ tmp;
            //排完序之后,按照字典序的顺序对每个点重新计算rank
            if(tmp >= n) break;
            //如果当前的排名已经 >= n,代表不存在两个相同的量,也就是排完了
        }
    }
    

    (它在luogu上跑过了1000000)

    请确保你在明白了(O(n log^2 n))的写法后,再继续往下阅读

    我们只需要把上面的(sort)换成基数排序即可

    基数排序原理十分的好懂,请自行了解

    void sort(int *a, int n, int m) {
        for(int i = 1; i <= n; i ++) cnt[p2[i]] ++;
        for(int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
        for(int i = 1; i <= n; i ++) b[cnt[p2[i]] --] = i;
        //b用来暂时存储对第二关键字排完序之后的结果
        for(int i = 0; i <= m; i ++) cnt[i] = 0;
        for(int i = 1; i <= n; i ++) cnt[p1[i]] ++;
        for(int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
        for(int i = n; i >= 1; i --) a[cnt[p1[b[i]]] --] = b[i];
        //这里一定要倒叙枚举
        for(int i = 0; i <= m; i ++) cnt[i] = 0;
    }
    
    inline void Suffix_sort() {
        for(int i = 1; i <= n; i ++) sa[i] = i;
        for(int i = 1; i <= n; i ++) rk[i] = s[i];
        int m = 128; //m初始化为字符集的大小
        for(int k = 1; k <= n; k <<= 1) {
            for(int i = 1; i <= n; i ++) 
                p1[i] = rk[i], p2[i] = rk[i + k];
            sort(sa, n, m);
            int tmp = 1; rk[sa[1]] = 1;
            for(int i = 2; i <= n; i ++)
                rk[sa[i]] = 
                	(p1[sa[i]] == p1[sa[i - 1]] && p2[sa[i]] == p2[sa[i - 1]]) ? 
                		tmp : ++ tmp;
            if(tmp >= n) break;
            m = tmp;
        }
    }
    

    那么我们能不能进一步优化呢?

    当然是可以的,可以发现上面代码中的(rk, p1, p2)数组其实只需要保留两个即可

    并且,对第二关键字实际上并不需要基排,只需要调用上一次的(sa)数组就可以得到结果

    然而经过测试,发现在(10^6)的数据下只有(20ms)的常数差距

    因此实际上没有必要学习网上流传的写法...


    经过这么一段长长的文字,你终于懂了后缀数组,但是给后缀排序有啥用呢?

    我们需要引入一个更强大的数组

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

    (height)数组,表示(lcp(suf[sa[i]], suf[sa[i - 1]])),即字典序第(i)小和字典序第(i - 1)小的最长公共前缀


    一个十分重要的性质
    对于后缀(suf[i])(suf[j]),满足(lcp(i, j) = min(height[k]);(k in[rk[i] + 1, rk[j]]))


    考虑证明:

    首先证明,(lcp(i, j) leq min(height[k]);(k in[rk[i] + 1, rk[j]]))

    我们记(min(height[k]) = h)

    如果(lcp(i, j) > h),那么对于(k in [rk[i] + 1, rk[j]])而言

    我们记(lcp(i, j) = L)

    由于(k)的排名处于(i)(j)之间,并且(i)(j)的前(L)都相同,必然有(k)包含(L)这个前缀

    否则,(k)的排名由于在(L)之前就出现了不同,因此要么在(i)之前,要么在(j)之后

    因此,有(height[k] = L > h = min(height[k]))

    这不可能,因此(lcp(i, j) leq h)

    然后证明,可以取到上界

    这是因为(height[i] geq h),可以看做(suf[sa[i]])(suf[sa[i - 1]])至少有长为(h)的公共前缀

    公共前缀满足传递性,因此可以取到上界


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

    如果暴力的求解,复杂度显然是(O(n^2))

    我们可以很清楚的知道(suf[sa[i]])(suf[sa[i - 1]])在字符串构成的联系不如(suf[i])(suf[i - 1])的联系

    毕竟,(suf[i])(suf[i - 1])只相差了一个字符

    那么,我们直接按照下标顺序来计算的时候,可以发现


    我们记(h[i])表示(suf[i])和字典序排在它前面的最长公共前缀

    那么(h[i] geq h[i - 1] - 1)

    比如现在正在求(h[i]),排在(i)之前的后缀是(suf[j])

    后缀(i)(abc...),后缀(j)(aba...),那么(h[i] = 2)

    (h[i + 1])时,由于(h[i] geq 1;(h[i] = 0可以无视))

    因此,去掉首字母的后缀(i + 1)(bc...),后缀(j + 1)(ba...)

    可以发现,(j + 1)还是保持排在(i + 1)的前面,不妨设排在(i + 1)前面的后缀为(k)

    那么,一定有(h[i + 1] = lcp(i + 1, k) geq lcp(i + 1, j + 1) = h[i] - 1)


    和求后缀数组比起来,求(height)显得十分的简洁

    void Solve() {
        for(int i = 1, k = 0; i <= n; i ++) {
            if(k) k --; //此时的k相当于h[i],得到的新k相当于h[i] - 1
            int j = sa[rk[i] - 1];
            while(s[j + k] == s[i + k]) k ++;
            height[rk[i]] = k; // = h[i]
        }
    }
    

    由于每次求(h[i])时,(k)指针都只会(- 1),也就是增加(1)的势能

    而每次(k)往前挪移的时候,都需要(1)的势能

    那么,势能总量是(O(n))的,也就是求(height)数组只需要(O(n))的复杂度

  • 相关阅读:
    print 参数
    note
    action标签的属性说明
    Cannot load JDBC driver class 'oracle.jdbc.driver.OracleDriver'
    润乾报表
    javax.naming.NameNotFoundException: Name ZKING is not bound in this Context 的问题
    Myeclipse2013安装svn插件
    Myeclipse2013的优化设置
    Myeclipse解析.classpath文件
    Struts
  • 原文地址:https://www.cnblogs.com/reverymoon/p/9968778.html
Copyright © 2011-2022 走看看