AC 自动机

AC 自动机

自动机 (DFA)

「自动机」的英文是 Automaton .

OI 中「自动机」一般指「确定性有限状态自动机」(Deterministic Finite Automaton,DFA)

基本定义

DFA 一般是识别字符串,一个自动机 \(A\),若他能识别字符串 \(S\),则 \(A(S)=\mathbf{true}\),否则 \(A(S)=\mathbf{false}\) .

然后这个识别咋定义呢?当一个自动机读入一个字符串时,从初始状态(根节点)起按照转移函数一个一个字符地转移 . 如果读入完一个字符串的所有字符后位于一个接受状态,那么我们称这个自动机 接受 这个字符串,反之我们称这个自动机 不接受 这个字符串 .(这个接受其实就是识别吧)

形式化定义

其实弄懂了基本定义,形式化定义就很明了了 .

一个 DFA 由如下五个东西

  • 状态集合 \(Q\) .
  • 字符集 \(\Sigma\) .
  • 状态转移函数 \(\delta : Q\times \Sigma\to Q\) .
  • 一个开始状态 \(s\in Q\)(即根节点)
  • 接收状态集合 \(F\subseteq Q\) .

组成的五元组 \((Q,\Sigma,\sigma,s,F)\) .

把不能转移的都定义成 \(\mathbf{null}\) .

你定义一个 \(\delta(v, s) = \delta(\delta(v, s[1]), s[2:|S|])\),然后就可以定义接受 \(A(S) = [\delta(s, S) \in F]\) 了,实际上这个新的 \(\delta\)(「ex - \(\delta\)」)也就是沿着旧 \(\delta\) 走的过程 .

