1. 程式人生 > 實用技巧 >KMP,擴充套件KMP,AC自動機總結+模板

KMP,擴充套件KMP,AC自動機總結+模板

為了方便統一,本文中下標均從0開始
KMP
P3375 【模板】KMP字串匹配
對於兩個字串S1,S2(S1>S2),求S2在S1中的出現位置
例如S1=ababa,S2=aba
在這個樣例中答案就是0 2
首先考慮暴力做法,對於S1的每一個字元,我們都一該字元開始往後與S2對比.時間複雜度為\(O(n^2)\)
很明顯這並不是我們想要的,所以考慮優化,仔細觀察一下,其實我們不用每個字元都去列舉一遍
如圖

我們假設圖中綠色區域的字串相等,那麼當第i個字元匹配完時,對於第i+1個字元,我們不需要去列舉所有字元,因為我們可以知道,
圖中綠色區域時已經匹配好了的,所以我們就只需要從綠色區域以後開始匹配就行了.
那麼我們就可以設一個next陣列,表示以第i個字元終點,相同的字首和字尾的最大長度為多少
如上面的aba
那麼next[0]=1,next[1]=1,next[2]=1
注意不能包括自己本身,因為這樣的話存的東西就沒有了意義,一直都是它本身的長度
那麼在匹配的時候對於第i個字元,如果相等,那麼已經匹配好的長度就加1,如果不等,就開始往回跳

我們每次就往會跳,知道目前匹配的字元與第i個字元相等,這就是匹配的過程,對於求next的過程,其實也差不多,可以看成兩個S2在做匹配
code

#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1e6 + 5;

string p, s;
int net[N], ans[N], cnt;

int main()
{
    cin >> s >> p;
    int n = p.length(), m = s.length();
    for (int i = 1, j = 0; i < n; i++)
    {
        while (j && p[i] != p[j])
            j = net[j - 1];
        if (p[i] == p[j])
            j++;
        net[i] = j;
    }
    for (int i = 0, j = 0; i < m; i++)
    { 
        while (j && s[i] != p[j])
            j = net[j - 1];
        if (s[i] == p[j])
            j++;
        if (j == n)
        {
            cout << i - n + 2 << endl;
            j = net[j - 1];
        }
    }
    for (int i = 0; i < n; i++)
        cout << net[i] << " ";
    return 0;
}

擴充套件KMP
P5410 【模板】擴充套件 KMP(Z 函式)
之所以叫擴充套件KMP,肯定時因為這個東西要高階一點.S1,S2同上
擴充套件KMP求的東西與KMP中的next有點相似,它求的時以第i個字元為起點的字首與S2的最大字首長度
那麼這個東西要怎麼求?我們先引入一個z陣列,它表示S2中以第i個字元開頭的字尾與前最的最長公共長度
例如對於S1=aaaabaa,S2=aaaaa
那麼z[0]=5,z[1]=4,z[2]=3,z[3]=2,z[4]=1
考慮暴力做法,對於每一個字元,同樣是往後遍歷一邊,複雜度為\(O(n^2)\)
那麼如何用這個z陣列來優化這個演算法?

我們假設字串[L,R]是我們之前已經求出的R最大的字首那麼對於S2, 下標就為[0,R-L],那麼這個時候就要分兩種情況討論了,若i>R,
那麼說明我們無法利用前面已知的資訊,只能暴力匹配,若i<=R,那麼我們就可以知道以第i個字元為起點的字首的初始長度應為min(R-i+1,z[i-L]).
如果說z[i-L]是大於R-i+1的,那麼對於R之後的字元,我們任需暴力匹配,但總時間複雜度任為O(n).這點可以證明,在這裡不過多解釋
對於z陣列,求法同KMP的next陣列,將S2與S2自身匹配.

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 2e7 + 5;
typedef long long ll;

char a[N], b[N];
int ex[N], z[N], l1, l2;

