序列自动机
序列自动机
序列自动机(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;
}
P4112 [HEOI2015] 最短不公共子串 仅考虑二、四两问
给定两个字符串 \(S, T\) ,求:
- \(S\) 的一个最短的子串,它不是 \(T\) 的子序列,输出其长度。
- \(S\) 的一个最短的子序列,它不是 \(T\) 的子序列,输出其长度。
\(|S|, |T| \leq 2000\)
先建出 \(S, T\) 的序列自动机。
对于第一问,枚举 \(S\) 的所有 \(i\) 开始的最短满足条件的子串即可,这部分可以做到 \(O(n^2)\) 。
对于第二问,设 \(f_{i, j}\) 表示为从 \(S[i]\) 开始的一个最短的子序列满足其不为 \(T_j\) 开始的某个子序列,则: