后缀数组学习笔记

后缀数组挺好玩的,于是来写后缀数组学习笔记了。


一些规定:

字符串为 \(s\),字符串的长度为 \(n\),且下标从 \(1\) 标起。

\(s[l\ldots r]\) 表示字符串 \(s\) 的一个下标从 \(l\)\(\min(r,n)\) 的一个字串。

后缀 \(i\) 表示 \(s\) 中以第 \(i\) 个字符开头的后缀,即 \(s[i\ldots n]\)

\(\operatorname{lcp}(i,j)\) 表示后缀 \(i\) 和后缀 \(j\) 的最长公共前缀的长度。

什么是后缀数组?

后缀数组主要关系到 2 个数组:\(sa\)\(rk\)

  • \(sa[i]\) 表示将所有后缀按照字典序从小到大排序,排名第 \(i\) 的后缀为后缀 \(sa[i]\)

  • \(rk[i]\) 表示将所有后缀按照字典序从小到大排序,后缀 \(i\) 的排名为 \(rk[i]\)

这两个数组满足一个性质:\(sa[rk[i]]=rk[sa[i]]=i\)(请自行理解)。

后缀数组的求法:

\(O(n^2\log n)\) 的求法:

很显然可以将所有后缀取出来然后排序,然后容易求得 \(sa\) 数组和 \(rk\) 数组。

\(O(n\log^2 n)\) 的求法:

这个做法要用到倍增的思想。

我们设 \(sa_j[i]\)\(s[k,k+2^j-1](1\le k\le n)\) 中排名为 \(i\) 的字符串的开头。

我们可以先取出所有长度为 \(1\) 的子串,然后将其排序,可以求出 \(sa_0\) 数组和 \(rk_0\) 数组。

接下来假设我们已经求出了 \(sa_{i-1}\) 数组和 \(rk_{i-1}\) 数组,然后要求 \(sa_i\) 数组和 \(rk_i\) 数组。

对于长度为 \(2^i\) 的子串的比较,我们可以比较前长度为 \(2^{i-1}\) 的子串的字典序,在比较后长度为 \(2^{i-1}\) 的子串的字典序,将其变成双关键字排序,这样就可以利用 \(rk_{i-1}\) 数组求出 \(sa_i\) 数组,那么求出 \(rk_i\) 数组也很容易了。

代码如下:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 1e6 + 5;

int n, sa[kMaxN], rk[kMaxN * 2], oldrk[kMaxN * 2];  // 注意这里的 rk 要开 2 倍空间,否则在双关键字比较是会出问题
string s;

void SA(int n, string s, int sa[], int rk[]) {
  for (int i = 1; i <= n; i++) {
    sa[i] = i, rk[i] = s[i];  // 由于 sa_0[i] 无需用到,可以直接赋为 i。这里的 rk[i] 可以直接等于 s[i],自行体会
  }
  for (int w = 1; w < n; w <<= 1) {
    sort(sa + 1, sa + 1 + n, [&](int i, int j) { return rk[i] == rk[j] ? rk[i + w] < rk[j + w] : rk[i] < rk[j]; });  // 双关键字比较
    copy(rk, rk + 1 + n + n, oldrk);  // 复制一遍原来的 rk 数组
    for (int i = 1, p = 0; i <= n; i++) {                                                                // 排名可能会相同
      rk[sa[i]] = p += !(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]);  // 判断排名相邻的两个字符串是否相同
    }
  }
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> s, n = s.size(), s = ' ' + s, SA(n, s, sa, rk);
  for (int i = 1; i <= n; i++) {
    cout << sa[i] << ' ';
  }
  return 0;
}

