KMP演算法
KMP演算法是一個字串演算法,通常用於匹配字串。
KMP演算法的原理
如果我們暴力列舉下標 \(i,j\),\(i\) 是文字串的下標,\(j\) 是模式串(你要在文字串中匹配的字串)的下標,時間複雜度 \(O(NM)\),其中 \(N,M\) 分別為文字串和模式串的長度。
我們看一下匹配過程:(gif 動圖請耐心觀看)
時間複雜度高吧,出題人隨便就 \(hack\) 掉了。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|
文字串 | x | y | x | y | x | y | x | y | x | w |
模式串 | x | y | x | y | x | y | w | y |
咦?我們會發現 \(文字串.substr(3,4)=模式串.substr(1,4)=模式串.substr(3,4)="xyxy"\),這樣我們 \(i=7,j=7\) 匹配失敗時可以跳 \(2\) 次(\(j=3\)),就可以達到正確性和時間複雜度平衡的效果。
我們維護 \(nxt_i\) 表示s和s以i結尾的最長公共前字尾的長度,這樣我們在 \(文字串_i,模式串_j\) 匹配失敗時 \(j\) 可以直接跳到 \(nxt_j\)。
維護 nxt[i]
若 \(s_i==s_j\) 也就是 \(模式串_i,模式串_j\) 匹配時,nxt[++i]=++j
(其他同理寫法也可以,最好固定一個寫法),否則按文字串和模式串匹配失敗來。
程式碼
void getNext(string s)//初始化和文字串沒關係
{
nxt[0] = -1;
int i = 0, j = -1;
while (i < s.size())
if (j == -1 || s[i] == s[j])
nxt[++i] = ++j;
else
j = nxt[j];
return;
}
void KMP(string s, string t)//P3375的詢問程式碼
{
getNext(t);
int i = 0, j = 0;
while (i < s.size())
{
if (j == t.size() - 1 && s[i] == t[j])
{
cout << i - j + 1 << '\n';
j = nxt[j];
}
if (j == -1 || s[i] == t[j])
i++, j++;
else
j = nxt[j];
}
return;
}
nxt 陣列的性質
nxt[i]
既表示以i結尾的最長公共前字尾的長度,又表示 \(i\) 失配時跳躍的位置;nxt[i]
越大,匹配的速度越慢,但至少移動 \(1\) 步;- 對於字串 \(s\),
nxt[]
的最大下標s.size()
;
KMP演算法應用
P3375 【模板】KMP
P4391 [BOI2009] Radio Transmission 無線傳輸
給你一個字串 \(s_1\),它是由某個字串 \(s_2\) 不斷自我連線形成的(保證至少重複 \(2\) 次)。但是字串 \(s_2\) 是不確定的,現在只想知道它的最短長度是多少。
不想說過程,直接說結論:ans = n - nxt[n]
CF1200E Compress Words
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> s;
if (ans.empty())
ans = s;
else
{
int len = min(s.size(), ans.size());
string s1 = s.substr(0, len);
string s2 = ans.substr(ans.size() - len, len);
string s3 = s1 + "#" + s2;//中間必須拼上"#",不然有可能最長公共前字尾重合。
getNext(s3);
ans += s.substr(nxt[s3.size()]);
}
}
cout << ans;
CF126B Password
- 目標子串 \(t\) 一定是 \(s\) 的公共前字尾;
- 求出
nxt[]
陣列,並擷取最長公共前字尾 \(tmp\); - 在 \(s[1, len-2]\) 範圍內跑KMP,若找到 \(tmp\),則 \(tmp\)就是答案;
- 若
nxt[nxt[n]] != -1
,則 \(s[0,nxt[nxt[n]]\) 即為答案;
P3435 [POI2006] OKR-Periods of Words
- 根據畫圖推導,對於 \(s\) 的每一個字首 \(t\),要找 \(t\) 的最短公共前字尾;
int find(int x)//最短公共前字尾
{
if (nxt[x] <= 0)
return x;
return nxt[x] = find(nxt[x]);
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> s;
getNext(s);
for (int i = 1; i <= n; i++)
ans += i - find(i);
cout << ans;
return 0;
}
P4824 [USACO15FEB] Censoring S
- 刪除 \(t\) 串之後產生的新的 \(t\) 串的起點一定在刪除位置的左側;
- 後出現 \(t\) 串先處理,考慮用棧維護;
- 棧中儲存的下標維護已經匹配的 \(t\) 串的位數,
match[i]
;
//兩種寫法:
//1
void KMP(string s, string t)
{
getNext(t);
int i = 0, j = 0;
while (i < s.size())
{
/*if (j == t.size() - 1 && s[i] == t[j])
{
cout << i - j + 1 << '\n';
j = nxt[j];
}*/
if (j == -1 || s[i] == t[j])
{
st.push_back({ s[i],j + 1 });
i++, j++;
}
else
j = nxt[j];
if (j == t.size())
{
for (int i = 1; i <= t.size(); i++)
st.pop_back();
j = st.back().second;
}
}
return;
}
//2
void KMP(string s, string t)
{
getNext(t);
int i = 0, j = 0;
while (i < s.size())
{
if (j == t.size() - 1 && s[i] == t[j])
{
i++;
for (int i = 1; i < t.size(); i++)
st.pop_back();
j = st.back().second;
continue;
}
if (j == -1 || s[i] == t[j])
{
st.push_back({ s[i],j + 1 });
i++, j++;
}
else
j = nxt[j];
}
return;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> s >> t;
KMP(s, t);
for (auto& [x, y] : st)
cout << x;
return 0;
}
P4591 [TJOI2018] 鹼基序列
截止 \(2024\) 年 \(7\) 月 \(16\) 日,此題難度 \(\color{#9d3dcf} 省選/NOI−\)。
- 定義 \(dp_{i,j}\) 表示前 \(i\) 個氨基酸可能的鹼基序列以 \(s_j\) 結尾的可能的方案數;
- 答案為:\(\sum^{s.size()-1}_ {i=0}dp_{n,i}\);
- 狀態轉移方程:
dp[i][j+t.size()-1]=dp[i-1][j-1]
(這裡我寫的是雜湊的); - 初始狀態:
dp[0][i]=1
。
void KMP(string s, string t)
{
getNext(t);
int i = 0, j = 0;
while (i < s.size())
{
if (j == t.size() - 1 && s[i] == t[j])
{
dp[cnt][i] += dp[cnt - 1][i - j - 1];
j = nxt[j];
continue;
}
if (j == -1 || s[i] == t[j])
i++, j++;
else
j = nxt[j];
}
return;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> s;
s = "#" + s;
for (int i = 0; i < s.size(); i++)
dp[0][i] = 1;
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
cnt = i;
while (x--)
{
cin >> t;
KMP(s, t);
}
}
for (int i = 0; i < s.size(); i++)
ans += dp[n][i];
cout << ans % mod;
return 0;
}