之前学AC自动机查找的过程都是暴力跳fail,但是遇到a,aa,aaa…这种,每次只能向上跳1层,时间复杂度就直接退化成|STR|N|S| 了,要不得。
- 朴素的查询方法是每匹配一个字符串,我们通过fail指针不断往上跳把所有的后缀给答案加上去(如果后缀中存在模式串的话)
- 这样情况下有可能某些后缀已经被判断过了,但是又被跳到,产生重复过程
- 我们利用fail树或tupo使得每个后缀只被判断一次
- 另外,当模式串数量较少,比如20,可以压缩成二进制,在求fail指针的时候
isstr[i] |= isstr[fail[i]]
,这样匹配过程只需要把主串在trie上遍历一遍就可以求出模式串被匹配的最终答案。
(很多ac自动机+状压的题就是这样预处理的)
①trie图上tupo排序:
我们可以换一个思路,在建好AC自动机之后,当某个节点V被匹配1次,V的最长真后缀也会被匹配一次,即fail[V] 节点的答案+1 ,我们可以在查询的时候先只记录V被匹配的答案,然后把fail指针看成有向边<i,fail[i]>
进行tupo ,match[ fail[v] ] += match[v]
。tupo结束后,match数组就是每个模式串被匹配的次数。
细节见代码注释,
查询的时候,先不考虑fail指针,直接在trie树上匹配。
然后再利用拓扑排序,从下往上累加答案。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
const int maxn = 2e5+10;
const int mx = 40;
const int mod = 1e9+5;
const ll inf = 34359738370;
const int INF = 1e9+7;
int tree[maxn][26];//trie树
int fail[maxn];//fail指针 指向最长的真后缀
int in[maxn];//记录每个点的入度 用于给fail树tupo排序
queue<int> q;//队列 求fail和tupo的工具人
int match[maxn];//tupo排序前 每个尾节点被匹配的次数
int idstr[maxn];//i节点 对应的模式串第一次插入时的尾节点
int mp[maxn];//n个模式串 每个模式串对应的尾节点
int tot=0;//节点编号
int ans[maxn];//tupo后 每个尾节点最终被匹配的次数 也即是答案
inline void insert(char *s,int num)
{
int len=strlen(s),root=0;
for(int i=0;i<len;i++)
{
int id=s[i]-'a';
if(!tree[root][id]) tree[root][id]=++tot;
root=tree[root][id];
}
//所有相同的模式串 都映射到同一个trie树节点
if(!idstr[root]) idstr[root]=root;
mp[num]=idstr[root];
}
inline void getfail()
{
for(int i=0;i<