其实这种做法就够用了,简单好写,但前提是出题人不卡你。(

\(O(n\log n)\) 的求法:

容易发现用 sort 排序是很慢的,由于 \(rk\) 数组的值域十分的小,考虑用基数排序优化。

代码如下:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 1e6 + 5;

int n, V, sa[kMaxN], rk[kMaxN * 2], oldrk[kMaxN * 2], oldsa[kMaxN], cnt[kMaxN];
string s;

void SA(int n, string s, int sa[], int rk[]) {
  V = 128;
  for (int i = 1; i <= n; i++) {
    cnt[rk[i] = s[i]]++;
  }
  for (int i = 1; i <= V; i++) {
    cnt[i] += cnt[i - 1];
  }
  for (int i = n; i; i--) {
    sa[cnt[rk[i]]--] = i;
  }  // 由于要用到 sa_0[i],必须再排序一遍。
  copy(rk, rk + 1 + n + n, oldrk);
  for (int i = 1, p = 0; i <= n; i++) {
    rk[sa[i]] = p += !(oldrk[sa[i]] == oldrk[sa[i - 1]]);
  }
  for (int w = 1; w < n; w <<= 1, V = n) {
    fill(cnt, cnt + V + 1, 0), copy(sa, sa + 1 + n, oldsa);  // 将 sa 数组复制一遍
    for (int i = 1; i <= n; i++) {                           // 第二关键字排序
      cnt[rk[oldsa[i] + w]]++;
    }
    for (int i = 1; i <= V; i++) {
      cnt[i] += cnt[i - 1];
    }
    for (int i = n; i; i--) {
      sa[cnt[rk[oldsa[i] + w]]--] = oldsa[i];
    }
    fill(cnt, cnt + V + 1, 0), copy(sa, sa + 1 + n, oldsa);  // 将 sa 数组复制一遍
    for (int i = 1; i <= n; i++) {                           // 第一关键字排序
      cnt[rk[oldsa[i]]]++;
    }
    for (int i = 1; i <= V; i++) {
      cnt[i] += cnt[i - 1];
    }
    for (int i = n; i; i--) {
      sa[cnt[rk[oldsa[i]]]--] = oldsa[i];
    }
    copy(rk, rk + 1 + n + n, oldrk);
    for (int i = 1, p = 0; i <= n; i++) {
      rk[sa[i]] = p += !(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]);
    }
  }
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> s, n = s.size(), s = ' ' + s, SA(n, s, sa, rk);
  for (int i = 1; i <= n; i++) {
    cout << sa[i] << ' ';
  }
  return 0;
}

一些常数优化:

第二关键字无需排序:

因为第二关键字排序的本质是把超出字符串范围的 \(sa[i]\) 放到 \(sa\) 数组的头部,然后把剩下的一次放入。

优化基数排序的值域:

显然值域并不是每次都是 \(n\),可以优化。

若排名都不相同可以直接退出循环:

若排名都不相同说明已经后缀排序完毕,可以退出循环。

代码如下:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 1e6 + 5;

int n, V, sa[kMaxN], oldsa[kMaxN], rk[kMaxN * 2], oldrk[kMaxN * 2], cnt[kMaxN];
string s;

void SA(int n, string s, int sa[], int rk[]) {
  V = 256, fill(cnt, cnt + 1 + V, 0), fill(rk, rk + n + n + 1, -1);
  for (int i = 1; i <= n; i++) {
    cnt[rk[i] = s[i]]++;
  }
  for (int i = 1; i <= V; i++) {
    cnt[i] += cnt[i - 1];
  }
  for (int i = n; i; i--) {
    sa[cnt[rk[i]]--] = i;
  }
  for (int w = 1, p = 0, tot = 0; p != n /* 若排名都不相同直接退出循环 */; w <<= 1, tot = 0, V = p /* 优化值域 */) {
    for (int i = n - w + 1; i <= n; i++) {
      oldsa[++tot] = i;
    }  // 第二关键字无需排序
    for (int i = 1; i <= n; i++) {
      sa[i] > w && (oldsa[++tot] = sa[i] - w);
    }
    fill(cnt, cnt + 1 + V, 0), copy(rk, rk + 1 + n + n, oldrk), p = 0;
    for (int i = 1; i <= n; i++) {
      cnt[rk[i]]++;
    }
    for (int i = 1; i <= V; i++) {
      cnt[i] += cnt[i - 1];
    }
    for (int i = n; i; i--) {
      sa[cnt[rk[oldsa[i]]]--] = oldsa[i];
    }
    for (int i = 1; i <= n; i++) {
      rk[sa[i]] = p += !(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]);
    }
  }
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> s, n = s.size(), s = ' ' + s, SA(n, s, sa, rk);
  for (int i = 1; i <= n; i++) {
    cout << sa[i] << ' ';
  }
  return 0;
}

