zjp-shadow

前言 :Orz ShichengXiao 冬令营的时候就早解决了

字符串算法还是不能随意放弃啊 要认真学了!!

这个算法常用于解决字符串上的 \(\mathrm{LCP}\) 问题 和 一些字符串匹配的问题

这个算法思维难度不是很大 但是代码难度还是有一些的

想学好这个算法 一定要牢牢的记住各个数组的含义 不然容易弄混

  • 还是先简单介绍一下原理吧 :

后缀数组就是将一个字符串的后缀全部进行排序 然后把下标存入一些数组里

用那些数组来进行字符串的一些常用操作

为了后缀排序 我们常常使用 \(O(n \log n)\) 的倍增算法

(而不用 \(O(n)\)\(\mathrm{DC3}\) 因为它常数和空间大,并且十分不好写)

那接下来介绍一下倍增算法qwq

考虑这样一个小问题 我们比较任意两后缀的字典序大小 有没有什么快速比较的方法?

当然有 就是预处理出他们的一个前缀和后缀的大小关系 然后我们就能用另外两个来比较了。

倍增的思路大概就是如此 我们从小到大 每次长度乘二 排序长度是那些的后缀 然后用之前得到的信息去比较就行了。

具体就是变成双关键字排序,第一关键字就是前半部分的排名,第二关键字就是后半部分的排名。

这个东西套上基数排序就能优化一个 \(\log\) 。(基数排序具体见网上讲解吧qwq)

基数排序:我理解的就是 类似于桶排 我们开个桶来标记一下他们出现的次数

然后几个前缀和 那么这个数组的值就是他的排名 (可以模拟一下)

然后要满足双关键字 那么我们如果有几个元素第一关键字相同 它们在同一个下标上

那么我们就使第二关键字较大的先获得排名 其他的排名就比他小了。

具体实现见程序中的 Radix_Sort 就行了。

然后大概原理就是这样咯qwq (没讲清的话。。请去看看 刘汝佳的《算法竞赛入门经典》

然后我介绍一下等下要出没的数组含义(很重要!!)

  1. \(str[i]\) 表示 \(i\) 这个位置上的字符;
  2. \(sa[i]\) 表示 后缀排序后第 \(i\) 名后缀的起始点下标;
  3. \(rk[i]\) 表示 \(i\) 为起始点下标的后缀的排名;
  4. \(tmp[i]\) 表示 \(i\) 的第二关键字排名 (在 \(swap\) 后作为第一关键字)
  5. \(c[i]\) 表示在基数排序中在字符集中编号为 \(i\) 的出现次数(后面作为前缀和)、
  6. 还有两个变量 : \(n\) 为字符串长度 \(m\) 为字符集大小

然后这样就可以直接做后缀排序了

代码中有详细解释 可以看看

int sa[N], tmp[N], c[N], rk[N], m, n;
char str[N];

inline void Radix_Sort() {
    For (i, 1, m) c[i] = 0; //清空
    For (i, 1, n) ++ c[rk[i]]; //先标记一下 此时 rk 是第一关键字
    For (i, 1, m) c[i] += c[i - 1]; //记前缀和
    Fordown(i, n, 1) sa[c[rk[tmp[i]]] --] = tmp[i]; //得到当前的 sa 数组,第二关键字 tmp 大的先得到更大的排名
}

inline void Build_Sa() {
    For (i, 1, n) rk[i] = str[i], tmp[i] = i;
    m = 255; Radix_Sort(); //简单的初始化
    for (register int k = 1, p; k <= n; k <<= 1) { //开始倍增
        p = 0;
        For (i, n - k + 1, n) tmp[++ p] = i; //后 k 个的第二关键字为空 所以是最大的
        For (i, 1, n) if (sa[i] > k) tmp[++ p] = sa[i] - k; //前面的话 只有 sa > k 的 sa 作为它前第 k 个第二关键字
        Radix_Sort(); swap(rk, tmp); //基数排序,然后交换两个关键字
        rk[sa[1]] = 1, m = 1; //初始化
        For (i, 2, n) rk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + k] == tmp[sa[i - 1] + k]) ? m : ++ m; //重新得到 rk 双关键字相同的 rk 一样
        if (m >= n) return ; //如果当前排名两两不同就可以终止了
    }
}

有了这个其实我们搞不了太大的新闻

所以我们还有一个神奇的数组 能做一些事情

\(height[i]\) 定义为 \(sa[i-1]\)\(sa[i]\) 的最长公共前缀 \(\mathrm{(LCP)}\) 的长度

为什么这个有用呢? 因为有这样一个结论。

对于两个后缀 \(j\)\(k\) ,不妨设 \(rank[j]<rank[k]\) ,则不难证明后缀 \(j\)\(k\)\(\mathrm{LCP}\) 的长度等于

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

这个结论很显然 画图自己比对一下。

然后如何求这个数组呢 暴力求显然是 \(O(n^2)\) 的 不满足要求啦

我们又有这样一个结论

\(height[rank[i]] \ge height[rank[i]-1]-1\)

这个证明的话我们如此考虑即可

设排在后缀 \(i-1\) 前一个的是后缀 \(k\) 。后缀 \(k\) 和后缀 \(i-1\) 分别得到后缀 \(k+1\) 和 后缀 \(i\)

因此后缀 \(k+1\) 一定排在后缀 \(i\) 前面(这是因为后缀 \(k\) 的排名比后缀 \(i-1\) 要高),

并且最长公共前缀长度为 \(height[rank[i]-1]-1\) (考虑后缀 \(k\) 与 后缀 \(k-1\)\(\mathrm{LCP}\)

可以自己手动举例子来理解这个东西qwq

用这个结论去构建的话 总复杂度就是 \(O(n)\) 了 如何考虑呢

每次长度最多 \(-1\) 然后 \(height\) 最多就到 \(n\) 那复杂度就是这样了。

然后代码就很好写了

int height[N];
inline void Build_Height() {
    for (register int i = 1, j, k = 0; i <= n; ++ i) {
        if (k) -- k; //前一个减1
        j = sa[rk[i] - 1];
        while (str[i + k] == str[j + k]) ++ k; //和前一个去比较
        height[rk[i]] = k;
    }
}

说了这么多 也要一些例题啊qwq

  1. 洛谷P3809【模板】后缀排序

和标题一样 就是模板题。。。。

可以用二分哈希比较 \(\mathrm{LCP}\) 然后去排序 这样复杂度 是 \(O(n \log^2 n)\) 的 跑了 7000ms 后缀数组 1000ms....

  1. BZOJ : 1717: [Usaco2006 Dec]Milk Patterns 产奶的模式

这道题也类似一道模板题

给你一个字符串,求至少出现 \(k\) 次的串的最长长度。

这个我们就直接二分答案 \(\mathrm{ans}\) 然后看是否存在一段 连续的 \(height\) 长度 \(\ge k\) 且 值都大于 \(\mathrm{ans}\) 就行了。

/**************************************************************
    Problem: 1717
    User: DOFY (用大佬号子交题233)
    Language: C++
    Result: Accepted
    Time:84 ms
    Memory:5592 kb
****************************************************************/
 
#include <bits/stdc++.h>
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
using namespace std;
 
inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}
 
inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * fh;
}
 
void File() {
#ifdef zjp_shadow
    freopen ("P2852.in", "r", stdin);
    freopen ("P2852.out", "w", stdout);
#endif
}
 
const int N = 20010;
 
int n, k;
 
int str[N];
int m, sa[N], rk[N], tmp[N], height[N], c[1001000];
 
void Radix_Sort() {
    For (i, 1, m) c[i] = 0;
    For (i, 1, n) ++ c[rk[i]];
    For (i, 1, m) c[i] += c[i - 1];
    Fordown (i, n, 1) 
        sa[c[rk[tmp[i]]] --] = tmp[i];
}
 
int maxv;
void Build_Sa() {
    For (i, 1, n) rk[i] = str[i], tmp[i] = i;
    m = maxv; Radix_Sort();
    for (int k = 1, p; k <= n; k <<= 1) {
        p = 0;
        For (i, n - k + 1, n) tmp[++ p] = i;
        For (i, 1, n) if (sa[i] > k) tmp[++ p] = sa[i] - k;
        Radix_Sort(); swap(rk, tmp);
        rk[sa[1]] = 1, m = 1;
        For (i, 2, n)
            rk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + k] == tmp[sa[i - 1] + k]) ? m : ++ m;
        if (m >= n) break;
    }
}
 
void Build_Height() {
    for (register int i = 1, j, k = 0; i <= n; ++ i) {
        if (k) -- k;
        j = sa[rk[i] - 1];
        while (str[i + k] == str[j + k]) ++ k;
        height[rk[i]] = k;
    }
}
 
int Last, ans = 0;
 
inline bool Check(int val) {
    int cnt = 1;
    For (i, 1, n) {
        if (height[i] >= val) ++ cnt;
        else cnt = 1;
        if (cnt >= k) return true;
    }
    return false;
}
 
int main () {
    File();
    n = read(); k = read();
    For (i, 1, n) {
        str[i] = read();
        chkmax(maxv, str[i]);
    }
    Build_Sa();
    Build_Height();
    int l = 1, r = n;
    while (l <= r) {
        int mid = (l + r) >> 1;
        if (Check(mid)) ans = mid, l = mid + 1;
        else r = mid - 1;
    }
    printf ("%d\n", ans);
    return 0;
}
  1. BZOJ : 4566: [Haoi2016]找相同字符

