AC自动机相关:
$fail$树:
$fail$树上以最长$border$关系形成父子关系,我们定一个节点对应的串为根到该节点的路径。
对于任意一个非根节点$x$,定$y = fa_{x}$,那$y$对应的串就是$x$对应的串的最长$border$,也就是说如果母串能走到$x$,那母串中一定存在一个子串对应了$y$,而且是当前母串匹配到当前位置的一个后缀。
求每个模式串在母串中出现的次数:
这应该算是AC自动机最基本的问题。
把母串在自动机上跑一遍,显然所有被访问过的节点都是母串的子串,但以当前匹配位置为后缀的模式串可能不仅仅只有一个,还有它所有的$border$。一个很好的性质就是,$fail$树上某个节点的祖先链就恰好完全对应了该节点的所有$border$。我们用$cnt_{i}$表示$i$点的祖先链上有效节点的个数,那只要每次匹配到某个节点$x$时,把它的$cnt_{x}$加上贡献就行了。
如果要支持动态加减模式串,由于修改一个节点的有效性只会对它的子树中所有节点造成影响,只要维护$Dfs$序上的信息即可。
AC自动机上的$Dp$问题
通常情况下,这类$Dp$问题的数据范围不大,设计状态的基本套路通常有两维,一维是母串匹配到的位置,一维是在自动机上的位置,转移基本上都是枚举字符。
$star$ 一个简单的例子,就是给出一个数$n$和$m$个字符串,问长度为$n$的字符串有多少个满足这$m$个串都是它的子串。题目链接
由于$m$非常小,我们直接状压起来,表示该节点对应的串包含了哪些子串,$Dp$有两种方式,$Dfs$和$Bfs$都可以,如果$Dfs$的话状态的含义通常表示为当前状态到结束状态的方案数。
不过由于这题要输出方案,用$Bfs$的做法不太容易,还是用$Dfs$的吧。
#include <cstdio> #include <queue> #include <cstring> using namespace std; typedef long long LL; const int N = 27, M = 135; int n, m, ST, tot; char sr[29]; int ch[M][27], fail[M], fir[M]; queue<int> Q; LL dp[N][M][1037], ans; void Ins(char *s, int id) { int pos = 0; for (int i = 0; s[i]; ++i) { int nx = s[i] - 'a'; if (!ch[pos][nx]) { ch[pos][nx] = ++tot; memset(ch[tot], 0, sizeof ch[tot]); fir[tot] = fail[tot] = 0; } pos = ch[pos][nx]; } fir[pos] |= 1 << id; } void Build() { Q.push(0); for (; !Q.empty(); ) { int x = Q.front(); Q.pop(); for (int i = 0; i < 26; ++i) { int v = ch[x][i]; if (!v) ch[x][i] = ch[fail[x]][i]; else Q.push(v); if (x && v) fail[v] = ch[fail[x]][i], fir[v] |= fir[fail[v]]; } } } LL Dfs(int x, int p, int s) { if (~dp[x][p][s]) return dp[x][p][s]; if (x == n) { dp[x][p][s] = (LL) (s == ST); return dp[x][p][s]; } dp[x][p][s] = 0; for (int i = 0; i < 26; ++i) { int np = ch[p][i]; dp[x][p][s] += Dfs(x + 1, np, s | fir[np]); } return dp[x][p][s]; } void F_(int x, int p, int s) { if (x == n) { sr[x] = '