字符串杂记
字符串杂记
哈希
对于字符串哈希,定义哈希函数:
则:
若两个串的哈希值相同,基本可以认为两个串相同。
应用
P3538 [POI2012] OKR-A Horrible Poem
给出一个由小写英文字母组成的字符串 \(S\),再给出 \(q\) 个询问,要求回答 \(S\) 某个子串的最短循环节。
\(n \leq 5 \times 10^5\) ,\(q \leq 2 \times 10^6\)
结论:若 \(S[1 : n - x] = S[1 + x, n]\) ,则 \(S\) 有一个长度为 \(x\) 的循环节
注意到最短循环节长度一定是子串长度的因子,但是直接枚举因子会超时。考虑质因数分解 \(\prod p_i^{c_i}\)
注意到若循环节长度为 \(x\) ,则 \(k \times x\) 也是循环节
可以把每个指数依次变小并判断是否合法,复杂度小很多
#include <bits/stdc++.h>
using namespace std;
const int base = 233;
const int N = 5e5 + 7;
vector<int> factor[N];
int power[N], h[N];
char str[N];
bool IsPrime[N];
int n, q;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = c == '-';
while (c < '0' || c > '9')
c = getchar(), sign |= c == '-';
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline int query(int l, int r) {
return h[r] - 1ll * h[l - 1] * power[r - l + 1];
}
inline bool check(int l, int r, int x) {
return query(l, r - x) == query(l + x, r);
}
inline void prework() {
memset(IsPrime, true, sizeof(IsPrime));
IsPrime[1] = false;
for (int i = 2; i <= n; ++i)
if (IsPrime[i])
for (int j = 1; i * j <= n; ++j)
factor[i * j].emplace_back(i);
}
signed main() {
n = read(), scanf("%s", str + 1);
power[0] = 1;
for (int i = 1; i <= n; ++i) {
power[i] = power[i - 1] * base;
h[i] = h[i - 1] * base + str[i];
}
prework();
q = read();
while (q--) {
int l = read(), r = read();
if (l == r) {
puts("1");
continue;
}
if (query(l + 1, r) == query(l, r - 1)) {
puts("1");
continue;
}
int len = r - l + 1, ans = r - l + 1;
for (int x : factor[len])
while (!(ans % x) && check(l, r, ans / x))
ans /= x;
printf("%d\n", ans);
}
return 0;
}
KMP
记 \(nxt_i\) 表示 \(S[1, i]\) 的最长公共前后缀长度,每次失配的时候就跳 \(nxt\) 指针直到匹配为止。
对于求 \(nxt\) ,考虑将 \(S\) 与自己做一个类似匹配的过程即可。
时间复杂度 \(O(n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
char s[N], p[N];
int nxt[N];
int lens, lenp;
signed main() {
scanf("%s%s", s + 1, p + 1);
lens = strlen(s + 1), lenp = strlen(p + 1);
for (int i = 2, j = 0; i <= lenp; ++i) {
while (j && p[j + 1] != p[i])
j = nxt[j];
if (p[j + 1] == p[i])
++j;
nxt[i] = j;
}
for (int i = 1, j = 0; i <= lens; ++i) {
while (j && p[j + 1] != s[i])
j = nxt[j];
if (p[j + 1] == s[i])
++j;
if (j == lenp) {
printf("%d\n", i - lenp + 1);
j = nxt[j];
}
}
for (int i = 1; i <= lenp; ++i)
printf("%d ", nxt[i]);
return 0;
}
应用
求每个前缀在该串中的出现次数。
\(n \leq 2 \times 10^5\)
首先设 \(ans_i\) 为每个 \(nxt\) 值出现次数,如果长度为 \(i\) 的前缀出现了恰好 \(ans_i\) 次,那么其最长的既是前缀又是后缀的子串也会出现 \(ans_i\) 次,累加即可。
最后对于原始的前缀出现位置,我们统一 \(+1\) 即可。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e4 + 7;
const int N = 2e5 + 7;
int nxt[N], ans[N];
char str[N];
int n;
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%s", &n, str + 1);
for (int i = 2, j = 0; i <= n; ++i) {
while (j && str[j + 1] != str[i])
j = nxt[j];
if (str[j + 1] == str[i])
++j;
nxt[i] = j;
}
memset(ans + 1, 0, sizeof(int) * n);
for (int i = n; i; --i)
ans[nxt[i]] += (ans[i]++) + 1;
int sum = 0;
for (int i = 1; i <= n; ++i)
sum = (sum + ans[i]) % Mod;
printf("%d\n", sum);
}
return 0;
}
若要求每个前缀在另一串中的出现次数,则把两串拼起来,并在中间塞入一个无用字符,直接容斥即可。
P3435 [POI2006] OKR-Periods of Words
求给定字符串所有前缀的最大周期长度之和。
\(n \leq 10^6\)
发现最大周期就是中间部分的循环节,为了让其最小,循环节就要最小,所以考虑进行跳 \(nxt\) 数组直到其后一个为 \(0\) 。
发现直接跳会 TLE,所以考虑路径压缩,就是每次先将答案算好,之后再将这一段字符串跳的部分更新成当前答案。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e6 + 7;
int nxt[N];
char str[N];
int n;
signed main() {
scanf("%d%s", &n, str + 1);
for (int i = 2, j = 0; i <= n; ++i) {
while (j && str[j + 1] != str[i])
j = nxt[j];
if (str[j + 1] == str[i])
++j;
nxt[i] = j;
}
ll ans = 0;
for (int i = 2; i <= n; ++i) {
while (nxt[nxt[i]])
nxt[i] = nxt[nxt[i]];
if (nxt[i])
ans += i - nxt[i];
}
printf("%lld", ans);
return 0;
}
求有多少长度为 \(n\) 的字符串 \(S\) 不包含给定的长度为 \(m\) 的数字串 \(T\) ,答案对 \(k\) 取模。
\(n \leq 10^9\) ,\(m \leq 20\) ,\(k \leq 1000\)
设 \(f_{i, j}\) 表示 \(S\) 匹配了 \(i\) 位且 \(T\) 匹配了 \(j\) 位的方案数,转移时不要 \(j = |T|\) 的状态即可。
考虑设 \(g_{i, j}\) 表示 \(T\) 已经匹配了 \(i\) 位,再加一个字符匹配长度变为 \(j\) 的方案数。则有:
于是可以用矩阵快速幂优化,接下来考虑求 \(g\) ,枚举添加的字符不断跳 \(nxt\) 指针即可。
#include <bits/stdc++.h>
using namespace std;
const int M = 2e1 + 7;
int nxt[M];
char str[M];
int n, m, Mod;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
struct Matrix {
int a[M][M];
inline Matrix() {
memset(a, 0, sizeof(a));
}
inline Matrix operator * (const Matrix &rhs) const {
Matrix res;
for (int i = 0; i < m; ++i)
for (int j = 0; j < m; ++j)
for (int k = 0; k < m; ++k)
res.a[i][k] = add(res.a[i][k], 1ll * a[i][j] * rhs.a[j][k] % Mod);
return res;
}
inline Matrix operator ^ (int n) const {
Matrix res;
for (int i = 0; i < m; ++i)
res.a[i][i] = 1;
for (Matrix base = *this; n; base = base * base, n >>= 1)
if (n & 1)
res = res * base;
return res;
}
} g;
signed main() {
scanf("%d%d%d%s", &n, &m, &Mod, str + 1);
for (int i = 2, j = 0; i <= m; ++i) {
while (j && str[j + 1] != str[i])
j = nxt[j];
if (str[j + 1] == str[i])
++j;
nxt[i] = j;
}
for (int i = 0; i < m; ++i) {
for (char c = '0'; c <= '9'; ++c) {
int j = i;
while (j && str[j + 1] != c)
j = nxt[j];
if (str[j + 1] == c)
++j;
++g.a[i][j];
}
}
g = g ^ n;
int ans = 0;
for (int i = 0; i < m; ++i)
ans = add(ans, g.a[0][i]);
printf("%d", ans);
return 0;
}
CF1286E Fedya the Potter Strikes Back
对于一个小写字符串 \(S\) ,若子串 \(S[l, r]\) 满足 \(S[l, r] = S[1, r - l + 1]\) ,则称其为好串。
对于长度为 \(n\) 的小写字符串 \(S[1, n]\) 与权值 \(w_{1 \sim n}\) ,定义子串 \(S[l, r]\) 的权值为 \(\min_{i = l}^r w_i\) 。
在线给出序列(每次在末尾插入字符与权值),每次求所有好串的权值和,强制在线。
\(n \leq 6 \times 10^5\)
以 \(r\) 结尾的好串实际上就是 \(S[1, r]\) 的 border 。先处理掉这个取 \(\min\) 的操作,不难使用 ST 表做到在线插入。
接下来考虑维护答案的增量。维护 border 集合,从 \(i - 1 \to i\) 的时候实际上就是先去掉 \(S_{x + 1} \neq S_i\) 的 border,再将每个 border 扩展为 \(x \to x + 1\) ,若 \(S_i = S_1\) 则再加入 \(1\) 为新的 border。
考虑删除不合法的 border 。对于一个 border \(x\) ,若 \(S_{x + 1} \neq S_i\) 则其无法扩展。即对于每个 border ,跳到最近的满足 \(S_{x + 1} = S_i\) 的祖先,并删去路径上的 border。
对于合法的 Border ,由于扩展,需要将原来的权值对 \(w_i\) 取 \(\min\) 。直接用 map
存答案,每次将 \(> w_i\) 的数删除并加入等量 \(w_i\) 即可。
由于只有 \(O(n)\) 次操作,所以删除的总复杂度是 \(O(n)\) 的,总复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll B = 1e18;
const int N = 6e5 + 7;
struct bignum {
ll x, y, z1, z2;
inline bignum operator += (const ll &k) {
x += (y += k) / B, y %= B;
z1 = (z1 + k) % 26, z2 = (z2 + k) & ((1 << 30) - 1);
return *this;
}
inline void writeln() {
if (x)
printf("%lld%018lld\n", x, y);
else
printf("%lld\n", y);
}
} ans;
map<int, int> mp;
int anc[N][26];
int val[N], nxt[N];
char str[N];
ll res;
int n;
namespace ST {
const int LOGN = 21;
int f[LOGN][N];
int n;
inline void emplace_back(int k) {
f[0][++n] = k;
for (int j = 1; j <= __lg(n); ++j)
f[j][n] = min(f[j - 1][n], f[j - 1][n - (1 << (j - 1))]);
}
inline int query(int l, int r) {
int k = __lg(r - l + 1);
return min(f[k][r], f[k][l + (1 << k) - 1]);
}
} // namespace ST
inline void update(int x, int k) {
mp[x] += k, res += 1ll * x * k;
}
inline void insert(int x) {
int now = 0;
for (auto it = mp.upper_bound(x); it != mp.end(); it = mp.erase(it))
now += it->second, res -= 1ll * it->first * it->second;
update(x, now);
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1, j = 0; i <= n; ++i) {
cin >> str[i] >> val[i];
str[i] = 'a' + (str[i] - 'a' + ans.z1) % 26, val[i] ^= ans.z2;
ST::emplace_back(val[i]), ans += ST::query(1, i);
if (i == 1) {
ans.writeln();
continue;
}
while (j && str[i] != str[j + 1])
j = nxt[j];
if (str[i] == str[j + 1])
++j;
nxt[i] = j;
if (str[1] == str[i])
update(val[i], 1);
for (int k = 0; k < 26; ++k)
anc[i][k] = anc[nxt[i]][k];
anc[i][str[nxt[i] + 1] - 'a'] = nxt[i];
for (int k = 0; k < 26; ++k)
if (str[i] != 'a' + k)
for (int cur = anc[i - 1][k]; cur; cur = anc[cur][k])
update(ST::query(i - cur, i - 1), -1);
insert(val[i]), (ans += res).writeln();
}
return 0;
}
Manacher
记 \(p_i\) 表示以 \(i\) 为中心的最长回文子串的半径。
考虑维护目前 \(r\) 表示已经触及到的最右边的字符, \(mid\) 表示触及到 \(r\) 的回文串其中心。
- 若 \(i \leq r\) ,则有 \(p_i \geq p_{mid \times 2 - i}\) ,因此令 \(p_i \gets \min(p[mid \times 2 - i], r - i + 1)\) 。
- 若 \(i > r\) ,令 \(p_i \gets 1\) 。
之后暴力扩展直到无法扩展为止,时间复杂度线性。
注意该算法只能求出长度为奇数的回文串,长度为偶数的回文串可以考虑在每个字符之间插入一个无关字符统计。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e7 + 7;
int p[N];
char s[N], str[N];
int n;
signed main() {
scanf("%s", s + 1);
for (int i = 1, len = strlen(s + 1); i <= len; ++i)
str[++n] = '#', str[++n] = s[i];
str[++n] = '#';
for (int i = 1, mid = 0, r = 0; i <= n; ++i) {
p[i] = (i <= r ? min(p[mid * 2 - i], r - i + 1) : 1);
while (i - p[i] && i + p[i] <= n && str[i - p[i]] == str[i + p[i]])
++p[i];
if (i + p[i] - 1 > r)
mid = i, r = i + p[i] - 1;
}
printf("%d", *max_element(p + 1, p + n + 1) - 1);
return 0;
}
应用
求 \(S\) 中形如 \(AB\) 的最长子串长度,其中 \(A, B\) 为两个回文串。
\(n \leq 10^5\)
考虑求出每个位置开头和结尾的最长回文串长度 \(R_i, L_i\) 。对于每个位置为中心的最长回文串,先更新回文串两端的 \(L, R\) 。对于剩下的 \(L, R\) ,一定可以从相隔两个位置的 \(L, R\) 推过来。于是可以做到线性。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;
int p[N], L[N], R[N];
char s[N], str[N];
int n;
signed main() {
scanf("%s", s + 1);
for (int i = 1, len = strlen(s + 1); i <= len; ++i)
str[++n] = '#', str[++n] = s[i];
str[++n] = '#';
for (int i = 1, mid = 0, r = 0; i <= n; ++i) {
p[i] = (i <= r ? min(p[mid * 2 - i], r - i + 1) : 1);
while (i - p[i] && i + p[i] <= n && str[i - p[i]] == str[i + p[i]])
++p[i];
if (i + p[i] - 1 > r)
mid = i, r = i + p[i] - 1;
L[i + p[i] - 1] = max(L[i + p[i] - 1], p[i] - 1);
R[i - p[i] + 1] = max(R[i - p[i] + 1], p[i] - 1);
}
for (int i = n - 2; i; --i)
L[i] = max(L[i], L[i + 2] - 2);
for (int i = 3; i <= n; ++i)
R[i] = max(R[i], R[i - 2] - 2);
int ans = 0;
for (int i = 1; i <= n; i += 2)
if (L[i] && R[i])
ans = max(ans, L[i] + R[i]);
printf("%d", ans);
return 0;
}
求字符集仅含 \(a, b\) 的串 \(S\) 中有多少子串满足:
- 位置和字符都关于某条对称轴对称。
- 不能是连续的一段。
\(n\leq 10^5\)
答案等于“位置对称的回文子序列数”减去“回文子串数”,后者不难用 Manacher 解决。
记 \(i\) 为位置对称的回文子序列的中心,共有 \(cnt\) 个 \(j \geq 0\) 满足 \(S[i - j] = S[i + j]\) ,则中心 \(i\) 的贡献为 \(2^j - 1\) 。
考虑统计 \(S[i - j] = S[i + j] = \text{a}\) 的数量,设多项式 \(A_i = [S[i] = \text{a}]\) ,将 \(A\) 与自己卷积后,\(A_{2i}\) 的值就是中心为 \(i\) 且 \(S[i - j] = S[i + j] = \text{a}\) 的数量。
时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 1e9 + 7;
const int N = 2e5 + 7;
int p[N], a[N], b[N], pw[N];
char s[N], str[N];
int len, n;
namespace Poly {
#define cpy(a, b, n) memcpy(a, b, sizeof(int) * n)
#define clr(a, n) memset(a, 0, sizeof(int) * n)
const double Pi = acos(-1);
const int S = 2e6 + 7;
int rev[S];
inline void calrev(int n) {
for (int i = 0; i < n; ++i)
rev[i] = (rev[i >> 1] >> 1) | (i & 1 ? n >> 1 : 0);
}
inline int calc(int n) {
int len = 1;
while (len <= n)
len <<= 1;
return calrev(len), len;
}
inline void FFT(complex<double> *f, int n, int op) {
for (int i = 0; i < n; ++i)
if (i < rev[i])
swap(f[i], f[rev[i]]);
for (int k = 1; k < n; k <<= 1) {
complex<double> tG(cos(Pi / k), sin(Pi / k) * op);
for (int i = 0; i < n; i += k << 1) {
complex<double> buf = 1;
for (int j = 0; j < k; ++j) {
complex<double> fl = f[i + j], fr = buf * f[i + j + k];
f[i + j] = fl + fr, f[i + j + k] = fl - fr;
buf *= tG;
}
}
}
if (op == -1) {
for (int i = 0; i < n; ++i)
f[i] /= n;
}
}
inline void Mul(int *f, int n) {
static complex<double> a[S];
int len = calc(n * 2 - 1);
for (int i = 0; i < n; ++i)
a[i] = f[i];
for (int i = n; i < len; ++i)
a[i] = 0;
FFT(a, len, 1);
for (int i = 0; i < len; ++i)
a[i] *= a[i];
FFT(a, len, -1);
for (int i = 0; i < n * 2 - 1; ++i)
f[i] = round(a[i].real());
}
#undef cpy
#undef clr
} // namespace Poly
signed main() {
scanf("%s", s + 1);
len = strlen(s + 1);
for (int i = 1; i <= len; ++i)
str[++n] = '#', str[++n] = s[i];
str[++n] = '#';
for (int i = 1, mid = 0, r = 0; i <= n; ++i) {
p[i] = (i <= r ? min(p[mid * 2 - i], r - i + 1) : 1);
while (i - p[i] && i + p[i] <= n && str[i - p[i]] == str[i + p[i]])
++p[i];
if (i + p[i] - 1 > r)
mid = i, r = i + p[i] - 1;
}
for (int i = 1; i <= len; ++i)
a[i] = (s[i] == 'a'), b[i] = (s[i] == 'b');
Poly::Mul(a, len + 1), Poly::Mul(b, len + 1);
pw[0] = 1;
for (int i = 1; i <= len; ++i)
pw[i] = 2ll * pw[i - 1] % Mod;
int ans = 0;
for (int i = 1; i <= len * 2; ++i)
ans = (ans + pw[(a[i] + b[i] + (~i & 1)) / 2] - 1) % Mod;
for (int i = 1; i <= n; ++i)
ans = (ans - p[i] / 2) % Mod;
printf("%d", (ans + Mod) % Mod);
return 0;
}
exKMP(Z 函数)
记 \(z_i\) 表示 \(S\) 与 \(S[i, n]\) 的 LCP 的长度。
定义 \(z_0 = 0\) ,\(z_1 = |S|\) 。称区间 \([i, i + z[i] - 1]\) 为 \(i\) 的匹配段(Z-box)。
考虑维护右端点最靠右的匹配段,记作 \([l, r]\) 。
- 若 \(i \leq r\) ,则有 \(s[i, r] = s[i - l, r - l]\) ,因此令 \(z_i \gets \min(z_{i - l + 1}, r - i + 1)\) 。
- 若 \(i > r\) ,令 \(z_i \gets 0\) 。
之后暴力枚举下一位匹配到无法拓展为止,时间复杂度线性。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 4e7 + 7;
int z[N];
char a[N], b[N], str[N];
int lena, lenb, n;
signed main() {
scanf("%s%s", a + 1, b + 1);
lena = strlen(a + 1), lenb = strlen(b + 1);
memcpy(str + 1, b + 1, sizeof(char) * lenb);
str[lenb + 1] = '#';
memcpy(str + lenb + 2, a + 1, sizeof(char) * lena);
n = lena + lenb + 1;
z[1] = n;
for (int i = 2, l = 0, r = 0; i <= n; ++i) {
z[i] = (i <= r ? min(z[i - l + 1], r - i + 1) : 0);
while (i + z[i] <= n && str[i + z[i]] == str[1 + z[i]])
++z[i];
if (i + z[i] - 1 > r)
l = i, r = i + z[i] - 1;
}
ll ans = 0;
for (int i = 1; i <= lenb; ++i)
ans ^= 1ll * i * (min(z[i], lenb) + 1);
printf("%lld\n", ans), ans = 0;
for (int i = 1; i <= lena; ++i)
ans ^= 1ll * i * (z[lenb + 1 + i] + 1);
printf("%lld", ans);
return 0;
}
应用
求 \(S = (AB)^i C\) 的拆分方案数,其中 \(A, B, C\) 均非空,且 \(F(A) \le F(C)\) ,其中 \(F(S)\) 表示 \(S\) 中出现奇数次的字符的数量。
\(|S| \leq 2^{20}\)
先不考虑奇偶的限制,枚举 \(AB\) 的长度 \(i\) ,并计算有多少前缀可以被表示成 \((AB)^k\) 的形式,明显此前缀的长度为 \(ik\) 。
根据循环节经典理论,有 \(S[1, i(k - 1)] = S[i + 1, ik]\) 。求出 \(S\) 的 Z 函数,则 \(i(k - 1) \leq z_{i + 1}\) 。又因为 \(C\) 非空,于是 \(k \leq t = \min(\frac{z_{i + 1}}{i} + 1, \frac{n - 1}{i})\) 。
再考虑奇偶的限制,定义权值为某个串中出现奇数次的字符数量。
- 当 \(k\) 是奇数时, \(C\) 的权值等价于 \(s[i + 1 : n]\) 的权值。
- 当 \(k\) 是偶数时, \(C\) 的权值等价于 \(s[1 : n]\) 的权值。
对于合法 \(A\) 的数量,发现 \(A\) 是 \(S\) 的一个前缀,用桶进行维护所有前缀中各权值即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1 << 20 | 1;
int z[N];
char str[N];
int n;
inline void exKMP() {
z[1] = n;
for (int i = 2, l = 0, r = 0; i <= n; ++i) {
z[i] = (i <= r ? min(z[i - l + 1], r - i + 1) : 0);
while (i + z[i] <= n && str[i + z[i]] == str[1 + z[i]])
++z[i];
if (i + z[i] - 1 > r)
l = i, r = i + z[i] - 1;
}
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%s", str + 1);
n = strlen(str + 1);
exKMP();
vector<int> pre(26), suf(26), sum(27);
for (int i = 1; i <= n; ++i)
suf[str[i] - 'a'] ^= 1;
ll ans = 0;
int valn = count(suf.begin(), suf.end(), 1), sufval = valn, preval = 0, res1 = 0, res2 = 0;
for (int i = 1; i < n; ++i) {
res1 += (suf[str[i] - 'a'] ? -sum[sufval--] : sum[++sufval]), suf[str[i] - 'a'] ^= 1;
int t = min(z[i + 1] / i + 1, (n - 1) / i);
ans += 1ll * (t + 1) / 2 * res1;
preval += (pre[str[i] - 'a'] ? -1 : 1), pre[str[i] - 'a'] ^= 1;
++sum[preval], res1 += (preval <= sufval);
ans += 1ll * t / 2 * res2, res2 += (preval <= valn);
}
printf("%lld\n", ans);
}
return 0;
}
周期与 Border 理论
定义:
- \(p\) 为 \(S\) 的周期当且仅当 \(p \leq |S|\) 且 \(S[i] = S[i + p] (i \in [1, |S| - p])\) 。
- 每个字符串 \(S\) 都存在一个平凡周期: \(|S|\) 。
- \(T\) 是 \(S\) 的 Border 当且仅当 \(T\) 同时为 \(S\) 的一个前缀与后缀且 \(T \neq S\) 。为了方便也称 \(|T|\) 是 \(S\) 的 Border。
- 每个字符串 \(S\) 都存在一个平凡 Border: \(\epsilon\) 。
一个浅显的结论:\(p\) 是 \(S\) 的周期,当且仅当 \(|S| - p\) 是 \(S\) 的 Border。
证明:\(p\) 是 \(S\) 的周期等价于 \(S[i] = S[i + p] (i \in [1, |S| - p])\) ,即 \(S[1, |S| - p] = S[p + 1, |S|]\) ,即 \(|S| - p\) 是 \(S\) 的 Border。
周期相关
弱周期引理(Weak Periodicity Lemma):若 \(p, q\) 是 \(S\) 的周期,且 \(p + q \leq |S|\) ,则 \(\gcd(p, q)\) 也是 \(S\) 的周期。
证明:\(p = q\) 显然,不妨设 \(p < q\) 。
对于 \(i > q\) ,有 \(S[i] = S[i - q] = S[i - q + p]\) 。
对于 \(q - p < i \leq q\) ,\(i + p \leq |S|\) ,因此 \(S[i] = S[i + p] = S[i - q + p]\) 。
故 \(S[i] = S[i - q + p] (i \in [q - p + 1, |S|])\) ,即 \(q - p\) 是 \(S\) 的周期。而 \((q - p) + p \leq |S|\) ,模拟欧几里得算法过程即可得证。
周期引理(Periodicity Lemma):若 \(p, q\) 是 \(S\) 的周期,且 \(p + q - \gcd(p, q) \leq |S|\) ,则 \(\gcd(p, q)\) 也是 \(S\) 的周期。
证明:然而并不会。
短周期结构:\(S\) 的所有不超过 \(\dfrac{|S|}{2}\) 的周期都是其最短周期的倍数。
字符串周期结构: \(S\) 的所有周期可以形成 \(O(\log n)\) 个值域不交的等差数列。
证明:由于周期与 Border 一一对应,上面两个结论转化为 Border 的形式即可证明。
Border 相关
记 \({\rm Border}(S)\) 为 \(S\) 的所有 Border 构成的集合。
显然的结论:\({\rm Border}(S) = \max \{ {\rm Border}(S) \} + {\rm Border}(\max \{ {\rm Border}(S) \})\) 。
在 KMP 中,\(S\) 的所有 Border 就是 fail 树上的一条链。
定理:
- 若 \(S\) 是 \(T\) 的前缀,且 \(T\) 有周期 \(a\) , \(S\) 有整周期 \(b\) 满足 \(b|a\) 且 \(|S|\geq a\) ,则 \(T\) 也有周期 \(b\) 。
- 若 \(|S| \geq \dfrac{|T|}{2}\) ,则 \(S\) 在 \(T\) 中的匹配位置必为等差序列。
证明:手动图解即可。
长 Border 结构:\(S\) 所有不小于 \(\dfrac{|S|}{2}\) 的 Border 构成等差数列,且如果排序后延申这个数列,下一项就是 \(|S|\) 。
证明:设 \(|S| - p\) 为 \(S\) 最长的 Border,另一个 Border 长度为 \(|S| - q\) ,且 \(p, q \leq \dfrac{|S|}{2}\) 。
由弱周期引理得到 \(\gcd(p, q)\) 也是 \(S\) 的周期,因此存在长度为 \(|S| - \gcd(p, q)\) 的 Border 。又 \(|S| - p\) 是最长的 Border,因此 \(\gcd(p, q) \geq p\) ,即 \(p | q\) 。
故 \(S\) 所有不小于 \(\dfrac{|S|}{2}\) 的 Border 构成公差为 \(p\) 的等差数列。
字符串 Border 结构:\(S\) 的所有 Border 可以形成至多 \(\lceil \log_2 S \rceil\) 个值域不交的等差数列。
证明:设 \(S\) 最长的 Border 为 \(b_0\) ,则长度不小于 \(\dfrac{b_0}{2}\) 的 Border 与 \(b_0\) 构成等差数列。再设最长的 \(< \dfrac{b_0}{2}\) 的 Border 为 \(b_1\) ,同理长度在 \([\dfrac{b_1}{2}, b_1]\) 间的 Border 构成一个等差数列。由于 \(b_i < \dfrac{b_{i - 1}}{2}\) ,因此得证。
推论:\(S\) 公差 \(\geq d\) 的 Border 的等差数列总大小是 \(O(\dfrac{n}{d})\) 的。
失配树
定义:建立一个 \(n + 1\) 个点的图,编号为 \(0 \sim n\) ,对于 \(i \geq 1\) 从 \(i\) 向 \(fail_i\) 连边,这构成了一棵树,称为失配树或 Fail 树。
称 \(fail_i\) 为 \(i\) 的失配指针,那么一个前缀的所有 Border 即它在失配树上的所有祖先。
给出字符串 \(S\) ,\(m\) 次询问两个前缀的共同真 Border。
\(|S| \leq 10^6\)
两个前缀的共同真 Border 就是树上的 LCA。注意是真 Border,若构成祖先关系还要向上跳一次。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7, LOGN = 23;
int fa[N][LOGN], dep[N];
char str[N];
int n, m;
signed main() {
scanf("%s", str + 1);
n = strlen(str + 1);
fa[0][0] = 0, dep[0] = 0;
fa[1][0] = 0, dep[1] = 1;
for (int i = 2, j = 0; i <= n; ++i) {
while (j && str[j + 1] != str[i])
j = fa[j][0];
if (str[j + 1] == str[i])
++j;
dep[i] = dep[fa[i][0] = j] + 1;
}
for (int j = 1; j < LOGN; ++j)
for (int i = 1; i <= n; ++i)
fa[i][j] = fa[fa[i][j - 1]][j - 1];
scanf("%d", &m);
while (m--) {
int x, y;
scanf("%d%d", &x, &y);
if (dep[x] < dep[y])
swap(x, y);
for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1)
if (h & 1)
x = fa[x][i];
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
printf("%d\n", fa[x][0]);
}
return 0;
}
应用
给出串 \(S\) ,对于所有前缀 \(S[1, i]\) 求其长度不超过 \(\frac{i}{2}\) 的 Border 数量。
\(|S| \leq 10^6\)
求出 fail 指针后,考虑从后往前遍历前缀。若一个 Border 的长度大于 \(\frac{i}{2}\) ,则在时候往前跳的过程中一定也不合法,于是只要每次把不合法的 fail 路径压缩一下即可做到线性时间复杂度。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e6 + 7;
int fail[N], len[N];
char str[N];
int n;
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%s", str + 1);
n = strlen(str + 1);
len[1] = 1;
for (int i = 2, j = 0; i <= n; ++i) {
while (j && str[j + 1] != str[i])
j = fail[j];
if (str[j + 1] == str[i])
++j;
fail[i] = j, len[i] = len[fail[i]] + 1;
}
int ans = 1;
for (int i = n; i; --i) {
vector<int> vec;
while (fail[i] > i / 2)
vec.emplace_back(fail[i]), fail[i] = fail[fail[i]];
ans = 1ll * ans * (len[fail[i]] + 1) % Mod;
for (int it : vec)
fail[it] = fail[i];
}
printf("%d\n", ans);
}
return 0;
}
序列自动机
序列自动机(Subsequence automaton)是接受且仅接受一个字符串的子序列的自动机。
原理
状态
序列自动机一共有 \(|S| + 1\) 个状态,每个状态表示一个子序列 \(T\) 第一次在 \(S\) 出现时的末尾位置。
转移
即字符 \(c\) 下一次出现的位置。
实现
字符集较小
从后往前倒序枚举 \(i\) ,维护 \(to_{i, c}\) 。枚举时维护一个 \(las_c\) 表示 \(c\) 在当前后缀中出现最前的位置,每次更新时令 \(to_i \leftarrow las_c\) ,再令 \(las_{S[i]} = i\) 即可。
namespace SeqAM {
int nxt[N][S];
inline void build(char *str, int len) {
memset(nxt[len], -1, sizeof(nxt[len]));
for (int i = len; i; --i) {
memcpy(nxt[i - 1], nxt[i], sizeof(nxt[i]));
nxt[i - 1][str[i] - 'a'] = i;
}
}
inline bool query(char *str, int len) {
int u = 0;
for (int i = 1; i <= n; ++i) {
u = nxt[u][str[i] - 'a'];
if (u == -1)
return false;
}
return true;
}
} // namespace SeqAM
字符集较大
考虑用主席树维护,\(i\) 代表的线段树就是 \(to_i\) 。每次都是单点修改一个 \(las\) ,所以时间复杂度是 \(O(n \log |\sum|)\) 的。
namespace SeqAM {
namespace SMT {
const int SIZE = N << 5;
int lc[SIZE], rc[SIZE], val[SIZE];
int rt[N];
int tot;
int update(int x, int nl, int nr, int pos, int k) {
int y = ++tot;
lc[y] = lc[x], rc[y] = rc[x];
if (nl == nr)
return val[y] = k, y;
int mid = (nl + nr) >> 1;
if (pos <= mid)
lc[y] = update(lc[x], nl, mid, pos, k);
else
rc[y] = update(rc[x], mid + 1, nr, pos, k);
return y;
}
int query(int x, int nl, int nr, int pos) {
if (!x)
return -1;
if (nl == nr)
return val[x];
int mid = (nl + nr) >> 1;
return pos <= mid ? query(lc[x], nl, mid, pos) : query(rc[x], mid + 1, nr, pos);
}
} // namespace SMT
inline void build(int *str, int len) {
for (int i = n; i; --i)
SMT::rt[i - 1] = SMT::update(SMT::rt[i], 1, m, str[i], i);
}
inline bool query(int *str, int len) {
int u = 0;
for (int i = 1; i <= len; ++i) {
u = SMT::query(SMT::rt[u], 1, m, str[i]);
if (u == -1)
return false;
}
return true;
}
} // namespace SeqAM
扩展
可以用一种更简洁的方法构建自动机。
给每一个字符开一个 vector
,存储着这个字符出现的所有下标。每次查询 \(to_{i, c}\) ,就是在 \(c\) 对应的 vector
里面二分出第一个 \(\geq i\) 的下标即可。
应用
给定 \(a_{1 \sim n}\) ,\(q\) 次询问一个序列是否为 \(a\) 的子序列。
\(n, q, a_i \leq 10^5\)
用主席树构建出序列自动机后暴力跑即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
int a[N], b[N];
int testid, n, q, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
namespace SeqAM {
namespace SMT {
const int SIZE = N << 5;
int lc[SIZE], rc[SIZE], val[SIZE];
int rt[N];
int tot;
int update(int x, int nl, int nr, int pos, int k) {
int y = ++tot;
lc[y] = lc[x], rc[y] = rc[x];
if (nl == nr)
return val[y] = k, y;
int mid = (nl + nr) >> 1;
if (pos <= mid)
lc[y] = update(lc[x], nl, mid, pos, k);
else
rc[y] = update(rc[x], mid + 1, nr, pos, k);
return y;
}
int query(int x, int nl, int nr, int pos) {
if (!x)
return -1;
if (nl == nr)
return val[x];
int mid = (nl + nr) >> 1;
return pos <= mid ? query(lc[x], nl, mid, pos) : query(rc[x], mid + 1, nr, pos);
}
} // namespace SMT
inline void build(int *str, int len) {
for (int i = n; i; --i)
SMT::rt[i - 1] = SMT::update(SMT::rt[i], 1, m, str[i], i);
}
inline bool query(int *str, int len) {
int u = 0;
for (int i = 1; i <= len; ++i) {
u = SMT::query(SMT::rt[u], 1, m, str[i]);
if (u == -1)
return false;
}
return true;
}
} // namespace SeqAM
signed main() {
testid = read(), n = read(), q = read(), m = read();
for (int i = 1; i <= n; ++i)
a[i] = read();
SeqAM::build(a, n);
while (q--) {
int len = read();
for (int i = 1; i <= len; ++i)
b[i] = read();
puts(SeqAM::query(b, len) ? "Yes" : "No");
}
return 0;
}
P3856 [TJOI2008] 公共子串 P1819 公共子序列
求三个串的不同公共子序列数量。
\(n \leq 100\)
设 \(f_{x, y, z}\) 表示在第一个串以 \(x\) 开始、第二个串以 \(y\) 开始、第三个串以 \(z\) 开始的公共子序列数量,不难记忆化搜索实现,转移时枚举序列自动机上的公共边即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e2 + 7, S = 27;
struct SeqAM {
int nxt[N][S];
inline void build(char *str, int len) {
memset(nxt[len], -1, sizeof(nxt[len]));
for (int i = len; i; --i) {
memcpy(nxt[i - 1], nxt[i], sizeof(nxt[i]));
nxt[i - 1][str[i] - 'a'] = i;
}
}
} A, B, C;
ll f[N][N][N];
char a[N], b[N], c[N];
ll dfs(int x, int y, int z) {
if (~f[x][y][z])
return f[x][y][z];
f[x][y][z] = (x || y || z);
for (int i = 0; i < S; ++i)
if (~A.nxt[x][i] && ~B.nxt[y][i] && ~C.nxt[z][i])
f[x][y][z] += dfs(A.nxt[x][i], B.nxt[y][i], C.nxt[z][i]);
return f[x][y][z];
}
signed main() {
scanf("%s%s%s", a + 1, b + 1, c + 1);
A.build(a, strlen(a + 1)), B.build(b, strlen(b + 1)), C.build(c, strlen(c + 1));
memset(f, -1, sizeof(f));
printf("%lld", dfs(0, 0, 0));
return 0;
}
Aho-Corasick 自动机
在一般的字符串匹配问题中,一般会给定若干模式串 \(T_{1 \sim m}\) 与若干文本串 \(S_{1 \sim n}\) ,需要统计 \(T_i\) 在 \(S_j\) 的出现情况。
AC 自动机融合了:
- Trie 结构:对所有的模式串建一棵 Trie 。
- KMP 思想:对 Trie 树上所有的结点构建失配指针。
其通常被用于解决多模式匹配等任务。
构建 fail 树
先建立模式串的 Trie 树,并定义状态 \(u\) 的 fail 指针指向另一个状态 \(v\) 满足 \(v\) 是 \(u\) 的最长真后缀。考虑利用部分已经求出的 fail 指针推导当前节点的 fail 值,采用 BFS 实现。
设当前求解的点为 \(u\) ,其父亲为 \(p\) ,\(p\) 通过字符 \(c\) 的边指向 \(u\)
- 若 \(fail_p\) 通过 \(c\) 连接到的子节点 \(w\) 存在,则令 \(fail_u \leftarrow w\) 。
- 否则继续找 \(fail_{fail_p}\) ,重复上一步直到跳到根为止。
- 若还是没有,则说明没有真后缀,令 \(fail_u \leftarrow rt\) 。
实际上并不会真正跳这么多遍,可以用路径压缩优化。若 \(u\) 没有字符 \(c\) 的出边,则令 \(ch_{u, v} \gets ch_{fail_u, c}\) 即可。
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
}
文本串匹配
一个文本串匹配多个模式串。
考虑对于所有模式串建立 AC 自动机,则文本串在 AC 自动机上跑到点 \(u\) 时,\(u\) 在 fail 树上到根的链上所有的终止节点都会匹配,求和即可。
inline void query(char *str) {
int u = 1, res = 0;
for (int i = 1, len = strlen(str + 1); i <= len; ++i) {
u = ch[u][str[i] - 'a'];
for(int j = u; j; j = fail[j])
if(ed[j])
++cnt[ed[j]];
}
}
注意到匹配实际上就是一个 fail 树上的链上求和,那么我们只要在每次跑到的节点上权值 \(+1\) ,最后做一遍子树求和即可。
inline void query(char *str) {
int u = 1;
for (int i = 1, len = strlen(str + 1); i <= len; ++i)
++cnt[u = ch[u][str[i] - 'a']];
}
inline void solve() {
queue<int> q;
for (int i = 1; i <= tot; ++i)
if (!indeg[i])
q.emplace(i);
while (!q.empty()) {
int u = q.front();
q.pop();
int v = fail[u];
--indeg[v], cnt[v] += cnt[u];
if (!indeg[v])
q.emplace(v);
}
}
应用
有 \(n\) 只喵,每只喵有一个名和一个姓(两个字符串),还有 \(m\) 次点名(也是一个字符串),如果一只喵的名或姓中包含这个字符串,这只喵就会喊到。求:
- 对于每次点名询问有多少只喵喊到。
- 对于每一只喵问询它喊了多少次到。
\(|\sum| \leq 10^4\) ,\(\sum |S| \leq 2 \times 10^5\)
把一只喵的名和姓合并在一起,中间插入一个无关字符,建立 AC 自动机。
- 第一问:树上链求并,直接对跳到的点按 dfs 序排序,每个点到根链上的贡献 \(+1\) ,相邻节点的 LCA 到链的贡献 \(-1\) ,树上差分即可。
- 第二问:按第一问的方法计算贡献后就是一个子树求和,同样可以用树上差分将子树并转化为到根的链的并。
注意本题字符集比较大,需要用 map
存储 ch 数组。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7, LOGN = 21;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct BIT {
int c[N];
int n;
inline void prework(int _n) {
memset(c + 1, 0, sizeof(int) * (n = _n));
}
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
} bit;
int fa[N][LOGN];
int nameid[N], qryid[N], dep[N], siz[N], dfn[N];
int n, m, dfstime;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
namespace ACAM {
map<int, int> ch[N];
int fa[N], fail[N];
int tot = 1;
inline int insert(vector<int> &vec) {
int u = 1;
for (int it : vec) {
if (ch[u].find(it) == ch[u].end())
fa[ch[u][it] = ++tot] = u;
u = ch[u][it];
}
return u;
}
int getfail(int u, int c) {
if (ch[u].find(c) != ch[u].end())
return ch[u][c];
else if (!u)
return 1;
else
return ch[u][c] = getfail(fail[u], c);
}
inline void build() {
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (auto it : ch[u])
fail[it.second] = getfail(fail[u], it.first), q.emplace(it.second);
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
}
} // namespace ACAM
using ACAM::tot;
void dfs(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1, siz[u] = 1, dfn[u] = ++dfstime;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : G.e[u])
dfs(v, u), siz[u] += siz[v];
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1)
if (h & 1)
x = fa[x][i];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= n; ++i) {
int len = read();
vector<int> vec;
while (len--)
vec.emplace_back(read());
vec.emplace_back(-1), len = read();
while (len--)
vec.emplace_back(read());
nameid[i] = ACAM::insert(vec);
}
for (int i = 1; i <= m; ++i) {
int len = read();
vector<int> vec;
while (len--)
vec.emplace_back(read());
qryid[i] = ACAM::insert(vec);
}
ACAM::build(), dfs(1, 0);
bit.prework(tot);
for (int i = 1; i <= n; ++i) {
vector<int> kp;
for (int u = nameid[i]; u; u = ACAM::fa[u])
kp.emplace_back(u), bit.update(dfn[u], 1);
sort(kp.begin(), kp.end(), [](const int &a, const int &b) { return dfn[a] < dfn[b]; });
for (int i = 1; i < kp.size(); ++i)
bit.update(dfn[LCA(kp[i - 1], kp[i])], -1);
}
for (int i = 1; i <= m; ++i)
printf("%d\n", bit.query(dfn[qryid[i]] + siz[qryid[i]] - 1) - bit.query(dfn[qryid[i]] - 1));
bit.prework(tot);
for (int i = 1; i <= m; ++i)
bit.update(dfn[qryid[i]], 1), bit.update(dfn[qryid[i]] + siz[qryid[i]], -1);
for (int i = 1; i <= n; ++i) {
vector<int> kp;
int res = 0;
for (int u = nameid[i]; u; u = ACAM::fa[u])
kp.emplace_back(u), res += bit.query(dfn[u]);
sort(kp.begin(), kp.end(), [](const int &a, const int &b) { return dfn[a] < dfn[b]; });
for (int i = 1; i < kp.size(); ++i)
res -= bit.query(dfn[LCA(kp[i - 1], kp[i])]);
printf("%d ", res);
}
return 0;
}
给定 \(n\) 个串 \(S_{1 \sim n}\) ,求有多少对 \((i, j)\) 满足:
- \(i \neq j\) 。
- \(S_j\) 是 \(S_i\) 的子串。
- 不存在 \(k \ (k \neq i, k \neq j)\) 满足 \(S_j\) 是 \(S_k\) 的子串且 \(S_k\) 是 \(S_i\) 的子串。
\(\sum |S_i| \leq 10^6\)
考虑枚举 \(i\) ,计算 \(j\) 的数量。考虑 \(S_i\) 的每个前缀 \(S_i[1, k]\) ,显然在其所有可能的后缀中只有最长的后缀可能合法。进一步的,一个后缀合法当且仅当其左端点要小于后面一个 \(k\) 对应的左端点,否则就会被包含。一个细节是如果直接判会出现某个串在一个位置合法,但是在另一个位置不合法的情况,需要判一下出现次数与完全合法次数是否相等,出现次数直接做一个子树求和就行了。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
struct BIT {
int c[N];
int n;
inline void prework(int _n) {
memset(c + 1, 0, sizeof(int) * (n = _n));
}
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline int ask(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
inline int query(int l, int r) {
return ask(r) - ask(l - 1);
}
} bit;
int len[N], lpos[N], idx[N], cnt[N];
char _str[N], *str[N];
int n;
namespace ACAM {
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int ch[N][26], fail[N], anc[N], len[N], in[N], out[N];
int tot = 1, dfstime;
inline void insert(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int idx = str[i] - 'a';
if (!ch[u][idx])
ch[u][idx] = ++tot;
u = ch[u][idx];
}
anc[u] = u, len[u] = n;
}
inline void build() {
fill(ch[0], ch[0] + 26, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
}
void dfs(int u) {
in[u] = ++dfstime;
for (int v : G.e[u]) {
if (!anc[v])
anc[v] = anc[u], len[v] = len[u];
dfs(v);
}
out[u] = dfstime;
}
} // namespace ACAM
signed main() {
scanf("%d", &n);
str[0] = _str;
for (int i = 1; i <= n; ++i) {
str[i] = str[i - 1] + len[i - 1];
scanf("%s", str[i] + 1);
len[i] = strlen(str[i] + 1);
ACAM::insert(str[i], len[i]);
}
ACAM::build(), ACAM::dfs(1), bit.prework(ACAM::tot);
int ans = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1, u = 1; j <= len[i]; ++j) {
u = ACAM::ch[u][str[i][j] - 'a'], bit.update(ACAM::in[u], 1);
if (j == len[i])
u = ACAM::fail[u];
lpos[j] = j - ACAM::len[u] + 1, idx[j] = ACAM::anc[u];
}
vector<int> kp;
for (int j = len[i], p = len[i]; j; p = min(p, lpos[j--] - 1))
if (lpos[j] <= min(j, p))
kp.emplace_back(idx[j]), ++cnt[idx[j]];
for (int it : kp) {
if (!cnt[it])
continue;
if (bit.query(ACAM::in[it], ACAM::out[it]) == cnt[it])
++ans;
cnt[it] = 0;
}
for (int j = 1, u = 1; j <= len[i]; ++j)
u = ACAM::ch[u][str[i][j] - 'a'], bit.update(ACAM::in[u], -1);
}
printf("%d", ans);
return 0;
}
给出一个字符串 $ s $ 和 $ n $ 个字符串 $ p_i $,求每个字符串 $ p_i $ 在 $ s $ 中出现的次数。
注意这里两个字符串相等的定义稍作改变:给定一个常数 $ k $ ,对于两个字符串 \(a, b\),\(a = b\) 等价于:
- \(|a| = |b|\) 。
- \(|a| = |b| \le k\) 或 \(\forall i, j, a_i \neq b_i, a_j \neq b_j, |i - j| < k\) 。
\(|s|, \sum |p_i| \leq 2 \times 10^5\)
不难发现相等的条件就是 \(LCP(a, b) + LCS(a, b) + k \geq |a| = |b|\) ,考虑固定 LCP 求合法的 LCS 数量。
对所有 \(p_i\) 的正反串一起建立 AC 自动机,对于前缀 \(p_i[1, j]\) ,将 \(j + k + 1\) 后缀对应点挂在 \(j\) 对应前缀点上,计算每个前缀的贡献和。
统计答案时,遍历一遍 fail 树,当到达点 \(u\) 后,将点 \(u\) 子树内的节点上挂着文本串对应的后缀节点都 \(+1\) ,对于询问,查询模式串对应的后缀节点的子树和即可,这样就统计出了当前 LCP 所对应的合法的 LCS 。
但是直接这样做会算重,当 \(lcp + lcs + k > |s|\) 时,一个匹配的位置会计算多次。考虑钦定此时令 LCS 取到最大值(即 LCP 取到最小值)才统计贡献,可以通过在节点上再挂上 \(j + k\) 位置的后缀对应的节点减去其贡献做到。需要注意当 LCP 为空的时候不用减去贡献,因为此时 LCP 一定最小(空)。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct BIT {
int c[N];
int n;
inline void prework(int _n) {
memset(c + 1, 0, sizeof(int) * (n = _n));
}
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline int ask(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
inline int query(int l, int r) {
return ask(r) - ask(l - 1);
}
} bit1, bit2;
struct Node {
int id, p1, p2;
};
vector<Node> qry[N];
vector<pair<int, int> > upd[N];
int ans[N], in[N], out[N];
char str[N], p[N];
int k, m, n, dfstime;
namespace ACAM {
const int S = 127;
int ch[N][S], suf[N], fail[N];
int tot = 1;
inline void insert(char *str, int len, int id) {
for (int i = 1, u = 1; i <= len; ++i) {
if (!ch[u][str[i]])
ch[u][str[i]] = ++tot;
u = ch[u][str[i]];
}
suf[len + 1] = 1;
for (int i = len, u = 1; i; --i) {
if (!ch[u][str[i]])
ch[u][str[i]] = ++tot;
suf[i] = u = ch[u][str[i]];
}
for (int i = 0, u = 1; i <= len - k; ++i) {
qry[u].emplace_back((Node) {id, suf[i + k + 1], i ? suf[i + k] : -1});
u = ch[u][str[i + 1]];
}
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
memset(suf + 1, 0, sizeof(int) * n);
suf[n + 1] = 1;
for (int i = n, u = 1; i; --i)
suf[i] = u = ch[u][str[i]];
for (int i = 0, u = 1; i <= n - k; ++i) {
upd[u].emplace_back(suf[i + k + 1], suf[i + k]);
u = ch[u][str[i + 1]];
}
}
} // namespace ACAM
void dfs1(int u) {
in[u] = ++dfstime;
for (int v : G.e[u])
dfs1(v);
out[u] = dfstime;
}
void dfs2(int u) {
for (Node it : qry[u]) {
ans[it.id] -= bit1.query(in[it.p1], out[it.p1]);
if (~it.p2)
ans[it.id] -= bit2.query(in[it.p2], out[it.p2]);
}
for (auto it : upd[u])
bit1.update(in[it.first], 1), bit2.update(in[it.second], -1);
for (int v : G.e[u])
dfs2(v);
for (Node it : qry[u]) {
ans[it.id] += bit1.query(in[it.p1], out[it.p1]);
if (~it.p2)
ans[it.id] += bit2.query(in[it.p2], out[it.p2]);
}
}
signed main() {
scanf("%d%s%d", &k, str + 1, &m);
n = strlen(str + 1);
for (int i = 1; i <= m; ++i) {
scanf("%s", p + 1);
int len = strlen(p + 1);
if (len <= k)
ans[i] = n - len + 1;
else
ACAM::insert(p, len, i);
}
ACAM::build(), dfs1(1);
bit1.prework(ACAM::tot), bit2.prework(ACAM::tot);
dfs2(1);
for (int i = 1; i <= m; ++i)
printf("%d\n", ans[i]);
return 0;
}
给出一个包含小写字符和
B
、P
的字符串,分别表示:
- 小写字符:在当前串末尾插入该字符。
B
:删除当前串末尾字符。P
:打印当前串。\(m\) 次询问,每个询问第 \(x\) 次打印的字符串在第 \(y\) 次打印的字符串中的出现次数。
\(n, m \leq 10^5\)
不难发现三种操作都可以在 Trie 树上单次 \(O(1)\) 完成,于是可以线性构建出 Trie 树。询问则可以转化为 \(S_y\) 在 Trie 树上的祖先链中属于 \(S_x\) 在 Fail 树上子树的点数。考虑离线,在 Trie 上每个 \(y\) 处记录询问,然后类似构建的给当前串的每个前缀打上标记,查询就是 Fail 树上的处理子树查询。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct BIT {
int c[N];
int n;
inline void prework(int _n) {
memset(c + 1, 0, sizeof(int) * (n = _n));
}
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline int ask(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
inline int query(int l, int r) {
return ask(r) - ask(l - 1);
}
} bit;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<pair<int, int> > qry[N];
int ans[N], in[N], out[N];
char str[N];
int m, dfstime;
namespace ACAM {
int ch[N][26], fa[N], fail[N], id[N];
int tot = 1, cnt;
inline void insert(char *str) {
int u = 1;
for (int i = 1, len = strlen(str + 1); i <= len; ++i) {
if (str[i] == 'B')
u = fa[u];
else if (str[i] == 'P')
id[++cnt] = u;
else {
int idx = str[i] - 'a';
if (!ch[u][idx])
fa[ch[u][idx] = ++tot] = u;
u = ch[u][idx];
}
}
}
inline void build() {
fill(ch[0], ch[0] + 26, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
}
} // namespace ACAM
void dfs(int u) {
in[u] = ++dfstime;
for (int v : G.e[u])
dfs(v);
out[u] = dfstime;
}
signed main() {
scanf("%s%d", str + 1, &m);
ACAM::insert(str), ACAM::build();
dfs(1), bit.prework(ACAM::tot);
for (int i = 1, x, y; i <= m; ++i) {
scanf("%d%d", &x, &y);
qry[y].emplace_back(x, i);
}
for (int i = 1, len = strlen(str + 1), u = 1, id = 0; i <= len; ++i) {
if (str[i] == 'B')
bit.update(in[u], -1), u = ACAM::fa[u];
else if (str[i] == 'P') {
++id;
for (auto it : qry[id])
ans[it.second] = bit.query(in[ACAM::id[it.first]], out[ACAM::id[it.first]]);
} else
u = ACAM::ch[u][str[i] - 'a'], bit.update(in[u], 1);
}
for (int i = 1; i <= m; ++i)
printf("%d\n", ans[i]);
return 0;
}
给定 \(n\) 个 \(01\) 串 \(S_{1 \sim n}\) ,求是否存在无限长的串使得任何 \(S_i\) 均不为其子串。
\(n \leq 3 \times 10^4\)
考虑构建出 \(S_{1 \sim n}\) 的 AC 自动机,并将每个 \(S_i\) 对应结束节点在 Fail 树上的整个子树打上标记表示不能走到。问题转化为是否存在一条无限长的路径满足其不经过任何打标记的点。
考虑将未打上标记的点和它们之间的边拿出来,如果从起点出发能够到达的是一个 DAG,那么不存在这样的路径,否则只要走到一个环,就可以构造出一个无限长的路径。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e4 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int indeg[N];
char str[N];
int n;
namespace ACAM {
int ch[N][2], fail[N];
bool ed[N];
int tot = 1;
inline void insert(char *str) {
int u = 1;
for (int i = 1, len = strlen(str + 1); i <= len; ++i) {
int idx = str[i] & 15;
if (!ch[u][idx])
ch[u][idx] = ++tot;
u = ch[u][idx];
}
ed[u] = true;
}
inline void build() {
ch[0][0] = ch[0][1] = 1;
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop(), ed[u] |= ed[fail[u]];
for (int i = 0; i <= 1; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 1; i <= tot; ++i)
if (!ed[i])
for (int j = 0; j <= 1; ++j)
if (!ed[ch[i][j]])
G.insert(i, ch[i][j]), ++indeg[ch[i][j]];
}
} // namespace ACAM
using ACAM::tot;
inline bool TopoSort() {
queue<int> q;
for (int i = 1; i <= tot; ++i)
if (!indeg[i])
q.emplace(i);
int cnt = 0;
while (!q.empty()) {
int u = q.front();
q.pop(), ++cnt;
for (int v : G.e[u]) {
--indeg[v];
if (!indeg[v])
q.emplace(v);
}
}
return cnt == tot;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%s", str + 1);
ACAM::insert(str);
}
ACAM::build();
puts(TopoSort() ? "NIE" : "TAK");
return 0;
}