引入
我们首先提出一个问题:
给出n个串每个串的长度
然后给出一个长度为k的串,询问前n个串中有多少个是匹配成了的
暴力搜索
这题不是sb题目吗?
随随便便O(kmn)跑过。
。。。。
n=10000 m=50
k=1000000
。。。。
好吧——我们用AC自动机吧
样例
首先我们举一个例子,我们有n=3个串he 和 her 和 she
然后我们通过构建Trie可以得到下图
这里红色的节点到根的路径可以构成一个串(怎么那么像后缀自动机?)然后我们可以发现
正文
概念
我们使用Fail(i)表示i节点用虚线连接的边,这样的边我们的作用就是当前节点到根的路径构成的字符串的最长的存在的后缀的串的位置。比如she 和 he 中 he 就是 she 的最长的后缀所以我们连接 e2->e这样我们假设从e2无法再继续伸展的时候我们就可以O(1)找到类似的串,然后继续进行伸展得到如下的图
这样比如我们匹配sher的时候我们首先沿着边走到e2然后不需要重新搜索,而是选择立即跳转到e节点然后搜索到r节点
这样我们就构造出了一个AC自动机的图
构建方法
就使用上面的例子
对于这样的一个图,我们首先队列中有
Queue | 0 | 1 |
---|---|---|
h | s |
同时将深度为1的节点连接Fail到root
然后我们扫描一遍h节点可以得到e节点然后我们扫描s节点可以发现s的Fail边上的root节点存在和s的儿子h相同的另一个h那么因为s的fail边上的所有节点的后缀相同,所以我们有h2和h节点同时增加一个节点后仍然满足上面的Fail边的概念,所以我们h2->h得到
同时我们的队列变成了
Queue | 0 | 1 |
---|---|---|
2 | h2 |
对于下面的伸展我们可以得到(同理):
呵呵呵代码时间:
代码
void Insert(char *s){
int len = strlen(s),u = root;
for(int i=0;i<len;i++){
int id = idx(s[i]);
if(!pool[u].ch[id]) pool[u].ch[id] = ++ecnt;
u = pool[u].ch[id];
}
pool[u].End_Flag++;
}
void build(){
queue<int> que;
for(int i=0;i<26;i++)
if(pool[root].ch[i])
que.push(pool[root].ch[i]);
int u,v,t,p;
while(!que.empty()){
u = que.front();
que.pop();
for(int i=0;i<MAXK;i++) if(pool[u].ch[i]){
v = pool[u].ch[i];
que.push(v);
p = pool[u].Fail;
while(p && !pool[p].ch[i]) p = pool[p].Fail;
t = pool[p].ch[i];
pool[v].Fail = t;
pool[v].last = pool[t].End_Flag ? t : pool[t].last;
}
}
}