上述代码均为 Luogu - P3809【模板】后缀排序 的代码。

\(O(n)\) 的求法:

有两种方法:DC3 和 SA-IS,但是本文不在此赘述,如有需要请查看其他资料。

后缀数组的应用:

Luogu - P4051 [JSOI2007] 字符加密

题目链接:https://www.luogu.com.cn/problem/P4051

题意:

给定一个长度为 \(n\)\(1\le n\le 10^5\))的字符串 \(s\),将 \(s\) 的第一个字符放到最后,重复操作可以得到 \(n\) 个字符串,将这 \(n\) 个字符串按照字典序从小到大排序,输出排序后的 \(n\) 个字符串的末尾字符拼接而成的字符串。

思路:

字符串是环形的,考虑将字符串重复拼接一次,将题目变成子串排序,这些子串长度相同所以可以比较以其开头的为开头的后缀的字典序大小,若后缀小,那么其子串一定小于或等于另一个子串,否则是大于等于,但是在这里等于不重要,所以可以直接后缀排序,然后输出末尾字符即可。

代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 2e5 + 5;

int n, V, sa[kMaxN], oldsa[kMaxN], rk[kMaxN * 2], oldrk[kMaxN * 2], cnt[kMaxN];
string s, t;

void SA(int n, string s, int sa[], int rk[]) {
  V = 256, fill(cnt, cnt + 1 + V, 0), fill(rk, rk + n + n + 1, -1);
  for (int i = 1; i <= n; i++) {
    cnt[rk[i] = s[i]]++;
  }
  for (int i = 1; i <= V; i++) {
    cnt[i] += cnt[i - 1];
  }
  for (int i = n; i; i--) {
    sa[cnt[rk[i]]--] = i;
  }
  for (int w = 1, p = 0, tot = 0; p != n; w <<= 1, tot = 0, V = p) {
    for (int i = n - w + 1; i <= n; i++) {
      oldsa[++tot] = i;
    }
    for (int i = 1; i <= n; i++) {
      sa[i] > w && (oldsa[++tot] = sa[i] - w);
    }
    fill(cnt, cnt + 1 + V, 0), copy(rk, rk + 1 + n + n, oldrk), p = 0;
    for (int i = 1; i <= n; i++) {
      cnt[rk[i]]++;
    }
    for (int i = 1; i <= V; i++) {
      cnt[i] += cnt[i - 1];
    }
    for (int i = n; i; i--) {
      sa[cnt[rk[oldsa[i]]]--] = oldsa[i];
    }
    for (int i = 1; i <= n; i++) {
      rk[sa[i]] = p += !(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]);
    }
  }
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> s, n = s.size() * 2, s = ' ' + s + s, t.resize(n);  // 复制一遍拼在后面
  SA(n, s, sa, rk);
  for (int i = 1; i <= n / 2; i++) {
    t[rk[i] - 1] = s[i == 1 ? n / 2 : i - 1];
  }
  for (int i = 0; i < n; i++) {
    t[i] && (cout << t[i]);
  }
  return 0;
}

这里可以引申一下,若要比较两个子串 \(s[l\ldots r]\)\(s[L\ldots R]\) 的大小关系,我们可以先比较后缀 \(l\) 和后缀 \(L\) 的大小关系,若后缀 \(l\) 小于后缀 \(L\) 那么 \(s[l\ldots r]\) 一定小于等于 \(s[L\ldots R]\),否则 \(s[l\ldots r]\) 一定大于等于 \(s[L\ldots R]\),若要判断是否相等,请看到 \(height\) 数组那一章节。

Luogu - P1368 【模板】最小表示法

题目链接:https://www.luogu.com.cn/problem/P1368

题意:

给定一个长度为 \(n\)\(1\le n\le 3\times 10^5\))的序列 \(a\),将序列的第一个元素放到最后,可以形成 \(n\) 个序列,请你求出这 \(n\) 个序列中字典序最小的一个。

思路:

