KMP 与 ACAM

KMP

有一个串 \(T\),有一个串 \(S\)

如果要求 \(S\)\(T\) 中的一些信息,比如出现次数之类的,可以考虑 KMP。

考虑对 \(S\) 的每一个前缀 \(S[1:i]\) 求一个 \(fail_i\) 表示 \(S[1:i]\) 中一个最长的前缀 \(S[1:fail_i]\)(这里强制要求 \(fail_i\neq i\))能够与 \(S[1:i]\) 的后缀匹配上。

例子:对于串 \(S\)ABAABAA\(fail_3=1\)\(fail_4 = 1\)\(fail_5 =2\)\(fail_7 = 4\)

怎么求呢?如果要求 \(i\)\(fail\),那么考虑从 \(i-1\)\(fail\) 开始跳,令 \(p=fail_{i-1}\),一直 \(p\leftarrow fail_p\),直到 \(S[p+1]=S[i]\),那么 \(i\)\(fail\) 就是 \(p+1\)

for ( int i = 2; i <= len2; i ++) {
    int p = fail[i - 1];

    while (p && S[p + 1] != S[i]) p = fail[p];
    if (S[p + 1] == S[i]) fail[i] = p + 1;
}

求出这个就可以考虑在 \(T\) 中求答案了,以求 \(S\)\(T\) 中出现位置为例。

考虑去枚举 \(T\) 的前缀 \(T[1:i]\)

维护一个全局变量 \(p\) 表示 \(S[1:p]\) 已经与 \(T[1:i]\) 的后缀匹配。

假如现在要匹配 \(S[p+1]\)\(T[i]\)

如果 \(S[p+1]=T[i]\),直接匹配过去即可。

如果失配了,那么让 \(p\leftarrow fail_p\),直到 \(S[p+1]=T[i]\) 匹配上为止。

如果 \(p=|S|\) 就说明被匹配到了一个 \(S\)

int p = 0; // p 表示 S[1:p] 已经与 T[1:i] 的后缀匹配。
for ( int i = 1; i <= lenT; i ++) {
    while (p && S[p + 1] != T[i]) p = fail[p]; // 失配了,往前面跳
    if (S[p + 1] == T[i]) p ++;

    if (p == lenS) ans.push_back(i - lenS + 1);
}

考虑复杂度,唯一要证明的就是暴力跳 \(fail\) 的复杂度。

因为每次跳 \(fail\),匹配上的串长度(也就是 \(p\) 的值)会变小,只有匹配上的时候 \(p\) 会加 \(1\),最多增加 \(|T|\) 次,所以这个操作均摊 \(O(|T|)\)

ACAM

ACAM 可以做多个串 \(S\) 在串 \(T\) 上的匹配问题。

这玩意儿和 KMP 差不多。

如果给单个 \(S\) 建字典树,就是一个链。

那么 ACAM 就是 KMP 从链扩展到树的过程。

把这些串 \(S\) 插到 Trie 上,树上每一个节点 \(u\) 对应了一个前缀,把 \(u\) 对应的前缀称为状态 \(u\)

还是类似于 KMP,给每个节点 \(u\) 搞一个 \(fail\),定义与 KMP 的差不多,还是找到一个节点 \(v\),使得 \(v\) 所对应的前缀为 \(u\) 对应的前缀的最长的后缀(强制要求 \(v\neq u\)),那么 \(fail_u=v\)

具体就在 Trie 树上 BFS,求 \(fail\) 时就像 \(KMP\) 一样暴力跳,复杂度 \(O(\sum|S|)\)

另外一种更好写的写法,对每个节点 \(u\) 在维护一个 \(nxet_{u,c}\),表示在 \(u\) 对应的前缀后面加上一个字符 \(c\) 所对应的状态的 \(fail\)

每次想要匹配时,直接访问 \(nxet\) 即可,就不用每次都跳 \(fail\) 了。

复杂度 \(O(|\sum|n)\)\(n\) 为 Trie 的节点个数)。

同时在建立 ACAM 时,可以把 \(fail\) 树建出来,后面有妙用。

现在,考虑对每个 \(S\) 求在 \(T\) 中出现的次数。

那么还是和 KMP 一样,去枚举 \(T\) 的前缀 \(T[1:i]\),现在可以直接用 \(nxet\) 数组得到匹配上 \(T[1:i]\) 的后缀的一个状态 \(u\)

然后给这个 \(u\) 对应的前缀出现次数加 \(1\),但是因为 \(next\) 里存的是最大的匹配的前缀,所以比 \(u\) 小但又能够匹配的前缀就没有被统计到,怎么办?

前面所建立的 \(fail\) 树就有用了,不难发现 \(u\) 到根上的所有节点对应的前缀都该被统计到,那么最后在 \(fail\) 树上做一个子树和即可。

直接把这道例题的代码贴过来吧。

#include <bits/stdc++.h>

void Freopen() {
    freopen("", "r", stdin);
    freopen("", "w", stdout);
}

using namespace std;
const int N = 2e6 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;

int n;
char s[M], t[N];

int tot;
int fail[M], tr[26][M], nxt[26][M], vis[M], rev[M], cnt[M];

vector< int> G[M];

void init() {
    for ( int i = 0; i <= tot; i ++) {
        fail[i] = rev[i] = vis[i] = cnt[i] = 0;
        G[i].clear();

        for ( int j = 0; j < 26; j ++)
            tr[j][i] = nxt[j][i] = 0;
    }
    tot = 0;
}

void insert( char s[], int id) {
    int u = 0, len = strlen(s + 1);

    for ( int i = 1; i <= len; i ++) {
        int & v = tr[s[i] - 'a'][u];

        if (! v) v = ++ tot;
        nxt[s[i] - 'a'][u] = v;
        u = v;
    }

    vis[u] = 1, rev[id] = u;
}

void build() {
    queue< int> q;

    for ( int i = 0; i < 26; i ++)
        if (tr[i][0]) q.push(tr[i][0]);

    while (q.size()) {
        int u = q.front(); q.pop();
        
        for ( int i = 0; i < 26; i ++) {
            int v = tr[i][u];

            if (v) {
                fail[v] = nxt[i][fail[u]];
                q.push(v);
            } else nxt[i][u] = nxt[i][fail[u]];
        }
    }

    for ( int i = 1; i <= tot; i ++)
        G[fail[i]].push_back(i);
}

void dfs( int u) {
    for ( auto v : G[u]) dfs(v), cnt[u] += cnt[v];
}

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n;

    for ( int i = 1; i <= n; i ++)
        cin >> (s + 1), insert(s, i);

    build();
    cin >> (t + 1);
    int len = strlen(t + 1);

    int u = 0;
    for ( int i = 1; i <= len; i ++) {
        u = nxt[t[i] - 'a'][u];
        cnt[u] ++;
    }

    dfs(0);

    for ( int i = 1; i <= n; i ++) cout << cnt[rev[i]] << '\n';
    
    return 0;
}
posted @ 2025-08-14 09:22  咚咚的锵  阅读(16)  评论(0)    收藏  举报