其他分享
首页 > 其他分享> > L 语言 [AC自动机][状态压缩]

L 语言 [AC自动机][状态压缩]

作者:互联网

题目链接[洛谷]

题目大意:

给出一个大小为 n 的模式串集合 Sm 个目标串 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