同样的将序列复制一遍放到后面,然后后缀排序,找到排名最靠前的且后缀长度大于 \(n\) 的后缀,输出这个后缀所对应的子串即可。

代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 6e5 + 5;

int n, V, s[kMaxN], p, sa[kMaxN], oldsa[kMaxN], rk[kMaxN * 2], oldrk[kMaxN * 2], cnt[kMaxN];

void SA(int n, int s[], int sa[], int rk[]) {
  V = 256, fill(cnt, cnt + 1 + V, 0), fill(rk, rk + n + n + 1, -1);
  for (int i = 1; i <= n; i++) {
    cnt[rk[i] = s[i]]++;
  }
  for (int i = 1; i <= V; i++) {
    cnt[i] += cnt[i - 1];
  }
  for (int i = n; i; i--) {
    sa[cnt[rk[i]]--] = i;
  }
  for (int w = 1, p = 0, tot = 0; p != n; w <<= 1, tot = 0, V = p) {
    for (int i = n - w + 1; i <= n; i++) {
      oldsa[++tot] = i;
    }
    for (int i = 1; i <= n; i++) {
      sa[i] > w && (oldsa[++tot] = sa[i] - w);
    }
    fill(cnt, cnt + 1 + V, 0), copy(rk, rk + 1 + n + n, oldrk), p = 0;
    for (int i = 1; i <= n; i++) {
      cnt[rk[i]]++;
    }
    for (int i = 1; i <= V; i++) {
      cnt[i] += cnt[i - 1];
    }
    for (int i = n; i; i--) {
      sa[cnt[rk[oldsa[i]]]--] = oldsa[i];
    }
    for (int i = 1; i <= n; i++) {
      rk[sa[i]] = p += !(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]);
    }
  }
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> s[i], s[n + i] = s[i];  // 复制一遍拼在后面
  }
  SA(n + n, s, sa, rk);
  for (p = 1; sa[p] > n; p++) {  // 找到符合条件的第一名
  }
  for (int i = sa[p]; i <= sa[p] + n - 1; i++) {
    cout << s[i] << ' ';
  }
  return 0;
}

后缀数组还有一个用处就是查找子串。

Luogu - P3808 AC 自动机(简单版):

题目链接:https://www.luogu.com.cn/problem/P3808

题意:

给定 \(n\) 个模式串 \(s_i\) 和一个文本串 \(t\),求有多少个不同的模式串在文本串里出现过。

两个模式串不同当且仅当他们编号不同。

思路:

这里我们不用 AC 自动机做,用 SA 做。

问题的本质就是查找 \(t\) 的子串中是否含有 \(s\)。由于后缀的前缀就是子串(这句话非常重要,后面的 height 数组也要用到),那么我们可以先将 \(t\) 后缀排序,由于后缀已经有序,我们可以在后缀数组中二分,找到第一个大于 \(s\) 的后缀 \(t\),然后看 \(t\)\(s\) 长度相同的前缀是否等于 \(s\) 即可。但是这样时间复杂度太高:后缀排序 \(\lvert t\rvert\log\lvert t\rvert\),共有 \(n\) 个模式串,每次要二分 \(\log\lvert t\rvert\) 次,二分的 check 函数要算 \(\lvert t\rvert\) 次,总时间复杂度为 \(O(\lvert t\rvert\log\lvert t\rvert+n\lvert t\rvert\log\lvert t\rvert)\),显然会超时。其实在 check 中我们不需要比较 \(t\) 次,其实只需要比较 \(s\) 次即可,所以总时间复杂度降为 \(O(\lvert t\rvert\log\lvert t\rvert+\log\lvert t\rvert\sum_{i=1}^n\lvert s_i\rvert)\),由于 \(1\le\sum_{i=1}^n\lvert s_i\rvert\le10^6\),可以通过此题。

代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 1e6 + 5;

int n, m, ans, V, sa[kMaxN], oldsa[kMaxN], rk[kMaxN * 2], oldrk[kMaxN * 2], cnt[kMaxN];
string s[kMaxN], S, t;

void SA(int n, string s, int sa[], int rk[]) {
  V = 256, fill(cnt, cnt + 1 + V, 0), fill(rk, rk + n + n + 1, -1);
  for (int i = 1; i <= n; i++) {
    cnt[rk[i] = s[i]]++;
  }
  for (int i = 1; i <= V; i++) {
    cnt[i] += cnt[i - 1];
  }
  for (int i = n; i; i--) {
    sa[cnt[rk[i]]--] = i;
  }
  for (int w = 1, p = 0, tot = 0; p != n; w <<= 1, tot = 0, V = p) {
    for (int i = n - w + 1; i <= n; i++) {
      oldsa[++tot] = i;
    }
    for (int i = 1; i <= n; i++) {
      sa[i] > w && (oldsa[++tot] = sa[i] - w);
    }
    fill(cnt, cnt + 1 + V, 0), copy(rk, rk + 1 + n + n, oldrk), p = 0;
    for (int i = 1; i <= n; i++) {
      cnt[rk[i]]++;
    }
    for (int i = 1; i <= V; i++) {
      cnt[i] += cnt[i - 1];
    }
    for (int i = n; i; i--) {
      sa[cnt[rk[oldsa[i]]]--] = oldsa[i];
    }
    for (int i = 1; i <= n; i++) {
      rk[sa[i]] = p += !(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]);
    }
  }
}

bool C(int m, int n) {
  int L = 1, R = m, M = L + R >> 1;
  for (; L < R; M = L + R >> 1) {
    t.substr(sa[M], min(n, m - sa[M] + 1)) >= S ? R = M : L = M + 1;
  }
  return t.substr(sa[M], min(n, m - sa[M] + 1)) == S;
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> s[i];
  }
  cin >> t, m = t.size(), t = ' ' + t, SA(m, t, sa, rk);
  for (int i = 1; i <= n; i++) {
    S = s[i], ans += C(m, s[i].size());
  }
  cout << ans;
  return 0;
}

height 数组的定义:

\(height[i]=\operatorname{lcp}(sa[i], sa[i-1])\),即表示第 \(i\) 名与第 \(i-1\) 名的后缀的最长公共前缀的长度(若 \(i=1\),则 \(height[i]=0\)

height 数组的求法:

下面需要证一个引理:\(height[rk[i]]\ge height[rk[i-1]]-1\)

证明:

\(height[rk[i-1]]\le 1\),等式显然成立。

\(height[rk[i-1]]\gt 1\),即 \(height[rk[i-1]]=\operatorname{lcp}(sa[rk[i-1]],sa[rk[i-1]-1])=\operatorname{lcp}(i-1,sa[rk[i-1]-1])>1\),不妨设其最长公共前缀为 \(aA\)\(a\) 为一个字符,\(A\) 为一个非空子串),那么 \(sa[rk[i-1]-1]\) 可以写成 \(aAB\)\(i-1\) 可以写成 \(aAC\)\(B,C\) 为最长公共前缀长度为 \(0\) 的一个子串),由于 \(rk[sa[rk[i-1]-1]]=rk[i-1]-1<rk[i-1]\),所以 \(B<C\)