常见自动机

  • Trie:转移函数就是 Trie 上一条边,其能接受的字符串就是插入到 Trie 中的字符串(或者其前缀,这取决于怎么定义接受)
  • 子序列自动机:能接受的字符串是给定字符串的所有子序列,转移函数 \(trans(x, c)\) 是在字符 \(c\) 对应的 \(\texttt{vector}\)\(\texttt{upper_bound}(x)\) 得到的返回值 . 每个节点都可以看作接受状态 .
  • KMP 自动机:\(s\) 构造 的自动机能接受的字符串是以 \(s\) 为后缀的串 \(t\),转移函数就是不断跳 next 的过程(形式化的,here

然后就是我们现在要说的 AC 自动机,还有比较牛逼的(广义)后缀自动机(SAM),回文自动机(PAM)啥的,然而我不会啊 qwq

AC 自动机基本原理

AC 自动机的全称是 Aho-Corasick Automaton .
其中 Aho-Corasick 是人名 Alfred Aho 和 Margaret Corasick .

简单来说就是 Trie 树上 KMP .

这里我们的状态集合 \(Q\) 是 Trie 上的所有节点 .

失配指针(fail)

定义:\(u\) 的 fail 指针 \(\operatorname{fail}(u)\) 指向其表示字符串的最长后缀在原 Trie 树上的节点 .

构建类似 KMP 的过程(因为是树上的玩意,所以要 BFS(这里不能 DFS 原因是其实这玩意是个「字典图」,后面说)) .

KMP 匹配的时候是不是要跳 next,然而然后我们可以真的把自动机建出来,这样就没 fail 啥事了,直接跳就完了 .

相当于把 Trie 树补成 Trie 图

// Sig 是字符集大小 .
inline void build()
{
    queue<int> q; fail[root] = root;
    for (int i=0; i<Sig; i++)
    {
        if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
        else tr[root][i] = root;
    }
    while (!q.empty())
    {
        int u = q.front(); q.pop();
        for (int i=0; i<Sig; i++)
        {
            if (tr[u][i]){fail[tr[u][i]] = tr[fail[u]][i]; q.push(tr[u][i]);}
            else tr[u][i] = tr[fail[u]][i];
        }
    }
}

是不是非常简单~

后面说多模匹配 .

匹配的串串个数

跳跳跳跳跳,看看后缀 .

inline int query(string s)
{
    int u = 0, ans = 0, l = s.length();
    for (int i=0; i<l; i++)
    {
        u = tr[u][trans(s[i])];
        for (int j=u; j && ~mark[j]; j = fail[j]){ans += mark[j]; mark[j] = -1;}
    } return ans;
}

整体代码可以看 洛谷模板简单版

串串出现的个数

建出 fail 树,记录自动机上的每个状态被匹配了几次,最后求出每个模式串在 Trie 上的终止节点在 fail 树上的子树总匹配次数就可以了 .

这是 ouuan 大佬说的,那些拓扑排序优化啥的看 SoyTony 的博客 .

代码看洛谷模板二次加强(其实自己写也很容易的啦 -

然后这个玩意有个比较牛逼的写法(不用 DFS):here

时间复杂度

\(\displaystyle O\left(|\Sigma|\cdot\sum |s_i|\right)\) .

瓶颈在 build .

例题

洛谷模板

加强版没写 .

简单版
using namespace std;
typedef long long ll;
const int N = 1e6 + 50, Sig = 26;
struct AC
{
	int tr[N][Sig], fail[N], mark[N], cc, root;
	inline void insert(string s)
	{
		int u = root, l = s.length();
		for (int i=0; i<l; i++)
		{
			if (!tr[u][s[i] - 'a']) tr[u][s[i] - 'a'] = ++cc;
			u = tr[u][s[i] - 'a'];
		} ++mark[u];
	}
	inline void build()
	{
		queue<int> q; fail[root] = root;
		for (int i=0; i<Sig; i++)
		{
			if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
			else tr[root][i] = root;
		}
		while (!q.empty())
		{
			int u = q.front(); q.pop();
			for (int i=0; i<Sig; i++)
			{
				if (tr[u][i]){fail[tr[u][i]] = tr[fail[u]][i]; q.push(tr[u][i]);} // trie
				else tr[u][i] = tr[fail[u]][i]; // automaton
			}
		}
	}
	inline int query(string s)
	{
		int u = root, ans = 0, l = s.length();
		for (int i=0; i<l; i++)
		{
			u = tr[u][s[i] - 'a'];
			for (int j = u; j && ~mark[j]; j = fail[j]){ans += mark[j]; mark[j] = -1;}
		} return ans;
	}
	inline void clear(){memset(tr, 0, sizeof tr); memset(mark, 0, sizeof mark); cc = 0;}
	AC(){root = cc = 0;}
}ac;
int main()
{
	int T = 1;
	while (T--)
	{
		ac.clear(); int n; string tmp;
		scanf("%d", &n);
		for (int i=1; i<=n; i++){cin >> tmp; ac.insert(tmp);}
		cin >> tmp; ac.build();
		printf("%d\n", ac.query(tmp));
	} return 0;
}
二次加强版
using namespace std;
typedef long long ll;
const int N = 1e6 + 50, Sig = 26;
int n;
inline int trans(const char c){return c - 'a';}
struct SimpleGraph
{
	vector<int> g[N]; 
	inline void addedge(int u,int v){g[u].emplace_back(v);}
	inline void ade(int u,int v){addedge(u, v); addedge(v, u);}
	vector<int>& operator [](const int& idx){return g[idx];}
	inline vector<int>& out_edges(int u){return g[u];}
};
struct AC
{
	SimpleGraph FT; // fail tree
	int tr[N][Sig], fail[N], mark[N], ed[N], siz[N], cc, root;
	inline void insert(int id, string s)
	{
		int u = root, l = s.length();
		for (int i=0; i<l; i++)
		{
			int _ = trans(s[i]);
			if (!tr[u][_]) tr[u][_] = ++cc;
			u = tr[u][_];
		} ++mark[u]; ed[id] = u;
	}
	inline void build()
	{
		queue<int> q;
		for (int i=0; i<Sig; i++)
		{
			if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
			else tr[root][i] = root;
		}
		while (!q.empty())
		{
			int u = q.front(); q.pop();
			for (int i=0; i<Sig; i++)
			{
				if (tr[u][i]){q.push(tr[u][i]); fail[tr[u][i]] = tr[fail[u]][i];}
				else tr[u][i] = tr[fail[u]][i];
			}
		}
	}
	void dfs(int u)
	{
		for (int v : FT[u]) dfs(v), siz[u] += siz[v];
	}
	inline void query(string s)
	{
		int u = 0, l = s.length();
		for (int i=0; i<l; i++){u = tr[u][trans(s[i])]; ++siz[u];}
		for (int i=1; i<=cc; i++) FT.addedge(fail[i], i);
		dfs(0);
	}
	AC(){cc = root = 0;}
}ac;
int main()
{
#ifndef ONLINE_JUDGE
	freopen("i.in", "r", stdin);
#endif
	int n; string tmp;
	scanf("%d", &n);
	for (int i=1; i<=n; i++){cin >> tmp; ac.insert(i, tmp);}
	ac.build();
	cin >> tmp; ac.query(tmp);
	for (int i=1; i<=n; i++) printf("%d\n", ac.siz[ac.ed[i]]);
	return 0;
}

ed 数组的必要性:你至少得记下来这 \(n\) 个字符串都是啥呗 .

就是 匹配的串串个数(和 洛谷模板简单版 是同一个题)

玄武密码

咋做都行 .

我是 insert 的时候每个点都标记上结尾标记,相当于把这玩意的所有前缀的都插进 Trie 里了 .

然后是不是就随便做了?

Code
using namespace std;
typedef long long ll;
const int N = 1e6 + 50, Sig = 4;
int n;
string s[N];
inline int trans(const char c)
{
	if (c == 'E') return 0;
	if (c == 'S') return 1;
	if (c == 'W') return 2;
	if (c == 'N') return 3;
	return 114514;
}
struct AC
{
	int tr[N][Sig], fail[N], mark[N], cc, root;
	inline void insert(string s)
	{
		int u = root, l = s.length(); ++mark[root];
		for (int i=0; i<l; i++)
		{
			if (!tr[u][trans(s[i])]) tr[u][trans(s[i])] = ++cc;
			u = tr[u][trans(s[i])]; ++mark[u];
		}
	}
	inline void build()
	{
		queue<int> q; fail[root] = root;
		for (int i=0; i<Sig; i++)
		{
			if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
			else tr[root][i] = root;
		}
		while (!q.empty())
		{
			int u = q.front(); q.pop();
			for (int i=0; i<Sig; i++)
			{
				if (tr[u][i]){fail[tr[u][i]] = tr[fail[u]][i]; q.push(tr[u][i]);}
				else tr[u][i] = tr[fail[u]][i];
			}
		}
	}
	inline int query(string s)
	{
		int u = root, ans = 0, l = s.length();
		for (int i=0; i<l; i++)
		{
			u = tr[u][trans(s[i])];
			for (int j = u; j && ~mark[j]; j = fail[j]){ans += mark[j]; mark[j] = -1;}
		} return ans;
	}
	inline int Q(string s)
	{
		int u = 0, l = s.length(), ans = -1;
		for (int i=0; i<l; i++)
		{
			u = tr[u][trans(s[i])];
			if (!~mark[u]) ans = i;
		} return ans;
	}
	inline void clear(){memset(tr, 0, sizeof tr); memset(mark, 0, sizeof mark); cc = 0;}
	AC(){root = cc = 0;}
}ac;
int main()
{
	scanf("%d", &n); scanf("%d", &n);
	cin >> s[0];
	for (int i=1; i<=n; i++){cin >> s[i]; ac.insert(s[i]);}
	ac.build(); ac.query(s[0]);
	for (int i=1; i<=n; i++) printf("%d\n", ac.Q(s[i])+1);
	return 0;
}

单词

所有字符串中间随便加个字符隔开,然后就跑二次加强版就完了 .

Code

感谢 APJ 大佬的指导 .

你加的那个字符是要算进字符集的,所以 Sig 不能开太小,字符也要选一个好的(e.g. z + 1) .

using namespace std;
typedef long long ll;
const int N = 1e6 + 222, Sig = 30;
int n;
inline int trans(const char c){return c - 'a';}
struct SimpleGraph
{
	vector<int> g[N]; 
	inline void addedge(int u,int v){g[u].emplace_back(v);}
	inline void ade(int u,int v){addedge(u, v); addedge(v, u);}
	vector<int>& operator [](const int& idx){return g[idx];}
	inline vector<int>& out_edges(int u){return g[u];}
};
struct AC
{
	SimpleGraph FT; // fail tree
	int tr[N][Sig], fail[N], mark[N], ed[N], siz[N], cc, root;
	inline void insert(int id, string s)
	{
		int u = root, l = s.length();
		for (int i=0; i<l; i++)
		{
			int _ = trans(s[i]);
			if (!tr[u][_]) tr[u][_] = ++cc;
			u = tr[u][_];
		} ++mark[u]; ed[id] = u;
	}
	inline void build()
	{
		queue<int> q;
		for (int i=0; i<Sig; i++)
		{
			if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
			else tr[root][i] = root;
		}
		while (!q.empty())
		{
			int u = q.front(); q.pop();
			for (int i=0; i<Sig; i++)
			{
				if (tr[u][i]){q.push(tr[u][i]); fail[tr[u][i]] = tr[fail[u]][i];}
				else tr[u][i] = tr[fail[u]][i];
			}
		}
	}
	void dfs(int u){for (int v : FT[u]) dfs(v), siz[u] += siz[v];}
	inline void query(string s)
	{
		int u = 0, l = s.length();
		for (int i=0; i<l; i++){u = tr[u][trans(s[i])]; ++siz[u];}
		for (int i=1; i<=cc; i++) FT.addedge(fail[i], i);
		dfs(0);
	}
	AC(){cc = root = 0;}
}ac;
int main()
{
#ifndef ONLINE_JUDGE
	freopen("i.in", "r", stdin);
#endif
	int n; string tmp, s;
	scanf("%d", &n);
	for (int i=1; i<=n; i++){cin >> tmp; s = s + "{" + tmp; ac.insert(i, tmp);}
	ac.build(); ac.query(s);
	for (int i=1; i<=n; i++) printf("%d\n", ac.siz[ac.ed[i]]);
	return 0;
}

病毒

如果自动机上存在一个环,上面没有任何结束标记,且根到环没有结束标记,那么就可以构造出来 .

当然要根节点能到达,直接 DFS 即可 .

然后需要标记一下后缀,就是 mark[tr[u][i]] |= mark[fail[tr[u][i]]]; 那句 .

有向图判环

好像可以拓扑排序,见 http://blog.sina.com.cn/s/blog_82a8cba50100yd46.html


这个 DFS 注意判过没环的就不用判了 .

其实可以用俩标记数组 viscycle 实现等价功能 .

Code
using namespace std;
typedef long long ll;
const int N = 1e6 + 222, Sig = 2;
int n;
inline int trans(const char c){return c - '0';}
struct AC
{
	int tr[N][Sig], fail[N], mark[N], root, cc;
	inline void insert(string s)
	{
		int u = root, l = s.length();
		for (int i=0; i<l; i++)
		{
			int _ = trans(s[i]);
			if (!tr[u][_]) tr[u][_] = ++cc;
			u = tr[u][_];
		} ++mark[u]; //
	}
	inline void build()
	{
		queue<int> q; fail[root] = root;
		for (int i=0; i<Sig; i++)
		{
			if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
			else tr[root][i] = root;
		}
		while (!q.empty())
		{
			int u = q.front(); q.pop();
			for (int i=0; i<Sig; i++)
			{
				if (tr[u][i]){q.push(tr[u][i]); fail[tr[u][i]] = tr[fail[u]][i]; mark[tr[u][i]] |= mark[fail[tr[u][i]]];}
				else tr[u][i] = tr[fail[u]][i];
			}
		}
	}
	int cycle[N]; 
	void dfs(int u)
	{
		cycle[u] = 1;
		for (int i=0; i<Sig; i++)
		{
			int v = tr[u][i];
			if (mark[v]) continue;
			if (cycle[v] == 1){puts("TAK"); exit(0);} // find a cycle
			if (!cycle[v]) dfs(v);
		}
		cycle[u] = -1;
	}
	AC(){cc = root = 0;}
}ac;
int main()
{
	int n; string tmp;
	scanf("%d", &n);
	for (int i=1; i<=n; i++){cin >> tmp; ac.insert(tmp);}
	ac.build();
	ac.dfs(ac.root);
	puts("NIE");
	return 0;
}

最短母串

不太算 dp .

考虑你在 AC 自动机上怎么找一个串串 \(S\) 有哪些模式串是它的子串 .

和上一个类似,我们可以整个 bitset(或者说是二进制状态压缩),然后对于每个节点按位或上 fail 更新即可

然后你 BFS 一下,然后找到全集就输出,是不是就完了?

BFS 的过程保证了字典序最小 .

然后要找这个串串还得记一个 pre 跳回去,/tuu/tuu/tuu

Code
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int N = 114514, Sig = 26;
int n;
inline int trans(const char c){return c - 'A';}
struct AC
{
	int tr[N][Sig], fail[N], mark[N], root, cc;
	inline void insert(int id, string s)
	{
		int u = root, l = s.length();
		for (int i=0; i<l; i++)
		{
			int _ = trans(s[i]);
			if (!tr[u][_]) tr[u][_] = ++cc;
			u = tr[u][_];
		} mark[u] |= 1 << (id - 1); 
	}
	inline void build()
	{
		queue<int> q; fail[root] = root;
		for (int i=0; i<Sig; i++)
			if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
			else tr[root][i] = root;
		while (!q.empty())
		{
			int u = q.front(); q.pop();
			for (int i=0; i<Sig; i++)
				if (tr[u][i]){q.push(tr[u][i]); fail[tr[u][i]] = tr[fail[u]][i];  mark[tr[u][i]] |= mark[fail[tr[u][i]]];}
				else tr[u][i] = tr[fail[u]][i];
		}
	}
	unordered_set<int> vis;
	basic_string<int> pre;
	string ans;
	inline void bfs()
	{
		pre.push_back('?'); ans.push_back('$');
		queue<pii> q; // (node, state) 
		q.push(make_pair(root, 0)); vis.insert(0);
		int now=0;
		while (!q.empty())
		{
			pii _now = q.front(); q.pop();
			int u = _now.first, s = _now.second;
			if (s == ((1<<n)-1))
			{
				stack<char> st;
				while (now){st.push(ans[now]); now = pre[now];}
				while (!st.empty()){putchar(st.top()); st.pop();};
				puts(""); exit(0);
			}
			for (int i=0; i<Sig; i++)
			{
				auto _ = tr[u][i] * N + (s | mark[tr[u][i]]);
				if (vis.find(_) != vis.end()) continue; //
				vis.insert(_);
				pre.push_back(now); ans.push_back(i + 'A'); q.push(make_pair(tr[u][i], s | mark[tr[u][i]]));				
			} 
			++now;
		}
	}
	AC(){cc = root = 0;}
}ac;
int main()
{
	string tmp;
	scanf("%d", &n);
	for (int i=1; i<=n; i++){cin >> tmp; ac.insert(i, tmp);}
	ac.build(); ac.bfs();
	return 0;
} 

文本生成器

容斥(?)一下,变成 \(26^m\) 减掉不可读串串,看起来可做多了 .

首先必然建出所有了解单词的 AC 自动机 .

看看转移函数是啥意思?是不是有个自动跳 fail

于是标记一下不能完全匹配的,然后 AC 自动机上跳着转移即可 .

判断是否合法和 病毒 那题一模一样 .

Code
using namespace std;
typedef long long ll;
const int N = 114514, Sig = 26, P = 1e4+7;
inline int trans(const char c){return c - 'A';}
int n, m;
ll dp[123][12345];
struct AC
{
	int tr[N][Sig], fail[N], root, cc;
	bool mark[N];
	inline void insert(string s)
	{
		int u = root, l = s.length();
		for (int i=0; i<l; i++)
		{
			int _ = trans(s[i]);
			if (!tr[u][_]) tr[u][_] = ++cc;
			u = tr[u][_];
		} mark[u] = true;
	}
	inline void build()
	{
		queue<int> q; fail[root] = root;
		for (int i=0; i<Sig; i++)
		{
			if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
			else tr[root][i] = root;
		}
		while (!q.empty())
		{
			int u = q.front(); q.pop();
			for (int i=0; i<Sig; i++)
			{
				if (tr[u][i]){fail[tr[u][i]] = tr[fail[u]][i]; q.push(tr[u][i]); mark[tr[u][i]] |= mark[fail[tr[u][i]]];}
				else tr[u][i] = tr[fail[u]][i];
			}
		}
	}
	inline ll DP()
	{
		dp[0][0] = 1;
		for (int i=0; i<m; i++)
			for (int j=0; j<cc; j++)
				for (int k=0; k<Sig; k++)
	        		if (!mark[tr[j][k]]) dp[i+1][tr[j][k]] = (dp[i+1][tr[j][k]] + dp[i][j]) % P;
	    ll ans = 0;
	    for (int i=0; i<cc; i++) ans = (ans + dp[m][i]) % P;
	    return ans;
	}
	AC(){root = cc = 0;}
}ac;
int main()
{
	string tmp;
	scanf("%d%d", &n, &m);
	for (int i=1; i<=n; i++){cin >> tmp; ac.insert(tmp);}
	ac.build();
	ll full = 1;
	for (int i=0; i<m; i++) full = full * 26 % P; // full = 26^m
	printf("%lld\n", (full - ac.DP() + P) % P);
	return 0;
} 

背单词

先建 AC 自动机,另一个串是一个串的子串等价于这个串的任意前缀能够通过 \(fail\) 树到根的路径上走到另一个串 .

然后在 \(fail\) 树上 DP .

不给代码!!!!!

密码

和 文本生成器 类似 .
建出所有模式串的 AC 自动机,然后注意到数据范围非常小,考虑状压 DP .

\(dp_{i,j,k}\) 表示到第 \(i\) 位,自动机上是 \(j\),包含集合 \(k\) 的模式串个数 .

于是转移就在 AC 自动机上走一步即可(刷表).

关于输出方案 — 注意到如果输出方案则答案不超过 \(42\),于是暴搜即可 .

DP 转移咋写的暴搜就咋写,本质是相同的 .

Code
using namespace std;
typedef long long ll;
const int L = 27, N = 111, T = 1<<11, alphabet = 26;
inline int trans(const char c){return c - 'a';}
int n, m;
ll dp[L][N][T];
struct AC
{
	int tr[N][alphabet], mark[N], fail[N], root, cc;
	inline void insert(int id, string s)
	{
		int u = root, l = s.length();
		for (int i=0; i<l; i++)
		{
			int _ = trans(s[i]);
			if (!tr[u][_]) tr[u][_] = ++cc;
			u = tr[u][_];
		} mark[u] |= (1 << id);
	}
	inline void build()
	{
		queue<int> q; fail[root] = root;
		for (int i=0; i<alphabet; i++)
		{
			if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
			else tr[root][i] = root;
		}
		while (!q.empty())
		{
			int u = q.front(); q.pop(); mark[u] |= mark[fail[u]];
			for (int i=0; i<alphabet; i++)
			{
				if (tr[u][i]){fail[tr[u][i]] = tr[fail[u]][i]; q.push(tr[u][i]); mark[tr[u][i]] |= mark[fail[tr[u][i]]];}
				else tr[u][i] = tr[fail[u]][i];
			}
		}
	}
	inline ll DP()
	{
		dp[0][0][0] = 1;
		for (int i=0; i<n; i++)
			for (int j=0; j<=cc; j++)
				for (int s=0; s<(1<<m); s++)
				{
					if (!dp[i][j][s]) continue;
					for (int k=0; k<alphabet; k++) // trans
					{
						int v = tr[j][k];
						dp[i+1][v][s | mark[v]] += dp[i][j][s];
					}
				}
	    ll ans = 0;
	    for (int i=0; i<=cc; i++) ans += dp[n][i][(1<<m)-1];
	    return ans;
	}
	AC(){root = cc = 0;}
}ac;
bool vis[L][N][T], chk[L][N][T];
int mov[L];
bool dfs(int i, int j, int s)
{
	if (i == n)
	{
		vis[i][j][s] = true;
		return chk[i][j][s] = (s == (1<<m)-1);
	}
	if (vis[i][j][s]) return chk[i][j][s];
	vis[i][j][s] = true;
	bool ans = false;
	for (int k=0; k<alphabet; k++)
		ans |= dfs(i+1, ac.tr[j][k], s | ac.mark[ac.tr[j][k]]);
	return chk[i][j][s] = ans;
}
void output(int i, int j, int s)
{
	if (!chk[i][j][s]) return ;
	if (i == n)
	{
		for (int p=1; p<=n; p++) putchar(mov[p] + 'a');
		puts(""); return ;
	}
	for (int k=0; k<alphabet; k++)
	{
		mov[i+1] = k;
		output(i+1, ac.tr[j][k], s | ac.mark[ac.tr[j][k]]);
	}
}
int main()
{
	string tmp; scanf("%d%d", &n, &m);
	for (int i=1; i<=m; i++){cin >> tmp; ac.insert(i-1, tmp);}
	ac.build();
	ll ans = ac.DP();
	printf("%lld\n", ans);
	if (ans > 42) return 0;
	dfs(0, 0, 0); output(0, 0, 0);
	return 0;
}

禁忌

题目背景好评

这个 \(alphabet\) 看着就不顺眼,改成 \(c\) .

首先对模式串建出 AC 自动机 .

然后这个禁忌魔法的伤害只需要在 AC 自动机上贪心就可以了,比较平凡 .

考虑 DP,令 \(dp_{i,j}\) 表示到第 \(i\) 个字符,在 AC 自动机上到第 \(j\) 个字符的概率,这是 AC 自动机上 DP 的常见形式 .

考虑在 AC 自动机上走一步 \(j\to k\) 进行转移,于是

\[dp_{i+1,k}=\sum \dfrac1c\cdot dp_{i,j} \]

然而我们的划分不能重复,也就是样例解释里说的那个东西 .

于是如果 \(k\) 是某个模式串的末尾(也就是匹配上了),我们就令 \(k\gets root\)(相当于回到起始点重新匹配).

当然我们这个 \(dp\) 数组是概率,我们要在 DP 过程中统计期望 .

直接大力 DP 显然是过不去的,因为 \(len\) 贼大 .

然而这个东西相当于同样的转移跑 \(len\) 次,可以直接矩阵快速幂优化掉,这个和 GT 考试 的手法类似 .

然后时间复杂度瞬间变成 \(O\big((\sum|s|)^3\log len\big)\),轻松跑过 .

details: 加一维用来统计期望 .

Core Code:

Matrix<db> D, ans;
ACAM ac;
int n, len, ab;
db one = 1.0;
int main()
{
	scanf("%d%d%d", &n, &len, &ab); string tmp;
	for (int i=1; i<=n; i++){cin >> tmp; ac.insert(tmp);}
	ac.build();
	int S = ac.size();
	for (int i=0; i<=S; i++)
		for (int j=0; j<ab; j++)
		{
			int k = ac.tr[i][j];
			if (ac.mark[k]){D[S+1][i] += one/ab; D[0][i] += one/ab;}
			else D[k][i] += one/ab;
		}
	D[S+1][S+1] = one; D ^= len;
	ans[0][0] = 1; D *= ans; // !!!!!
	printf("%.10f\n", D[S+1][0]);
	return 0;
}

完整代码:R71833633 .

这个矩阵快速幂优化 DP 也可以看成 AC 自动机上走 \(len\) 步,这个先保留 .

在哪里看题 & 在哪里交

SoyTony 的题单

背单词是 BZOJ2905 .

Reference

posted @ 2022-03-05 17:50  Jijidawang  阅读(428)  评论(1编辑  收藏  举报
😅​