AC自动机 AC 自动机可以理解为在多个串上的 KMP,利用 Trie 树来维护这些串,nxt 数组变为 fail 指针。
fail 指针的构造思想如下:
考虑 Trie 树中当前的节点 $u$,$u$ 的父节点是 $p$,$p$ 通过字符 $c$ 的边指向 $u$,即 $\text{trie}[p,c]=u$。假设深度小于 $u$ 的所有节点的 $\text{fail}$ 指针都已求得。
1.如果 $\text{trie}[\text{fail}[p],c]$ 存在,则让 $u$ 的 fail 指针指向 trie[fail[p], c]。相当于在 $p$ 和 $\text{fail}[p]$ 后面加一个字符 $c$,分别对应 $u$ 和 $\text{fail}[u]$。
2.如果 trie[fail[p], c] 不存在,那么我们继续找到 trie[fail[fail[p]], c],重复 1 的判断过程,一直跳 fail 指针直到根节点。
3.如果真的没有,就让 fail 指针指向根节点。
可以发现,AC 自动机与 KMP 是非常相似的。
这是基本思想,具体实现我们可以直接构造 Trie 图 以进行多串匹配。
实现 普通实现 【模板】AC自动机(简单版)(luoguP3808)
给定 $n$ 个模式串 $s_i$ 和一个文本串 $t$,求有多少个不同的模式串在文本串里出现过,两个模式串不同当且仅当它们编号不同。
$1\le n\le10^6$,$1\le|t|\le10^6$,$1\le\sum_{i=1}^n|s_i|\le10^6$。
1 在 Trie 树上插入各串
1 2 3 4 5 6 7 8 9 10 11 void Insert (char *s) { int n = strlen (s); int pos = 0 ; for (int i = 0 ; i < n; i ++) { int tmp = s[i] - 'a' ; if (tr[pos][tmp] == 0 ) tr[pos][tmp] = ++ct; pos = tr[pos][tmp]; } val[pos] ++; }
2 fail指针的构建
按照上面的思路进行构建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void build () { queue<int > q; memset (fail, 0 , sizeof (fail)); for (int i = 0 ; i < 26 ; i ++) if (tr[0 ][i]) q.push (tr[0 ][i]); while (q.empty () == false ) { int x = q.front (); q.pop (); for (int i = 0 ; i < 26 ; i ++) if (tr[x][i]) { int tmp = fail[x]; while (tmp && tr[tmp][i] == 0 ) tmp = fail[tmp]; if (tr[tmp][i]) tmp = tr[tmp][i]; fail[tr[x][i]] = tmp; q.push (tr[x][i]); } } }
3 多模式匹配
多个模式串对文本串的匹配。
如果能匹配,就匹配,不能匹配,就跳 fail,注意可能多次匹配,所以要去掉重复匹配的贡献。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int query (char *s) { int pos = 0 , ret = 0 ; int n = strlen (s); for (int i = 0 ; i < n; i ++) { int tmp = s[i] - 'a' ; while (pos && tr[pos][tmp] == 0 ) pos = fail[pos]; if (tr[pos][tmp]) pos = tr[pos][tmp]; ret += val[pos]; val[pos] = 0 ; } return ret; }
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 #include <cstdio> #include <cstring> #include <queue> using namespace std;const int N = 1000010 ;int n;char s[N];int ct, tr[N][26 ], val[N], fail[N];void Insert (char *s) { int n = strlen (s); int pos = 0 ; for (int i = 0 ; i < n; i ++) { int tmp = s[i] - 'a' ; if (tr[pos][tmp] == 0 ) tr[pos][tmp] = ++ct; pos = tr[pos][tmp]; } val[pos] ++; } void build () { queue<int > q; for (int i = 0 ; i < 26 ; i ++) if (tr[0 ][i]) { fail[tr[0 ][i]] = 0 ; q.push (tr[0 ][i]); } while (q.empty () == false ) { int x = q.front (); q.pop (); for (int i = 0 ; i < 26 ; i ++) if (tr[x][i]) { int tmp = fail[x]; while (tmp && tr[tmp][i] == 0 ) tmp = fail[tmp]; if (tr[tmp][i]) tmp = tr[tmp][i]; fail[tr[x][i]] = tmp; q.push (tr[x][i]); } } } int query (char *s) { int pos = 0 , ret = 0 ; int n = strlen (s); for (int i = 0 ; i < n; i ++) { int tmp = s[i] - 'a' ; while (pos && tr[pos][tmp] == 0 ) pos = fail[pos]; if (tr[pos][tmp]) pos = tr[pos][tmp]; ret += val[pos]; val[pos] = 0 ; } return ret; } int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i ++) { scanf ("%s" , s+1 ); Insert (s+1 ); } build (); scanf ("%s" , s+1 ); printf ("%d\n" , query (s+1 )); return 0 ; }
Trie图的构建 建出真实的边,使 Trie 树变为一张图。
具体而言,只要把儿子记录上即可。
复杂度 $O(|S|\times n)$,$|S|$ 是字符集大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void build () { queue<int > q; for (int i = 0 ; i < 26 ; i ++) if (t[rt][i]) { q.push (t[rt][i]); fail[t[0 ][i]] = rt; } while (q.empty () == false ) { int x = q.front (); q.pop (); for (int i = 0 ; i < 26 ; i ++) if (t[x][i]) { fail[t[x][i]] = t[fail[x]][i]; q.push (t[x][i]); } else t[x][i] = t[fail[x]][i]; } }
解释:
对于 else
部分,相当于直接把这个不存在的儿子接到其失配指针的这个儿子,如果失配指针也没有这个儿子呢?那么它一定也通过 else
接上了可能的儿子,所以可以保证接上的恰好是一个最优的位置。
对于 if
部分,类似地,假如它的父亲($x$)有 $i$ 这个儿子,相当于直接匹配,那么显然是对的,如果没有,等同于 else
部分的解释,它一定指向了一个最优的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 int query (char *s) { int pos = 0 , ret = 0 ; int n = strlen (s); for (int i = 0 ; i < n; i ++) { int tmp = s[i] - 'a' ; pos = tr[pos][tmp]; for (int j = pos; j > 0 && val[j] != -1 ; j = fail[j]) { ret += val[j]; val[j] = -1 ; } } return ret; }
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #include <cstdio> #include <cstring> #include <queue> using namespace std;const int N = 1000010 ;int n;char s[N];int ct, tr[N][26 ], val[N], fail[N];void Insert (char *s) { int n = strlen (s); int pos = 0 ; for (int i = 0 ; i < n; i ++) { int tmp = s[i] - 'a' ; if (tr[pos][tmp] == 0 ) tr[pos][tmp] = ++ct; pos = tr[pos][tmp]; } val[pos] ++; } void build () { queue<int > q; for (int i = 0 ; i < 26 ; i ++) if (tr[0 ][i]) q.push (tr[0 ][i]); while (q.empty () == false ) { int x = q.front (); q.pop (); for (int i = 0 ; i < 26 ; i ++) if (tr[x][i]) { fail[tr[x][i]] = tr[fail[x]][i]; q.push (tr[x][i]); } else tr[x][i] = tr[fail[x]][i]; } } int query (char *s) { int pos = 0 , ret = 0 ; int n = strlen (s); for (int i = 0 ; i < n; i ++) { int tmp = s[i] - 'a' ; pos = tr[pos][tmp]; for (int j = pos; j > 0 && val[j] != -1 ; j = fail[j]) { ret += val[j]; val[j] = -1 ; } } return ret; } int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i ++) { scanf ("%s" , s+1 ); Insert (s+1 ); } build (); scanf ("%s" , s+1 ); printf ("%d\n" , query (s+1 )); return 0 ; }
另外一个模板
【模板】AC自动机(加强版)(luoguP3796)
有 $N$ 个由小写字母组成的模式串以及一个文本串 $T$,找出哪些模式串在文本串 $T$ 中出现的次数最多。
$1\le N\le 150$,单个模式串长度 $\le70$,文本串长度 $\le10^6$。
由于 Trie 数每个节点表示的字符串具有唯一性,并且 AC 自动机每到达一个位置就表示和当前位置表示的字符串匹配了,我们可以这么做:
记录每个模式串结尾位置,匹配时每到一个位置就累加这个位置的模式串出现次数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 #include <cstdio> #include <cstring> #include <queue> #include <algorithm> using namespace std;const int N = 20010 , M = 1000010 , H = 80 , P = 160 ;int n;char s[M];char str[P][H];int ct, t[N][26 ], fail[N], ed[N];int tim[P]; int top, stk[P], maxtim;void Insert (char *s, int rk) { int pos = 0 ; int len = strlen (s); for (int i = 0 ; i < len; i ++) { int tmp = s[i] - 'a' ; if (t[pos][tmp] == 0 ) t[pos][tmp] = ++ct; pos = t[pos][tmp]; } ed[pos] = rk; } void build () { queue<int > q; for (int i = 0 ; i < 26 ; i ++) if (t[0 ][i]) q.push (t[0 ][i]); while (q.empty () == false ) { int x = q.front (); q.pop (); for (int i = 0 ; i < 26 ; i ++) if (t[x][i]) { fail[t[x][i]] = t[fail[x]][i]; q.push (t[x][i]); } else t[x][i] = t[fail[x]][i]; } } void query (char *s) { int pos = 0 ; int len = strlen (s); for (int i = 0 ; i < len; i ++) { int tmp = s[i] - 'a' ; pos = t[pos][tmp]; for (int j = pos; j; j = fail[j]) tim[ed[j]] ++; } } void solv () { for (int i = 1 ; i <= n; i ++) { scanf ("%s" , str[i]+1 ); Insert (str[i]+1 , i); } build (); scanf ("%s" , s+1 ); query (s+1 ); maxtim = 0 ; top = 0 ; for (int i = 1 ; i <= n; i ++) if (tim[i] > maxtim) { maxtim = tim[i]; top = 0 ; stk[++top] = i; } else if (tim[i] == maxtim) stk[++top] = i; printf ("%d\n" , maxtim); for (int i = 1 ; i <= top; i ++) printf ("%s\n" , str[stk[i]] + 1 ); } void clea () { memset (ed, 0 , sizeof (ed)); memset (fail, 0 , sizeof (fail)); ct = 0 ; memset (t, 0 , sizeof (t)); memset (tim, 0 , sizeof (tim)); } int main () { while (scanf ("%d" , &n)) { if (n == 0 ) break ; solv (); clea (); } return 0 ; }
Fail树 Fail 指针构成了一个树形结构。
1.除了根节点都有 Fail 指针。
2.各个节点都能跳到根。
3.所以有 $N$ 个点 $N-1$ 条边,且连通,所以是树。
【模板】AC自动机(二次加强版)(luoguP5357)
给定一个文本串 $S$ 和 $n$ 个模式串 $T_{1\sim n}$,请你分别求出每个模式串 $T_i$ 在 $S$ 中出现的次数。
不保证任意两个模式串不同。
$1\le n\le2\times10^5$,$\sum_{i=1}^n|T_i|\le2\times10^5$,$|S|\le2\times10^6$。
观察一下代码片段:
1 2 3 4 5 6 for (int i = 0 ; i < len; i ++) { int tmp = s[i] - 'a' ; pos = t[pos][tmp]; for (int j = pos; j; j = fail[j]) tim[ed[j]] ++; }
可以发现一个问题,就是 j
循环的部分,跳 fail[j]
实际上复杂度是不确定的,深度可能很深,复杂度会退化。
但是我们发现其实每次转移是类似的,可以建立出 fail
树然后通过遍历一遍 fail
树将所有转移都做了。
具体地,j
恰好使一个链都 ++
,于是我们可以在底端打一个标记,然后构建 fail
树后在树上做 DP 向上传递并累加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include <cstdio> #include <vector> #include <queue> #include <cstring> using namespace std;const int N = 200010 , M = 2000010 ;int n;char s[M];int ct, t[N][26 ], fail[N];vector<int > ed[N]; int ans[N];int ctb, hd[N], ver[N<<1 ], nxt[N<<1 ];int f[N]; void add (int u, int v) { ver[++ctb] = v; nxt[ctb] = hd[u]; hd[u] = ctb; } void Insert (char *str, int rk) { int pos = 0 ; int len = strlen (str); for (int i = 0 ; i < len; i ++) { int tmp = str[i] - 'a' ; if (t[pos][tmp] == 0 ) t[pos][tmp] = ++ct; pos = t[pos][tmp]; } ed[pos].push_back (rk); } void build () { queue<int > q; for (int i = 0 ; i < 26 ; i ++) if (t[0 ][i]) q.push (t[0 ][i]); while (q.empty () == false ) { int x = q.front (); q.pop (); for (int i = 0 ; i < 26 ; i ++) if (t[x][i]) { fail[t[x][i]] = t[fail[x]][i]; q.push (t[x][i]); } else t[x][i] = t[fail[x]][i]; } } void query (char *str) { int pos = 0 ; int len = strlen (str); for (int i = 0 ; i < len; i ++) { int tmp = s[i] - 'a' ; pos = t[pos][tmp]; f[pos] ++; } } void dfs (int x, int fa) { for (int i = hd[x]; i; i = nxt[i]) { int y = ver[i]; if (y == fa) continue ; dfs (y, x); f[x] += f[y]; } int tmpsiz = ed[x].size (); for (int i = 0 ; i < tmpsiz; i ++) ans[ed[x][i]] += f[x]; } int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i ++) { scanf ("%s" , s); Insert (s, i); } build (); scanf ("%s" , s); query (s); for (int i = 1 ; i <= ct; i ++) { add (i, fail[i]); add (fail[i], i); } dfs (0 , -1 ); for (int i = 1 ; i <= n; i ++) printf ("%d\n" , ans[i]); return 0 ; }
例题 阿狸的打字机 (NOI2011) (luoguP2414) 有一个打字机,有 $28$ 个按键,分别是 $26$ 个小写英文字母和 B
、P
两个字母,打字机是这样工作的:
输入小写字母,会把这个字母加在凹槽最后。
按下 B
,凹槽中的最后一个字母会消失
按下 P
,会打印出凹槽中的字母,并换行,但凹槽中的字母不会消失
给定一个字符串,表示按键情况。
然后把打印出来的字符串按照 $1\sim n$ 编号,有 $m$ 组询问,每次给定一个 $(x,y)$ 表示询问第 $x$ 个打印的字符串在第 $y$ 个打印的字符串中出现了多少次。
$1\le n\le10^5$,$1\le m\le10^5$,表示按键情况的字符串长度 $\le10^5$。
最暴力的做法自然是跑 $m$ 遍 KMP,复杂度 $O(nm)$,是不能接受的。
本题的特点就在于它是由打字机打出来的,由于按键次数至多 $10^5$ 次,所以打出来的字符串相差必然都比较小。
如果放到 Trie 树上,必然不超过 $10^5$ 个节点。
我们想到构建 AC 自动机。
类似于 KMP,如果一个 fail 指针是从 $x$ 指向 $y$ 的,那么字符串 $y$ 在字符串 $x$ 中出现了一次。
对于一个字符串,从根到叶子,一路上每个节点都跳 fail 对应出来的若干个字符串全都在这个字符串内,并且不会漏,当然注意还要加上恰好就在这个串里的,即一路过来的这些子串。
于是我们可以在 Trie 树上 DFS,用一个全局桶,到每个节点跳 fail 把该加的都加进来。
然后光荣地 TLE 了。
跳 fail 复杂度是不对的,但是我们已经找到了策略,如何保证复杂度呢?
然后这时发现了一个问题,就是构建 AC 自动机太慢了,如果模拟的话,可能会插入许多许多次相同但很长的字符串,导致爆炸,于是我们需要特化本题的插入函数。
具体地,仍然是模拟,但是不从头插了,而是在 Trie 图上走,就可以保证复杂度。
然后我们就可以继续思考如何解决不可跳 fail 的问题。
分析可以发现,我们复杂度瓶颈在于一路上把各种串都插到桶里了,但是我们要查询的却比较少,于是我们考虑一对查询 $(x,y)$ 的关系。
按照上面的想法,就是 Trie 树上从根到 $y$ 一路上的点全插进来,fail 指针对应在 fail 树上的一条链全插进来,然后判断指向 $x$ 的个数,我们反向思考可以发现,只有 fail 树上 $x$ 的子树内的才能指向它,而且发现是一条链上全有贡献,所以子树内全都有贡献,于是我们每次不要跳 fail 到根,而是仅打一个标记,然后查 $x$ 只需要查 $x$ 子树内的标记数目。
然后这个东西可以数据结构优化,子树可以通过 DFS 序化为区间,然后就是单点修改区间查询。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 #include <cstdio> #include <cstring> #include <algorithm> #include <queue> using namespace std;const int N = 100010 ;typedef pair<int , int > pii;int m, ct, tr[N][26 ], tmptr[N][26 ], fail[N];char s[N];int ctb, top, stk[N];vector<int > strk[N]; vector<pii> qry[N]; int p[N]; int cnt[N]; int ans[N];int ctc, hd[N], ver[N<<1 ], nxt[N<<1 ];int ctd, dfn[N], siz[N];int t[N]; void addedge (int u, int v) { ver[++ctc] = v; nxt[ctc] = hd[u]; hd[u] = ctc; } void Insert (char *str) { int pos = 0 ; int len = strlen (str); for (int i = 0 ; i < len; i ++) { if (str[i] >= 'a' && str[i] <= 'z' ) { int tmp = str[i] - 'a' ; if (tr[pos][tmp] == 0 ) tr[pos][tmp] = ++ct; stk[++top] = tr[pos][tmp]; pos = tr[pos][tmp]; } else if (str[i] == 'B' ) { top --; pos = stk[top]; } else if (str[i] == 'P' ) { ++ ctb; strk[pos].push_back (ctb); p[ctb] = pos; } } } void build () { memcpy (tmptr, tr, sizeof (tr)); queue<int > q; for (int i = 0 ; i < 26 ; i ++) if (tmptr[0 ][i]) { q.push (tmptr[0 ][i]); fail[tmptr[0 ][i]] = 0 ; } while (q.empty () == false ) { int x = q.front (); q.pop (); for (int i = 0 ; i < 26 ; i ++) if (tmptr[x][i]) { fail[tmptr[x][i]] = tmptr[fail[x]][i]; q.push (tmptr[x][i]); } else tmptr[x][i] = tmptr[fail[x]][i]; } } void modify (int pos, int dlt) { for (; pos <= ct+1 ; pos += pos & (-pos)) t[pos] += dlt; } int subquery (int pos) { int ret = 0 ; for (; pos; pos -= pos & (-pos)) ret += t[pos]; return ret; } int query (int l, int r) { return subquery (r) - subquery (l-1 ); } void dfsa (int x, int fa) { siz[x] = 1 ; dfn[x] = ++ctd; for (int i = hd[x]; i; i = nxt[i]) { int y = ver[i]; if (y == fa) continue ; dfsa (y, x); siz[x] += siz[y]; } } void dfsb (int x) { int tmpsiz = strk[x].size (); for (int i = 0 ; i < tmpsiz; i ++) cnt[strk[x][i]] ++; modify (dfn[fail[x]], 1 ); tmpsiz = qry[x].size (); for (int i = 0 ; i < tmpsiz; i ++) { pii ttmp = qry[x][i]; ans[ttmp.second] = cnt[ttmp.first] + query (dfn[p[ttmp.first]], dfn[p[ttmp.first]] + siz[p[ttmp.first]] - 1 ); } for (int i = 0 ; i < 26 ; i ++) if (tr[x][i]) dfsb (tr[x][i]); tmpsiz = strk[x].size (); for (int i = 0 ; i < tmpsiz; i ++) cnt[strk[x][i]] --; modify (dfn[fail[x]], -1 ); } int main () { scanf ("%s" , s+1 ); Insert (s+1 ); build (); scanf ("%d" , &m); for (int i = 1 , tx, ty; i <= m; i ++) { scanf ("%d%d" , &tx, &ty); qry[p[ty]].push_back (make_pair (tx, i)); } for (int i = 1 ; i <= ct; i ++) { addedge (i, fail[i]); addedge (fail[i], i); } dfsa (0 , -1 ); dfsb (0 ); for (int i = 1 ; i <= m; i ++) printf ("%d\n" , ans[i]); return 0 ; }
魔法咒语 (BJOI2017) (luoguP3715) 给定 $n$ 个基本词汇,$m$ 个忌讳词语,它们都是字符串。求满足下列条件的字符串的个数:
1.长度等于 $L$。
2.可以被分割为若干个基本词汇。
3.不存在任何一个禁忌词语在字符串中出现过。
这里字符串不同的条件比较特殊:
把基本词汇标号 $1\sim n$,把字符串分割为若干个基本词汇,设 $i$ 号基本词汇的出现次数为 $c_i$,两个字符串不同当且仅当存在一个 $c_i$ 不相等。
而书写形式相同也可能是两个不同的字符串。
答案对 $10^9+7$ 取模。
$1\le n,m\le50$,$1\le L\le10^8$,当 $L>100$ 时,保证基本词汇长度不超过 $2$,$M\le20$。
基本词汇长度之和、忌讳词语的长度之和不超过 $100$,基本词汇不重复,禁忌词汇不重复。
由于字符串中不能出现忌讳词语,而判断是否出现忌讳词语就相当于匹配这个忌讳词语,容易想到要把所有忌讳词语插入 AC 自动机。
然后构建 fail 树并把所有不能到达的点做标记,然后在 AC 自动机上 DP,每次考虑加入一个基本词汇,然后枚举可以转移到的状态,为了使复杂度更优秀,我们可以 $O(n^3)$ 暴力预处理每个点插入每个字符串会到达的点,这样把我们 DP 的复杂度从 $O(L\times n^3)$ 优化到了 $O(L\times n^2)$。
这样,我们就解决了第一部分,本题另外一部分是 $L\le10^8$,但保证基本词汇长度不超过 $2$,这明显提示我们写矩乘。
这里已经不是这题在 AC 自动机方面作为例题的作用所在了,但还是说一下如何写吧。
主要还是建矩阵嘛。
设我们前面预处理那个数组是 $p[i][j]$,表示在点 $i$ 添加串 $j$ 到达的位置。转移都是形如下面这样的:
除了填完填了一位时不能填长度为 $2$ 的串以外,其他时候,转移都是随便填,这样就是个常系数的递推。
设初始列向量为 $f[0\sim ct][1],f[0\sim ct][0]$,然后矩阵的前 $ct+1$ 行就是看是否有转移,后 $ct+1$ 行是继承旧的前半部分。
当然,看是否有转移其实麻烦了,因为那样就还需要一个逆映射,没有必要,我们有 $p$ 这个映射就够了,我们仍然枚举所有转移,然后寻找转移在矩阵上的位置就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 #include <cstdio> #include <cstring> #include <queue> using namespace std;const int N = 100 , md = 1000000007 ;typedef long long ll;struct Matrix { int n, m; ll a[N<<1 ][N<<1 ]; void id () { for (int i = 1 ; i <= n; i ++) a[i][i] = 1ll ; } void init (int _n, int _m) { n = _n; m = _m; memset (a, 0 , sizeof (a)); } Matrix operator * (Matrix B) { Matrix res; res.init (n, B.m); for (int i = 1 ; i <= n; i ++) for (int j = 1 ; j <= B.m; j ++) for (int k = 1 ; k <= m; k ++) res.a[i][j] = (res.a[i][j] + a[i][k] * B.a[k][j]) % md; return res; } Matrix qpow (int b) { Matrix res, mat = *this ; res.init (n, n); res.id (); for (; b; b >>= 1 , mat = mat * mat) if (b & 1 ) res = res * mat; return res; } }; int n, m, l;char s[N][N], tmps[N];int len[N];int ct, tr[N][26 ], fail[N];bool tag[N]; int ctb, hd[N], ver[N<<1 ], nxt[N<<1 ];int p[N][N]; int f[N][N]; Matrix st, trans; void addedge (int u, int v) { ver[++ctb] = v; nxt[ctb] = hd[u]; hd[u] = ctb; } void Insert (char *s) { int pos = 0 ; int len = strlen (s); for (int i = 0 ; i < len; i ++) { int tmp = s[i] - 'a' ; if (tr[pos][tmp] == 0 ) tr[pos][tmp] = ++ct; pos = tr[pos][tmp]; } tag[pos] = true ; } void buildfail () { queue<int > q; for (int i = 0 ; i < 26 ; i ++) if (tr[0 ][i]) q.push (tr[0 ][i]); while (q.empty () == false ) { int x = q.front (); q.pop (); for (int i = 0 ; i < 26 ; i ++) if (tr[x][i]) { fail[tr[x][i]] = tr[fail[x]][i]; q.push (tr[x][i]); } else tr[x][i] = tr[fail[x]][i]; } } void buildfailtree () { for (int i = 1 ; i <= ct; i ++) { addedge (i, fail[i]); addedge (fail[i], i); } } void dfs (int x, int fa, bool flg) { flg |= tag[x]; tag[x] = flg; for (int i = hd[x]; i; i = nxt[i]) { int y = ver[i]; if (y == fa) continue ; dfs (y, x, flg); } } void getp () { for (int i = 0 ; i <= ct; i ++) { if (tag[i]) continue ; for (int j = 1 ; j <= n; j ++) { int pos = i; for (int k = 1 ; k <= len[j]; k ++) { int tmp = s[j][k] - 'a' ; pos = tr[pos][tmp]; if (tag[pos]) { p[i][j] = -1 ; break ; } } if (p[i][j] != -1 ) p[i][j] = pos; } } } void solv1 () { int ans = 0 ; f[0 ][0 ] = 1 ; for (int i = 0 ; i <= l; i ++) for (int j = 1 ; j <= n; j ++) { if (i + len[j] > l) continue ; for (int k = 0 ; k <= ct; k ++) { if (tag[k] || p[k][j] == -1 ) continue ; f[p[k][j]][i + len[j]] = (f[p[k][j]][i + len[j]] + f[k][i]) % md; } } for (int i = 0 ; i <= ct; i ++) ans = (ans + f[i][l]) % md; printf ("%d\n" , ans); } void solv2 () { int ans = 0 ; f[0 ][0 ] = 1 ; for (int i = 1 ; i <= n; i ++) { if (len[i] > 1 ) continue ; for (int j = 0 ; j <= ct; j ++) { if (tag[j] || p[j][i] == -1 ) continue ; f[p[j][i]][1 ] = (f[p[j][i]][1 ] + f[j][0 ]) % md; } } st.init (2 *(ct+1 ), 1 ); for (int i = 1 ; i <= ct+1 ; i ++) { st.a[i][1 ] = f[i-1 ][1 ]; st.a[i+(ct+1 )][1 ] = f[i-1 ][0 ]; } trans.init (2 *(ct+1 ), 2 *(ct+1 )); for (int i = ct+2 ; i <= 2 *ct+2 ; i ++) trans.a[i][i-(ct+1 )] = 1 ; for (int i = 1 ; i <= n; i ++) for (int j = 0 ; j <= ct; j ++) { if (tag[j] || p[j][i] == -1 ) continue ; if (len[i] == 1 ) trans.a[p[j][i] + 1 ][j+1 ] ++; else trans.a[p[j][i] + 1 ][(j+1 ) + (ct+1 )] ++; } trans = trans.qpow (l-1 ); st = trans * st; for (int i = 1 ; i <= ct+1 ; i ++) ans = (ans + st.a[i][1 ]) % md; printf ("%d\n" , ans); } int main () { scanf ("%d%d%d" , &n, &m, &l); for (int i = 1 ; i <= n; i ++) { scanf ("%s" , s[i]+1 ); len[i] = strlen (s[i]+1 ); } for (int i = 1 ; i <= m; i ++) { scanf ("%s" , tmps); Insert (tmps); } buildfail (); buildfailtree (); dfs (0 , -1 , false ); getp (); if (l <= 100 ) solv1 (); else solv2 (); return 0 ; }