接下来我们将 \(sa[rk[i-1]-1]+1\) 写成 \(AB\),将 \(i\) 写成 \(AC\),由于 \(B<C\),那么有 \(rk[sa[rk[i-1]-1]+1]<rk[i]\)。因为 \(height[rk[i]]=\operatorname{lcp}(sa[rk[i]-1],sa[rk[i]])=\operatorname{lcp}(sa[rk[i]-1],i)\),所以后缀 \(sa[rk[i]-1]\) 与后缀 \(i\) 只相差一个排名,即 \(rk[sa[rk[i]-1]<rk[i]\),所以我们可以得到 \(AB\le s[sa[rk[i]-1]\ldots n]\lt AC\),那么 \(sa[rk[i]-1]\)\(i\) 的最长公共前缀为 \(A\),证明了引理。

有了这个引理,求解 height 数组就非常方便,可以直接暴力算。

void Geth(int n, int sa[], int rk[], int h[]) {
  for (int i = 1, k = 0; i <= n; i++) {
    for (k -= !!k; i + k <= n && sa[rk[i] - 1] + k <= n && s[i + k] == s[sa[rk[i] - 1] + k]; k++) {
    }  // 暴力计算
    h[rk[i]] = k;  // 算出结果
  }
}

height 数组的应用:

现在我们可以比较子串字典序大小了。在前面我们知道了:若后缀 \(l\) 小于后缀 \(L\) 那么 \(s[l\ldots r]\) 一定小于等于 \(s[L\ldots R]\),否则 \(s[l\ldots r]\) 一定大于等于 \(s[L\ldots R]\)。现在我们可以利用 height 数组判断等号了,若 \(\operatorname{lcp}(l,L)\ge\min(r-l+1,R-L+1)\),那么这两个子串的大小关系就可以转化为子串长度的大小关系,否则就可以用上面的方法分出大于还是小于。求 \(\operatorname{lcp}(l,L)\) 可以用 height 数组求解。


height 数组其实还有特别重要一个性质 \(\operatorname{lcp}(sa[i],sa[j])=\min\{height[i+1\ldots j]\}\)

证明:

先证一个引理:\(\operatorname{lcp}(sa[i],sa[j])=\min(\operatorname{lcp}(sa[i],sa[k]),\operatorname{lcp}(sa[j],sa[k]))\),其中 \(i\lt k\lt j\)

设后缀 \(sa[i]\) 与后缀 \(sa[j]\) 的最长公共前缀为 \(A\),那么后缀 \(sa[i]\) 可以被表示为 \(AB\),后缀 \(sa[k]\) 可以表示为 \(AC\),后缀 \(sa[j]\) 可以表示为 \(AD\),那么 \(\operatorname{lcp}(sa[i],sa[k]),\operatorname{lcp}(sa[j],sa[k])\ge \operatorname{lcp}(sa[i],sa[j])\),所以接下来只需要证 \(\operatorname{lcp}(B,C)\)\(\operatorname{lcp}(D,C)\) 其中一个等于 \(0\) 即可。

\(\operatorname{lcp}(B,C)\)\(\operatorname{lcp}(D,C)\) 都不为 \(0\),那么显然 \(\operatorname{lcp}(B,C)\) 一定不为 \(0\),矛盾!这就证出了引理。

那么性质的证明就很显然了:\(\operatorname{lcp}(sa[i],sa[j])=\min_{k=i+1}^j\operatorname{lcp}(sa[k-1],sa[k])=\min\{height[i+1\ldots j]\}\)

有了这个性质,那么我们就可以把后缀的最长公共前缀转化为 RMQ 问题。


这样我们也可以算出两个子串的最长公共前缀了,要求 \(s[l\ldots r]\)\(s[L\ldots R]\) 的最长公共前缀的长度,我们可以先求出 \(\operatorname{lcp}(l,L)\),那么 \(\operatorname{lcp}(s[l\ldots r],s[L\ldots R])=\min(r-l+1,R-L+1,\operatorname{lcp}(l,L))\)


接下来看一些例题,先是一个字符串的问题。

UVA1223 Editor

题目链接:https://www.luogu.com.cn/problem/UVA1223

题意:

给定一个长度为 \(n\) 的字符串 \(s\),求 \(s\) 的最长子串,使得该子串在 \(s\) 至少出现 \(2\) 次(可以重叠)。

思路:

子串就是后缀的前缀,不同的后缀的前缀对应的区间一定不同,所以两个后缀的公共前缀就是重复出现的子串,要求子串长度最大,那么就是最长公共前缀,所以我们要求的答案就是:\(\max_{1\le i\lt j\le n} \operatorname{lcp}(sa[i], sa[j])\),又因为 \(\operatorname{lcp}(sa[i],sa[j])=\min\{height[i+1\ldots j]\}\),所以答案就是求 \(\min\{height[2\ldots n]\}\)

代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 5005;

int T, n, sa[kMaxN], rk[kMaxN * 2], oldrk[kMaxN * 2], h[kMaxN];
string s;

void SA(int n, string s, int sa[], int rk[]) {
  fill(rk, rk + 1 + n + n, -1);
  for (int i = 1; i <= n; i++) {
    sa[i] = i, rk[i] = s[i];
  }
  for (int w = 1, p = 0; w < n && p != n; w <<= 1) {
    sort(sa + 1, sa + 1 + n, [&](int i, int j) { return rk[i] == rk[j] ? rk[i + w] < rk[j + w] : rk[i] < rk[j]; });
    copy(rk, rk + 1 + n + n, oldrk), p = 0;
    for (int i = 1; i <= n; i++) {
      rk[sa[i]] = p += !(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]);
    }
  }
}