void zbox()
{
    int l = 0, r = 0;
    z[0] = l2;
    for (int i = 1; i < l2; i++)
    {
        if (i > r)
            z[i] = 0;
        else 
            z[i] = min(r - i + 1, z[i - l]);
        while (i + z[i] < l2 && b[i + z[i]] == b[z[i]])
            z[i]++;
        if (i + z[i] - 1 > r)
            r = i + z[i] - 1, l = i;
    }
}

void exkmp()
{
    int l = 0, r = 0;
    while (ex[0] < l1 && ex[0] < l2 && a[ex[0]] == b[ex[0]])
        ex[0]++;
    for (int i = 1; i < l1; i++)
    {
        if (i > r)
            ex[i] = 0;
        else 
            ex[i] = min(r - i + 1, z[i - l]);
        while (i + ex[i] < l1 && ex[i] < l2 && a[i + ex[i]] == b[ex[i]])
            ex[i]++;
        if (i + ex[i] - 1 > r)
            r = i + ex[i] - 1, l = i;
    }
}

int main()
{
    scanf("%s%s", &a, &b);
    l1 = strlen(a), l2 = strlen(b);
    zbox();
    exkmp();
    ll ans1 = 0, ans2 = 0;
    for (int i = 0; i < l2; i++)
        ans1 ^= (ll)(i + 1) * (z[i] + 1);
    for (int i = 0; i < l1; i++)
        ans2 ^= (ll)(i + 1) * (ex[i] + 1);
    printf("%lld\n%lld", ans1, ans2);
    return 0;
}

AC自動機
KMP保證一個字串時為線性,那麼對於多個字串,就需要AC自動機了,注意它和自動AC機的區別,它並不能自動AC題目,雖然我以前一直以為它時這個意思.
P3808 【模板】AC自動機(簡單版)
對於一個字串,以及一堆長度小於它的模式串,求這個字串出現了多少個模式串.
例母串為ababa,模式串為a ab aba bc
那麼答案為3
AC自動機是KMP與trie樹的結合
如樣例,首先建trie樹

其中有綠色標記的代表單詞結尾
其思想其實和KMP差不多,只是改成了在樹上跳而已
程式碼

for (int i = 0, j = 0; str[i]; i++)
{
      int t = str[i] - 'a';
      while (j && !tr[j][t])
            j = net[j];
      int p = j;
      while (p)
      {
            ans += cnt[p];
            cnt[p] = 0;
            p = net[p];
      }
}

這裡可以有個優化,就是在建trie圖的時候,直接記錄到可以跳的位置,那麼就可以省掉一層迴圈

#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;

const int N = 1e6 + 5;
int tr[N][26], net[N], cnt[N], idx, q[N], front, tail = -1;
char str[N];

void insert()
{
    int p = 0;
    for (int i = 0; str[i]; i++)
    {
        int t = str[i] - 'a';
        if (!tr[p][t])
            tr[p][t] = ++idx;
        p = tr[p][t];
    }
    cnt[p]++;
}

void build()
{
    for (int i = 0; i < 26; i++)
        if (tr[0][i])
            q[++tail] = tr[0][i];
    while (front <= tail)
    {
        int t = q[front++];
        for (int i = 0; i < 26; i++)
        {
            int p = tr[t][i];
            if (!p)
                tr[t][i] = tr[net[t]][i];
            else 
            {
                net[p] = tr[net[t]][i];
                q[++tail] = p;
            }
        }
    }
}

int main()
{
    int n, ans = 0;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%s", &str);
        insert();
    }
    build();
    scanf("%s", &str);
    for (int i = 0, j = 0; str[i]; i++)
    {
        int t = str[i] - 'a';
        j = tr[j][t];
        int p = j;
        while (p)
        {
            if (cnt[p] == -1)
                break;
            ans += cnt[p];
            cnt[p] = -1;
            p = net[p];
        }
    }
    printf("%d", ans);
    return 0;
}

AC自動機的拓撲優化這裡就不寫了