序列自动机

序列自动机

序列自动机(Subsequence automaton)是接受且仅接受一个字符串的子序列的自动机。

原理

状态

序列自动机一共有 \(|S| + 1\) 个状态,每个状态表示一个子序列 \(T\) 第一次在 \(S\) 出现时的末尾位置。

转移

\[\delta(u, c) = \min \{ i | i > u, S[i] = c \} \]

即字符 \(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\) 的下标即可。

应用

P5826 【模板】子序列自动机

给定 \(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\) 开始的某个子序列,则:

\[f_{i, |T| + 1} = 0 \\ f_{i, j} = \min \{ f_{to_{i, c}, to_{j, c}} + 1 \} \]

posted @ 2024-08-13 15:33  我是浣辰啦  阅读(7)  评论(0编辑  收藏  举报