void Geth(int n, string s, int sa[], int rk[], int h[]) {
  for (int i = 1, k = 0; i <= n; i++) {
    for (k -= !!k; i + k <= n && sa[rk[i] - 1] + k <= n && s[i + k] == s[sa[rk[i] - 1] + k]; k++) {
    }
    h[rk[i]] = k;
  }
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  for (cin >> T; T; T--) {
    cin >> s, n = s.size(), s = ' ' + s;
    SA(n, s, sa, rk), Geth(n, s, sa, rk, h);
    cout << *max_element(h + 2, h + 1 + n) << '\n';
  }
  return 0;
}

Editor 2

作者暂时还没找到原题……

题意:

给定一个长度为 \(n\) 的字符串 \(s\),求 \(s\) 的最长子串长度,满足该子串在 \(s\) 中出现至少 \(2\) 次,且这两次的位置不重叠。

思路:

由于不能重叠不能再像原来那样直接求最大值,观察到答案具有单调性,考虑二分答案将题目转化为判定性问题。设此次要判定的长度为 \(k\),这样我们就可以将后缀分成若干段,每一段的前 \(k\) 的字符都相同,然后在每一段中选取后缀,那么其最长公共前缀一定大于等于 \(k\),所以只要每一段的后缀中的最长减最短的差值大于或等于 \(k\),那么 \(k\) 就一定满足要求。

代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 1e6 + 5;

int T, n, L, R, M, sa[kMaxN], rk[kMaxN * 2], oldrk[kMaxN * 2], h[kMaxN];
string s;

void SA(int n, string s, int sa[], int rk[]) {
  fill(rk, rk + 1 + n + n, -1);
  for (int i = 1; i <= n; i++) {
    sa[i] = i, rk[i] = s[i];
  }
  for (int w = 1, p = 0; w < n && p != n; w <<= 1) {
    sort(sa + 1, sa + 1 + n, [&](int i, int j) { return rk[i] == rk[j] ? rk[i + w] < rk[j + w] : rk[i] < rk[j]; });
    copy(rk, rk + 1 + n + n, oldrk), p = 0;
    for (int i = 1; i <= n; i++) {
      rk[sa[i]] = p += !(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]);
    }
  }
}

void Geth(int n, string s, int sa[], int rk[], int h[]) {
  for (int i = 1, k = 0; i <= n; i++) {
    for (k -= !!k; i + k <= n && sa[rk[i] - 1] + k <= n && s[i + k] == s[sa[rk[i] - 1] + k]; k++) {
    }
    h[rk[i]] = k;
  }
}

bool C(int k) {
  for (int i = 1, j = 1, maxl = n - sa[1] + 1, minl = n - sa[1] + 1, lcp = 1e9; j <= n; maxl = minl = n - sa[i = ++j] + 1) {
    for (; j + 1 <= n && min(lcp, h[j + 1]) >= k; j++, maxl = max(maxl, n - sa[j] + 1), minl = min(minl, n - sa[j] + 1)) {
    }
    if (maxl - minl >= k) {
      return 1;
    }
  }
  return 0;
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  for (cin >> T; T; T--) {
    cin >> s, n = s.size(), s = ' ' + s;
    SA(n, s, sa, rk), Geth(n, s, sa, rk, h);
    L = 0, R = n, M = L + R + 1 >> 1;
    for (; L < R; M = L + R + 1 >> 1) {
      C(M) ? L = M : R = M - 1;
    }
    cout << L << '\n';
  }
  return 0;
}

Luogu - P2408 不同子串个数

题目链接:https://www.luogu.com.cn/problem/P2408

题意:

给定一个长度为 \(n\) 的字符串 \(s\),求其本质不同的字符串个数。

