后缀数组学习笔记

定义

后缀

从字符串某个位置i到字符串末尾的子串,定义s的第i个字符为第一个元素的后缀为suf(i)。

后缀数组

把s的每一个后缀按照字典序排序,后缀数组sa[i]表示排名为i的后缀的起始位置的下标。rk[i]数组代表起始位置为i的后缀的排名。

rk[]和sa[] 是一一对应关系,互为逆运算,可以相互推导

倍增

倍增法求后缀数组是从字符串的第一个字符开始比较,然后每次倍增比较长度,直到完成。每次都递增两倍,所以总步骤一共只有 \(\log_2 n\) 次,非常高效。

优化

如果字符串很长,包含10000个字符,那么在最后一步,每个数字都有10000位,无法存储与排序。

方法是在每步操作后就对组合数字进行排序,用序号产生一个新数字,再用新数字进行下一步排序。

基数排序即可。

sa2

height数组

大概是SA里最核心,最有用的部分。

\(height[i]\) 表示 \(suf(sa[i])\)\(suf[sa[i -1]]\) 的最长公共前缀(LCP)。

性质:\(height[i] \ge height[rk[i - 1]]-1\)

lcp求解:\(lcp(i, j) = \min_{k =rk[i]+1}^{rk[j]} height[k]\)

代码

const int N = 1e6 + 7;
int sa[N], rk[N], c[N], x[N], y[N], height[N];

char s[N];
int n, m;

void get_sa() {
    for(int i = 1; i <= n; i ++) c[x[i] = s[i]] ++;
    for(int i = 2; i <= m; i ++) c[i] += c[i - 1];
    for(int i = n; i; i --) sa[c[x[i]] --] = i;
    for(int k = 1; k <= n; k *= 2) {
        int num = 0;
        for(int i = n - k + 1; i <= n; i ++) y[++ num] = i;
        for(int i = 1; i <= n; i ++) {
            if(sa[i] > k) y[++ num] = sa[i] - k;
        }
        for(int i = 1; i <= m; i ++) c[i] = 0;
        for(int i = 1; i <= n; i ++) c[x[i]] ++;
        for(int i = 2; i <= m; i ++) c[i] += c[i - 1];
        for(int i = n; i; i --) sa[c[x[y[i]]] --] = y[i], y[i] = 0;
        swap(x, y);
        num = 1, x[sa[1]] = 1;
        for(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;
    }
}
void get_height() {
    for(int i = 1; i <= n; i ++) rk[sa[i]] = i;
    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(i + k <= n && j + k <= n && s[i + k] == s[j + k]) k ++;
        height[rk[i]] = k;
    }
}

题目

主要是进行一些做题的记录,所以思路写的比较草,不是很详细。

P4051 [JSOI2007] 字符加密

把原串拼一个在后面,求出sa数组后,如果这个位置在原串里,那输出对应结尾的字符。

UVA1223 Editor

给定文本字符串 \(s\),查找文本字符串 \(s\) 中最长的子字符串,使得子字符串至少出现两次。

对于出现两次的子串,一定是后缀排序后相邻的两个的公共前缀,答案即为对 \(height\) 数组取最大值。

P2408 不同子串个数

不同字串的个数就是所有的字串减去相同的字串个数。总的字串个数为 \(\frac{n(n + 1)}{2}\) 。所有后缀产生的相同的前缀个数为 \(\sum_{1}^{n} height[rk[i]]\),相同的字串个数为 \(\sum_{1}^{n} height[rk[i]]\)。答案为 \(\frac{n(n+1)}{2} - \sum_{1}^{n} height[rk[i]]\)

CF802I Fake News (hard)

\(x\) 为该子串在这之前出现的次数。

\(Ans = \sum_{i = 1}^{n} \sum_{pre(sa_i)} 2\times x + 1 = \sum_{i=1}^{n}\sum_{pre(sa_i)} + 2\times\sum_{i=1}^{n}\sum_{pre(sa_i)}x = \frac{n(n+1)}{2}+\sum_{i=1}^{n} \sum_{pre(sa_i)}x\)

然后单调栈即可。

P3181 [HAOI2016] 找相同字符

把两个字符串中间加一个特殊字符合并在一起,求一遍sa和height数组。答案就是 \(\sum _{sa[i]<n+1<sa[j]} \min _{i\le k \le j} height[k] + \sum_{sa[j]<n+1<sa[i]} \min_{i\le k \le j} height[k]\)

P2178 [NOI2015] 品酒大会

好题,学到了。

求有多少对后缀的lcp 大于等于r。发现当限制条件为r的时候height数组一定会被一个小于r的分成若干个块。随着r的变小,块会向外扩,可以理解为并查集操作。

P1117 [NOI2016] 优秀的拆分

\(a_i\) 为从1到 \(i\) ,有多少个形如AA。\(b_i\) 为从 \(i\) 到1,有多少个形如BB。\(Ans=\sum_{i=1}^{n-1} a[i]*b[i+1]\)

\(O(n^2)\)​ 有95 赚麻了/

很厉害题。

挂个题解链https://www.cnblogs.com/acfunction/p/10087144.html

P3975 [TJOI2015] 弦论

给定字符串,求字符串的第 \(k\) 小字串。同时每次给 \(t\) ,若 \(t=0\) 表示不同位置相同子串算一个,否则算作多个。

posted @ 2024-04-17 10:27  你说得太对辣  阅读(13)  评论(0编辑  收藏  举报