闲话 Day1
前情提要。
必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了必须要开始写闲话了。
不过突然意识到没啥可写的。
那就把一年之前的素材搬过来吧。
AC自动机
好像一直以来写的 AC自动机 就和常规的板子不太一样。
(虽然本质相同)
来一个好理解的自动机建法。
在此之前,我们先考虑一个简化版的问题。
KMP自动机
貌似是叫这个,不叫这个也没事。
也就是对一个串建AC自动机。
其实重点就是 fail 和那一大堆转移边怎么搭建。
首先,一个很显然的事实:
建了一半的自动机也是自动机。
就是前缀的自动机。
那么,对于一个字符串 \(S\),如果我们已经建立出来了 \(S[1, x]\) 的自动机。
现在需要给 \(S[x + 1]\) 对应的节点找 fail。
用一个指针 \(p\),然后从 \(S[2]\) 开始在自动机上跑匹配,跑到 \(S[x]\) 为止。
显而易见的是 \(p\) 对应的为止就是 \(fail[x + 1]\)。
然后把 \(S[x + 1]\) 的转移边赋值为 \(p\) 的转移边。
搭建结束。容易发现复杂度为 \(O(n^2)\)。
虽然但是,都 \(O(n^2)\) 了我要这自动机有何用。
然后考虑化简。
显然,每次 \(p\) 指针相对于上一次的转移量是 \(O(1)\) 的。
所以可以动态维护 \(p\) 指针。
复杂度 \(O(n)\)。
来一份正常实现的代码来感受一下这东西多简单。
void build()
{
for (int i = 1; i <= n; ++i)
{
p = tree[p].son[str[i]];
tree[i - 1].son[str[i]] = i;
tree[i] = tree[p];
}
}
主体部分只有三行。
好了是时候回归正题了。
我们要说啥来着,AC自动机?
带着 APJifengc 不就好了。
上面的KMP自动机已经介绍完了。
那么现在要给多个串建,首先需要广搜。
如果有 \(n\) 个串,那么需要维护 \(n\) 个当前位置和 \(p\) 指针。
其次,把上面那三句话全都变成循环。
好了建完了。
是不是有点草率啊。
但确实就是这个样子的啊。
贴个代码看一看。
for (int i = 1; i <= n; ++i)
sta[i] = i;
sta[0] = n;
for (int i = 0; sta[0]; ++i)
{
for (int j = 1; j <= sta[0]; ++j)
{
int k = sta[j];
if (st[k] + i > ed[k])
{
sta[j] = sta[sta[0]--];
--j; continue;
}
fal[k] = tree[fal[k]][str[st[k] + i]];
// p = tree[p].son[str[i]];
}
last = top;
for (int j = 1; j <= sta[0]; ++j)
{
int k = sta[j];
if (tree[now[k]][str[st[k] + i]] <= last)
tree[now[k]][str[st[k] + i]] = ++top;
now[k] = tree[now[k]][str[st[k] + i]];
// tree[i - 1].son[str[i]] = i;
}
for (int j = 1; j <= sta[0]; ++j)
{
int k = sta[j];
for (int s = 0; s < 26; ++s)
tree[now[k]][s] = tree[fal[k]][s];
// tree[i] = tree[p];
}
}
这里是把所有的字符串串在一起了,所以代码显得略长。
如果对效率没有要求的话可以换个写法。
虽然代码变长了,但是思维难度一下子就没有了对吧。
写了一篇啥用没有的凑数闲话。
。。。。。。。。