L 语言 [AC自动机][状态压缩]
作者:互联网
题目大意:
给出一个大小为 n 的模式串集合 S,m 个目标串 t,找出每个 t 中能由 S 中任意个模式串(可重复使用)拼接成的最长前缀。
1 ≤ n ≤ 20,1 ≤ m ≤ 50,1 ≤ ∣s∣ ≤ 20,1 ≤ ∣t∣ ≤ 2×106
思路:
首先考虑一下我们用字典树是怎么匹配的。假设我们在目标串的某处走到了字典树的叶子节点,如果到目标串的此处是符合题意的前缀,那在此处回溯到目标串上对应于字典树根节点的位置,刚好是另外一次匹配的末尾位置。即 t[0 ... d1], t[(d1+1) ... d2], t[(d2+1) ... d3], ..., t[(dn-1 + 1) ... dn] 都能在字典树上找到匹配。
这样是找一次的复杂度就是 ∣t∣2 的,很明显需要优化。使用AC自动机,我们在目标串的每个位置上跳fail,如果能跳到字典树的某个叶子节点上,且减去模式串长度后刚好是上一次匹配的结束位置,那么这个位置就是新找到的答案。找一次的复杂度变为∣s∣∣t∣。可以写出这样的匹配代码:
vis[0] = true; // vis[目标串长度]为true表示是一个合法前缀
int p = 0;
int ans = 0;
for (auto c = &tar[1]; *c; ++c) // tar 是目标串
{
p = trie[p][*c - 'a']; // 匹配当前字符
for (int v = p; v; v = fail[v]) // 跳fail
{
if (exists[v]) // 此节点是trie叶子节点
{
vis[c - tar] |= vis[c - tar - len[v]]; // 刚好是之前匹配的结束位置,len[]是目标串长度,即字典树节点的深度
if (vis[c - tar])
{
ans = c - tar; // 新答案
break;
}
}
}
}
此时我们可以发现,在完成自动机的构建之后,每个节点跳fail经过的地方都已经固定下来了,只要找到一个符合目标串长度的匹配,那么这个位置就能成为新答案。那能不能通过预处理的手段来优化掉跳fail的过程呢?假设我们把在trie上每个节点跳fail能找到的所有匹配目标串的长度都存起来,是不是就能快速计算呢?
再看看模式串的最大长度,只有20,则可以进一步尝试使用位运算来优化这个过程。
对于trie上的每一个节点,我们可以预处理出一个二进制数, 从低位数起,若第 i 位为1则表示此节点可以匹配到一个长度为 (i - 1) 的目标串。我们检查目标串时也会借鉴这个思路,我们用一个二进制数来表示目标串的每一个位置,从低位数起,若第 i 位为1则表示这个位置往前走 (i - 1) 步可以到达之前的一个合法前缀。也就是 f(i) = f(i - 1) << 1,且如果找到一个答案,我们就给 f(i) OR 1。
这样我们找一次的复杂度是∣t∣, 预处理fail跳转是的复杂度是 ∣s∣2n 。
实现
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 20;
const int MAX_PAT_L = 20;
const int MAX_TAR_L = 2e6;
int trie[MAX_N * MAX_PAT_L + 1][26], cnt;
int len[MAX_N * MAX_PAT_L + 1];
bool exists[MAX_N * MAX_PAT_L + 1];
int fail[MAX_N * MAX_PAT_L + 1];
void insert(char str[])
{
int p = 0;
auto c = &str[0];
for (; *c; ++c)
{
if (!trie[p][*c - 'a']) trie[p][*c - 'a'] = ++cnt;
p = trie[p][*c - 'a'];
}
exists[p] = true;
len[p] = c - str;
}
void build()
{
queue<int> q;
for (int c = 0; c != 26; ++c)
{
if (trie[0][c]) q.emplace(trie[0][c]);
}
while (q.size())
{
auto u = q.front();
q.pop();
auto f = fail[u];
for (int c = 0; c != 26; ++c)
{
if (trie[u][c])
{
fail[trie[u][c]] = trie[f][c];
q.emplace(trie[u][c]);
}
else trie[u][c] = trie[f][c];
}
}
}
char tar[MAX_TAR_L + 2];
int vis[MAX_N * MAX_PAT_L + 1];
int mat[MAX_TAR_L + 2];
int main(void)
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i != n; ++i)
{
char bf[MAX_PAT_L + 1];
scanf(" %s", bf);
insert(bf);
}
build();
for (int u = 1; u <= cnt; ++u)
{
for (int v = u; v; v = fail[v])
{
if (exists[v]) vis[u] |= 1 << (len[v]);
}
}
while (m--)
{
scanf(" %s", tar + 1);
mat[0] = 1;
int p = 0;
int ans = 0;
for (auto c = &tar[1]; *c; ++c)
{
p = trie[p][*c - 'a'];
int i = c - tar;
mat[i] = mat[i - 1] << 1;
if (mat[i] & vis[p]) mat[i] |= 1, ans = i;
}
printf("%d\n", ans);
}
return 0;
}
标签:AC,PAT,int,MAX,压缩,trie,fail,自动机,节点 来源: https://www.cnblogs.com/julic20s/p/16364783.html