加载中...

后缀数组SA

相关链接

blog1

模板

struct SuffixArray{
    int n, m;
    string s;
    vector<int> x, y, c, sa, rk, height;

    explicit SuffixArray(string copy_s){
        s = copy_s;
        init();
        get_sa();
        get_height();
    };

    void init(){
        n = s.size();
        s = " " + s;
        m = 500;
        x.resize(2 * n + 1);
        y.resize(2 * n + 1);
        c.resize(max(m, n) + 1);
        sa.resize(n + 1);
        rk.resize(n + 1);
        height.resize(n + 1);
    }
    void get_sa(){ // 倍增法+桶排序 O(nlogn) 求sa[]
        int i, j, k;
        // 初始化,按第一个字母排序
        for(i = 1; i <= n; i ++) c[x[i] = s[i]] ++;
        for(i = 1; i <= m; i ++) c[i] += c[i - 1];
        for(i = n; i >= 1; i --) sa[c[x[i]] --] = i;
        // 初始时的 sa 仅是将所有后缀按照第一个字母的字典序升序排序得到的排名,不同后缀的排名可能相同
        for(k = 1; k <= n; k<<=1){ // 倍增,每次执行完当前轮后得到所有后缀按 长度为2^k的前缀(后面若有缺失的部分用空字符填补) 的字典序排序后的 sa   
            
            // 按第二关键字排序
            for(i = 0; i <= m; i ++) c[i] = 0;
            for(i = 1; i <= n; i ++) y[i] = sa[i];
            for(i = 1; i <= n; i ++) c[x[y[i] + k]] ++;
            for(i = 1; i <= m; i ++) c[i] += c[i - 1];
            for(i = n; i >= 1; i --) sa[c[x[y[i] + k]] --] = y[i];
            
            // 按第一关键字排序
            for(i = 0; i <= m; i ++) c[i] = 0;
            for(i = 1; i <= n; i ++) y[i] = sa[i];
            for(i = 1; i <= n; i ++) c[x[y[i]]] ++;
            for(i = 1; i <= m; i ++) c[i] += c[i - 1];
            for(i = n; i >= 1; i --) sa[c[x[y[i]]] --] = y[i];

            // 把后缀放入桶数组
            for(i = 1; i <= n; i ++) y[i] = x[i];
            for(m = 0, i = 1; i <= n; i ++){
                if(y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) x[sa[i]] = m;
                else x[sa[i]] = ++m;
            }
            if(m == n) break; // 所有后缀排名均不相同时,说明已经得到了正确的 sa[],可以提前退出
        }

        // rk[sa[i]] = i
        for(int i = 1; i <= n; i ++) rk[sa[i]] = i;
    }

    void get_height(){ // O(n) 得到 height[]
        // 利用性质:height[rk[i]] >= height[rk[i - 1]] - 1
        for(int i = 1, k = 0; i <= n; i ++){
            if(rk[i] == 1) continue;
            if(k > 0) 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;
        }
    }
};

P2408

Q:给定一个字符串,求不同的子串个数。

答案为 所有子串个数 - \(\sum height_{i}\)

证明:对本质不同的子串计数,核心在于保证无重复:对于每种子串,一定出现在排名连续的若干个后缀中,我们可以只在排名值最大的后缀累计贡献

而对于任何一种子串,它会出现在这些后缀中任意两个排名相邻后缀的 lcp 中,进而对排名更高的后缀的 height 产生一次贡献。因此我们可以通过减掉这些后缀的所有 height,来消除这种子串的冗余贡献(只会剩下一次)。进而得证上述结论。

code

P4051

破环成链再跑 SA

code

P2852

Q:求一个字符串中 至少出现 \(k\) 次的子串 的最大长度。

显然可以二分答案 + 字符串 hash 解决,这里写一下后缀数组 SA 的做法。

在 SA 中,一对后缀的排名越相近,它们的相似度就越高,重合前缀的长度 (即 lcp)就越长。

而每个子串,可以看作是某个后缀的一个前缀。

因此,对于重复出现的子串,它们必然在一组排名 (rk) 连续的后缀中。

对于 height:

\[height_{i} = lcp(sa_{i}, sa_{i-1}) \]

即排名为 \(i\) 的后缀与排名为 \(i-1\) 的后缀的最长公共前缀长度。

显然 lcp 函数是具有嵌套性质的,即:

\[lcp(sa_{i}, sa_{i+1}, sa_{i+2}) = \min(lcp(sa_{i}, sa_{i+1}), lcp(sa_{i+1}, sa_{i+2})) = \min(height_{i+1}, height_{i+2}) \]

因此求在原串中至少出现 \(k\) 次的子串的最大长度,本质上就是求 height 数组内 所有长度为 \(k - 1\) 的连续段内的最小值中的最大值,这可以通过对 height 数组做单调队列来解决。

code

P4341

Q:给定一个长度为 \(n\) 的 01 串,求所有出现次数 \(>1\) 的子串的出现次数,按照子串的字典序升序输出答案。\(n \le 3000\)

正常做法应该是 \(O(n^{2})\) 枚举所有子串建字典树,在每个子串的结尾位置记录 \(cnt\),然后先序遍历字典树(相当于按照字典序升序遍历所有可能的子串),找所有 \(cnt\)\(>1\) 的子串即可。

这里写一种 SA 做法:由于只需要子串的出现次数 \(>1\),因此我们只需要找每一对排名相邻的后缀,并对它们的 \(lcp\) 的所有前缀计数就可以了。但是需要注意同一个后缀可能会重复找两次,需要注意去重。

code

P8023

Q:给定两个字符串,可以按照队列的方式取出任意一个字符串的首字符,最终合成一个字符串,求字典序最小的合成字符串。

每次贪心取出 当前两个字符串中字典序较小的字符串 的首字符,不断重复该过程直至取完所有字符。但是需要注意一个特判:当字典序较小的字符串恰好是字典序较大的字符串的前缀时,改为取字典序较大的字符串的首字符

将两个字符串拼接在一起然后做 SA,为了实现特判需要提前在两个字符串结尾添加一个无穷大,然后再做拼接。然后每次取出某个字符串的首字符后,我们都会得到当前两个字符串的排名,排名的相对大小等价于字典序的相对大小,因此省去了暴力比较两个字符串字典序的过程,实现了优化。

code

P4070

Q:初始时字符串为空,多次操作,每次操作往字符串结尾添加一个字符,每次操作后求当前字符串的不同子串个数。

如果正序处理字符串,那么所有后缀都会发生变化,关于后缀数组 SA 的所有信息都会无规律地变化,这显然不是我们想看到的。而若倒序处理字符串,则每次相当于往开头加一个字符,所有原后缀都不会改变,只会添加一个新的后缀。又因为将字符串倒序不会改变本质不同子串的个数,因此我们不妨将整个字符串倒序,然后每次往开头加字符,尝试动态且高效地维护 height 数组。

由于 lcp 的嵌套性质,我们会发现一个结论:对于排名为 \(i\) 与排名为 \(j\) 的后缀,它们的 lcp 长度为:

\[\min_{k=i+1}^{j} height[k] \]

在往开头加字符的过程中,每次都会产生一个新的后缀,且该后缀的排名已经通过 SA 预处理好了。考虑当前所有后缀的排名的有序集合,当前字符串的 height 数组显然由该有序集合离散化后排名相邻的一对后缀的 lcp 长度组成。

于是,加入一个后缀,height 数组会分别加入该后缀与原后缀集合中排名最相近(一小一大)的两个后缀的 lcp 长度,然后再减去这两个原后缀的 lcp 长度。这便是每次加入新后缀时整个 height 数组的变化,显然根据上述结论,height 数组的总和是可以利用 ST表维护 height 最小值 \(O(1)\) 维护的。

code

P2178

题意较繁琐,但与下一道题很类似,均需按照 height 值降序枚举,然后利用并查集维护区间集合。

code

CF1780G

Q:给一个字符串,求满足以下条件的所有子串的数量:该子串在原串中的出现次数是该子串长度的倍数。\(n \le 10^{6}\)

显然能想到枚举子串长度,然后调和级数枚举子串出现次数,复杂度 \(O(n\log n)\) 特别合理,但如何统计这样的子串是难点。

对原串求 SA 后,得到 height 数组。由 lcp 的嵌套性质可知,\(min_{i=l+1}^{r} \space height_{i}\) 表示排名在 \([l, r]\) 的所有后缀的 lcp 长度。

那么,若某个长度为 \(l\) 的子串在原串中出现恰好 \(k\) 次,则说明:在 height 数组中,一定存在一个长度恰好为 \(k-1\) 的区间(\(k-1\) 个连续的 height 对应 \(k\) 个排名连续的后缀),该区间内 height 的最小值 \(\geq l\)(注意该区间长度必须恰好为 \(k\),不能向外延展,也就是左右边界两侧的 height 值不存在或者 \(< l\))。

因此,对于长度为 \(l\) 的任意子串,我们只需要枚举它的出现次数 \(q * l\),然后在 height 数组中找长度恰好为 \(q*l-1\) 的区间,使得该区间的最小值 \(\ge l\)。设这样的区间数量为 \(cnt_{q*l-1}\),则答案累计 \(cnt_{q*l-1} * (q*l)\)

于是,问题变成了如何维护关于 height 数组的区间长度桶 \(cnt\)。由于有效的 height 值必须 \(\ge\) 当前枚举的长度 \(l\),因此不难发现,升序枚举长度相当于删点,降序枚举长度相当于加点。而显然加点是比较好维护的:只需要一个并查集维护相邻区间的合并就行了。具体实现见 code。

code

P4248

Q:给定一个长度为 \(n\) 的字符串,令 \(T_{i}\) 表示它从第 \(i\) 个字符开始的后缀,求:

\[\sum_{1\le i<j \le n} len(T_{i}) + len(T_{j}) - 2 * lcp(T_{i}, T_{j}) \]

前面两个增量的贡献为定值 \(\frac{n(n+1)(n-1)}{2}\),问题等价于求:

\[\sum_{1\le i<j \le n} lcp(T_{i}, T_{j}) \]

\(height\) 数组的性质:\(lcp(s_{i\backsim n}, s_{j\backsim n}) = \min(height_{i+1 \backsim j})\),我们可以将计算上式转化为每个 \(height_{i}\) 作为最小值时的子区间数量之和,可以单调栈 \(O(n)\) 解决。具体细节见 code。

code

posted @ 2026-03-31 16:25  小橘奏  阅读(27)  评论(0)    收藏  举报