给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数。

两个方案不同当且仅当这两个子串中有一个位置不同。

这题有些难度。。。

但思路还是很简单?

就是将两个字符串先拼起来 然后再进行后缀排序

对于每个位置 统计 \(rk\) 前面和它 \(height\) 有贡献的 且不是同一个串的贡献

统计两遍 一个是后面为 \(a\) 串,一个是 \(b\) 串。

然后这个直接用单调栈维护就行了qwq

至于如何维护 考虑前连续一段的 \(height\) 的贡献就行了 因为 \(height\) 连续一段的 \(\min\) 是单调递减的

\(cnt,tot\) 统计前面出现了不同串的个数 \(cal\) 存的答案 \(sta\) 统计的是当前的 \(height\) 。。。

/**************************************************************
    Problem: 4566
    User: zjp_shadow
    Language: C++
    Result: Accepted
    Time:5108 ms
    Memory:50516 kb
****************************************************************/
 
#include <bits/stdc++.h>
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
using namespace std;
 
inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}
 
inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * fh;
}
 
void File() {
#ifdef zjp_shadow
    freopen ("P3818.in", "r", stdin);
    freopen ("P3818.out", "w", stdout);
#endif
}
 
const int N = 800100;
 
int c[N], rk[N], sa[N], tmp[N], n, m;
void Radix_Sort() {
    For (i, 1, m) c[i] = 0;
    For (i, 1, n) ++ c[rk[i]];
    For (i, 1, m) c[i] += c[i - 1];
    Fordown (i, n, 1) sa[c[rk[tmp[i]]] -- ] = tmp[i];
}
 
char str[N];
void Build_Sa() {
    For (i, 1, n) rk[i] = str[i], tmp[i] = i;
    m = 255; Radix_Sort();
    for (register int k = 1, p; k <= n; k <<= 1) {
        p = 0;
        For (i, n - k + 1, n) tmp[++ p] = i;
        For (i, 1, n) if (sa[i] > k) tmp[++ p] = sa[i] - k;
        Radix_Sort(); swap(rk, tmp);
        rk[sa[1]] = 1, m = 1;
        For (i, 2, n)
            rk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + k] == tmp[sa[i - 1] + k]) ? m : ++ m;
        if (m >= n) return;
    }
}
 
int height[N];
void Get_Height() {
    for (register int i = 1, j, k = 0; i <= n; ++ i) {
        if (k) -- k;
        j = sa[rk[i] - 1];
        while (str[i + k] == str[j + k]) ++ k;
        height[rk[i]] = k;
    }
}
 
char str1[N], str2[N];
 
int suma[N], sumb[N];
int l[N], r[N];
 
typedef long long ll;
ll ans, cnt[N], cal[N], top, sta[N];
 
int main () {
    File();
    scanf ("%s", str1 + 1); int len1 = strlen(str1 + 1);
    scanf ("%s", str2 + 1); int len2 = strlen(str2 + 1);
    n = len1 + len2 + 1;
    For (i, 1, n) {
        if (i <= len1) str[i] = str1[i];
        else if (i > len1 + 1) str[i] = str2[i - len1 - 1];
        else str[i] = 'X';
    }
 
    Build_Sa();
    Get_Height();
    For (i, 1, n) {
        suma[i] += suma[i - 1];
        sumb[i] += sumb[i - 1];
        if (sa[i] <= len1) ++ suma[i];
        else if (sa[i] > len1 + 1) ++ sumb[i];
    }
 
    For (i, 1, n) {
        if (!height[i]) { top = 0; continue ; }
        int tot = sa[i - 1] <= len1 ? 1 : 0, val = height[i];
        while (top && sta[top] > val) tot += cnt[top --];
        if (tot) sta[++ top] = val, cnt[top] = tot;
        cal[top] = cal[top - 1] + sta[top] * cnt[top];
        if (sa[i] > len1 + 1) ans += cal[top];
    }
     
    top = 0;
    For (i, 1, n) {
        if (!height[i]) { top = 0; continue ; }
        int tot = sa[i - 1] > len1 + 1 ? 1 : 0, val = height[i];
        while (top && sta[top] > val) tot += cnt[top --];
        if (tot) sta[++ top] = val, cnt[top] = tot;
        cal[top] = cal[top - 1] + sta[top] * cnt[top];
        if (sa[i] <= len1) ans += cal[top];
    }
    printf ("%lld\n", ans);
    return 0;
}

相关文章: