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自動機的拓撲優化這裡就不寫了