2024.12.16 自动机理论 & 常见自动机 讲课笔记
自动机理论
严格来说,自动机不是算法或数据结构,只是一种数学模型
符号 和 基本定义
对于
若一个语言可以被
若将状态看为点,转移函数看为边,则可以把一个
非确定有限状态自动机(NFA)
一个
对于字符串
对于
对于每个块,枚举
转移时,直接将
总时间复杂度
DFA 最小化
将
使用
首先去除无法从
定义两个状态
先将所有状态按是否被接受划分为两个等价类
每次取出一个等价类
最后将每个等价类作为新
此算法可以进一步优化
维护一个还没有被考虑过的等价类的集合
每次从
这样时间复杂度为
例 1:CF1142D Foreigner
定义正整数
定义一个合法正整数
根据题意,若合法正整数
对于一个
前者一共
因此
由于
-
\sum=[0,9]\cap\Bbb Z -
F=[0,10]\cap\Bbb Z -
Q=\{s\}\cup F -
q_0=s -
\delta(u,c)=\begin{cases}c&u=s,c\ne0\\(10+\sum_{i=0}^{u-1}i+c)&u\ne s,c<u\bmod11\end{cases}
其中状态
容易根据转移写出
代码
例 2:P4590 [TJOI2018] 游园会
给定长为
令
令
这种技巧也被称为
总时间复杂度
代码
子序列自动机
接受且仅接受给定序列的子序列的自动机
模板:
#include <bits/stdc++.h>
//subsequence automaton
namespace SsAM {
struct SsAM {
static constexpr std::size_t MAX_LENGTH = 100010;
std::size_t nx[MAX_LENGTH][26], __n;
void build(const std::string &_s, std::size_t _n){//_s begin at 1
__n = _n; static std::size_t last_pos[26]; std::fill_n(last_pos, 26, 0);
for (std::size_t i = _n; ~i; --i)std::copy_n(last_pos, 26, nx[i]), last_pos[_s[i] - 'a'] = i;
}
std::size_t delta(std::size_t _Nw, char _ch) const { return nx[_Nw][_ch - 'a']; }
bool is_subsq(const std::string &s) const//s begin at 0
{ std::size_t nw = 0; for (const char &c : s){ nw = delta(nw, c); if (!nw)return false; } return true; }
};
}
KMP 自动机(KMPAM)
将原本均摊
模板题:CF1721E Prefix Function Queries
模板:
#include <bits/stdc++.h>
namespace KMPAM {
struct KMPAM {
static constexpr std::size_t MAX_LENGTH = 100010;
std::size_t nxt[MAX_LENGTH][26], pi[MAX_LENGTH], __n;
std::string __s;
void build(const std::string &s, std::size_t n){
if (!n)return;pi[0] = pi[1] = 0;std::fill_n(nxt[0], 26, 0); __n = n; __s = s;
for (std::size_t i = 1; i <= n; ++i){
if (i > 1){
pi[i] = pi[i - 1];
while (pi[i] && s[pi[i] + 1] ^ s[i]){
if (pi[i] + 1 < i){pi[i] = nxt[pi[i]][s[i] - 'a'];break;}
pi[i] = pi[pi[i]];
}
pi[i] += s[pi[i] + 1] == s[i];
}
for (std::size_t c = 0; c < 26; ++c)nxt[i][c] = nxt[pi[i]][c];if (i < n)nxt[i][s[i + 1] - 'a'] = i;
}
}
std::size_t delta(std::size_t nw, char c){ return nxt[nw][c - 'a']; }
void append(const std::string &t){
if (!t.size())return;
__s += t; std::size_t m = t.size();
for (std::size_t c = 0; c < 26; ++c)nxt[__n][c] = nxt[pi[__n]][c];
nxt[__n][__s[__n + 1] - 'a'] = __n;
for (std::size_t i = __n + 1; i <= __n + m; ++i){
pi[i] = pi[i - 1];
while (pi[i] && __s[pi[i] + 1] ^ __s[i]){
if (pi[i] + 1 < i){pi[i] = nxt[pi[i]][__s[i] - 'a'];break;}
pi[i] = pi[pi[i]];
}
pi[i] += __s[pi[i] + 1] == __s[i];
for (std::size_t c = 0; c < 26; ++c)nxt[i][c] = nxt[pi[i]][c];if (i < __n + m)nxt[i][__s[i + 1] - 'a'] = i;
}
__n += m;
}
void resize(std::size_t _N){ __n = _N; __s.resize(_N + 1); }
};
}
AC 自动机(ACAM)
模板题:P5357 【模板】AC 自动机
模板:
#include <bits/stdc++.h>
namespace ACAM {
struct ACAM {
typedef std::size_t size_t;
static constexpr std::size_t MAX_LENGTH = 200010;
size_t trie[MAX_LENGTH][26], tot, fail[MAX_LENGTH], deg[MAX_LENGTH];
ACAM():tot(0){
std::fill_n(*trie, sizeof(trie) / sizeof(**trie), 0);
std::fill_n(fail, sizeof(fail) / sizeof(*fail), 0);
std::fill_n(deg, sizeof(deg) / sizeof(*deg), 0);
}
size_t insert(const std::string &_s)//begin at 0
{ size_t nw = 0; for (const char &c : _s){ size_t &_nx = trie[nw][c - 'a']; if (!_nx)_nx = ++tot; nw = _nx; } return nw; }
void build(){
std::queue<size_t> _q; for (const size_t &i : trie[0])if (i)_q.emplace(i);
while (!_q.empty()){
size_t _t = _q.front(); _q.pop();
for (size_t i = 0; i < 26; ++i)
if (trie[_t][i])++deg[fail[trie[_t][i]] = trie[fail[_t]][i]], _q.emplace(trie[_t][i]);
else trie[_t][i] = trie[fail[_t]][i];
}
}
size_t delta(size_t nw, char _ch) const { return trie[nw][_ch - 'a']; }
std::vector<size_t> get_match_count(const std::vector<std::string> _v, const std::string &_s){
size_t __cnt = 0; std::vector<size_t> edps(_v.size());
for (const std::string &_s : _v) edps[__cnt++] = insert(_s);
build(); std::vector<size_t> __tmp(tot + 1);
size_t nw = 0; for (const char &_c : _s)++__tmp[nw = delta(nw, _c)];
std::queue<size_t> _q; for (size_t i = 0; i <= tot; ++i)if (!deg[i])_q.emplace(i);
while (!_q.empty()){
size_t _t = _q.front(); _q.pop(); __tmp[fail[_t]] += __tmp[_t];
if (!--deg[fail[_t]])_q.emplace(fail[_t]);
}
std::vector<size_t> ret(_v.size());
for (size_t _i = 0, _ = _v.size(); _i < _; ++_i)ret[_i] = __tmp[edps[_i]]; return ret;
}
};
}//passed P5357
其常用于多模式字符串匹配
例 1:QOJ # 6842. Popcount Words
令
将所有询问离线并建出
实际上
定义
因此可以将
令
最后所有覆盖次数都存在
总时间复杂度
代码
例 2:P8147 [JRKSJ R4] Salieri
给定
先用
用
二分答案,转化为求
对
直接高斯消元有
在
当处理到某个标记节点时,相当于新得到一个方程
处理非标记节点时,则选择其一个儿子为重儿子,其余轻儿子全部设为未知量,重儿子的答案可以用其父亲、其父亲的亲儿子、其父亲非儿子转移节点(显然它们深度更小,此时其表示已被求出)表示出
总未知量数量为轻儿子总数加一,等于
于是可以高斯消元
然后回带即可求出每个节点的答案
总时间复杂度
代码
回文树 / 回文自动机(PAM)
维护某个序列所有回文子串的结构,严格来说不是自动机,但形式相近
模板题:P5496 【模板】回文自动机(PAM)
模板:
#include <bits/stdc++.h>
namespace PAM {
struct PAM {
typedef std::size_t size_t;
static constexpr size_t MAX_LENGTH = 500010;
std::string __s; size_t __lst;
size_t tot, nx[MAX_LENGTH][26], Ln[MAX_LENGTH], fail[MAX_LENGTH], hlf[MAX_LENGTH], __f[MAX_LENGTH];
//str hlf[x] is the longest str that str hlf[x] is str x's suffix and len(hlf[x])<=len(x)/2
size_t _tot[MAX_LENGTH], _cnt[MAX_LENGTH];
size_t __i;
PAM():tot(1), __s("."), __i(0){
std::fill_n(*nx, sizeof(nx) / sizeof(**nx), 0);
std::fill_n(Ln, MAX_LENGTH, 0);
std::fill_n(fail, MAX_LENGTH, 0);
std::fill_n(hlf, MAX_LENGTH, 0);
std::fill_n(_tot, MAX_LENGTH, 0);
std::fill_n(_cnt, MAX_LENGTH, 0);
fail[0] = 1;Ln[1] = -1;hlf[1] = 1;__i = 0;__lst = 0;
}
size_t _get_fail(size_t __nw)
{while (__nw ^ 1 && __s[__i - Ln[__nw] - 1] != __s[__i])__nw = fail[__nw];return __nw;}
size_t insert(char c){
__s.push_back(c);++__i;
size_t nw = _get_fail(__lst);
if (!nx[nw][c - 'a']){
Ln[++tot] = Ln[nw] + 2;
fail[tot] = nx[_get_fail(fail[nw])][c - 'a'];
_tot[tot] = _tot[fail[tot]] + 1;
nx[nw][c - 'a'] = tot;
if (Ln[tot] < 2)hlf[tot] = 0;
else {
int x = hlf[nw];
while (x ^ 1 && (Ln[x] + 2 > Ln[tot] >> 1 || !nx[x][c - 'a'] || __s[__i - Ln[x] - 1] ^ __s[__i]))x = fail[x];
hlf[tot] = nx[x][c - 'a'];
}
__f[tot] = nw;
}
++_cnt[__lst = nx[nw][c - 'a']];
return _tot[__lst];
}
void build(const std::string s){for (const char &c : s)insert(c);}
size_t delta(size_t nw, char ch){return nx[nw][ch - 'a'];}
};
}
由其建立过程可知,某个字符串本质不同的回文子串数量不超过其长度
例 1:P4762 [CERC2014] Virus synthesis
一个字符串初始为空,每次可以在其开头或结尾加一个字符,或在其开头或结尾拼接其逆串(以下称此操作为翻转),求构造出给定字符串
先对原字符串建立
令
可证对于一个偶回文串,其一定存在一种最优操作方式,满足最后一步为翻转
因此转移有两种(奇回文串不用转移),从父节点转移 和 从其祖先中最长的满足长度不超过其一半的偶串(若不存在则忽略这种转移)转移
前者只要在父节点翻转前在末尾增一个字符即可,
后者需要补足一半的长度然后翻转,令满足要求的节点为
总时间复杂度
代码
例 2:bzoj#P3103. Palindromic Equivalence
给定
在建立
具体地,在
将相同的用并查集合并为一个等价类,不同的等价类之间连边(代码中是将等价类之间的边全部连在等价类中编号最小的点上)
按编号从小到大考虑每个等价类,这个等价类的选择有
时间复杂度
代码
例 3:QOJ # 5440. [The 2022 ICPC Asia Shenyang Regional Contest] [The 1st Universal Cup. Stage 1: Shenyang] P-P-Palindrome
给定
对于一组
原问题等价于对
若
可证若
对
答案即为每个节点
另一种统计答案的方式(代码中的方式)为对于
时间复杂度
代码
例 4:P5433 月宫的符卡序列
给点字符串
先求出
每个回文串保存一个值
则每个极长回文子段会令其对应节点到根路径上所有点的
打上标记最后统一上传即可
时间复杂度
代码
vjudge 练习
FSM,pw:AstroBot
参考
oi-wiki AC 自动机
oi-wiki 回文树
FSM.pdf by Mine_King
字符串乱学 by Mine_King