思路:

先将字符串后缀排序,对于每一个后缀会产生 \(n-sa[i]+1\) 个子串,但是会有重复,会与所有排名在它前的字符串产生冲突,容易发现重复次数为 \(\max_{1\le j\lt i}\operatorname{lcp}(sa[j],sa[i])=\max_{1\le j\lt i}\min\{height[j+1\ldots i]\}=height[i]\),所以每一个后缀就会产生 \(n-sa[i]+1-height[i]\) 的贡献,计算总和即可。

代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 1e5 + 5;

int n, sa[kMaxN], rk[kMaxN * 2], oldrk[kMaxN * 2], h[kMaxN];
string s;
long long ans;

void SA(int n, string s, int sa[], int rk[]) {
  fill(rk, rk + 1 + n + n, -1);
  for (int i = 1; i <= n; i++) {
    sa[i] = i, rk[i] = s[i];
  }
  for (int w = 1, p = 0; w < n && p != n; w <<= 1) {
    sort(sa + 1, sa + 1 + n, [&](int i, int j) { return rk[i] == rk[j] ? rk[i + w] < rk[j + w] : rk[i] < rk[j]; });
    copy(rk, rk + 1 + n + n, oldrk), p = 0;
    for (int i = 1; i <= n; i++) {
      rk[sa[i]] = p += !(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]);
    }
  }
}

void Geth(int n, string s, int sa[], int rk[], int h[]) {
  for (int i = 1, k = 0; i <= n; i++) {
    for (k -= !!k; i + k <= n && sa[rk[i] - 1] + k <= n && s[i + k] == s[sa[rk[i] - 1] + k]; k++) {
    }
    h[rk[i]] = k;
  }
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n >> s, s = ' ' + s;
  SA(n, s, sa, rk), Geth(n, s, sa, rk, h);
  for (int i = 1; i <= n; i++) {
    ans += n - sa[i] + 1 - h[i];
  }
  cout << ans;
  return 0;
}

SP32951 ADASTRNG - Ada and Substring

题目链接:https://www.luogu.com.cn/problem/SP32951

题意:

给定一个长度为 \(n\) 且仅由小写字母组成的字符串,求以 az 开头的本质不同的子串个数。

思路:

其实和前一题很像,首先可以将字符串后缀排序,对于 \(i\),其对 \(s[sa[i]]\) 的贡献为 \(n-sa[i]+1-height[i]\),累加即可。

代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 1e6 + 5;

int n, sa[kMaxN], rk[kMaxN * 2], oldrk[kMaxN * 2], h[kMaxN];
string s;
long long ans[26];

void SA(int n, string s, int sa[], int rk[]) {
  fill(rk, rk + 1 + n + n, -1);
  for (int i = 1; i <= n; i++) {
    sa[i] = i, rk[i] = s[i];
  }
  for (int w = 1, p = 0; w < n && p != n; w <<= 1) {
    sort(sa + 1, sa + 1 + n, [&](int i, int j) { return rk[i] == rk[j] ? rk[i + w] < rk[j + w] : rk[i] < rk[j]; });
    copy(rk, rk + 1 + n + n, oldrk), p = 0;
    for (int i = 1; i <= n; i++) {
      rk[sa[i]] = p += !(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]);
    }
  }
}

void Geth(int n, string s, int sa[], int rk[], int h[]) {
  for (int i = 1, k = 0; i <= n; i++) {
    for (k -= !!k; i + k <= n && sa[rk[i] - 1] + k <= n && s[i + k] == s[sa[rk[i] - 1] + k]; k++) {
    }
    h[rk[i]] = k;
  }
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> s, n = s.size(), s = ' ' + s;
  SA(n, s, sa, rk), Geth(n, s, sa, rk, h);
  for (int i = 1; i <= n; i++) {
    ans[s[sa[i]] - 'a'] += n - sa[i] + 1 - h[i];
  }
  for (int i = 0; i < 26; i++) {
    cout << ans[i] << ' ';
  }
  return 0;
}
posted @ 2024-08-31 16:36  liruixiong0101  阅读(24)  评论(0编辑  收藏  举报