字串演算法
by 建國中學 賴昭勳
今天的內容
-
字串匹配
-
Hashing
-
Z-Algorithm
-
KMP
字串是什麼
一堆字元
反正字元要是什麼就可以是什麼
字母
數字
軟軟
上古漢語
所以字串是什麼
一堆字元用一維的順序合在一起
aka. 整數序列
跟字串有關的問題
字串匹配:問一些字元是否一樣的問題(數值不重要)
字典序問題:字元有定義大小順序
字串匹配 #0:
C++ 裡面的 string
-
據說不用#include string 就能用
-
類似字元的vector,所有功能和vector 類似
string s;
cin >> s;
cin.ignore()
getline(cin, s);
s += 'a';
cout << s.size() << endl;
cout << s[0] << endl;
cout << s.substr(0, 5) << endl;
reverse(s.begin(), s.end());
要怎麼知道兩個字串是否相同?
阿不就一個一個比
int main() {
string a, b;
cin >> a >> b;
int same = 1;
for (int i = 0;i < a.size();i++) {
if (a[i] == b[i]) {
same = 0;
break;
}
}
if (same) {
cout << "JIZZ" << endl;
} else {
cout << 7122 << endl;
}
}
但是同一個字串跟很多東西比怎麼辦?
或是在一個字串中找某一個子字串?
字串匹配 #1:
哈希 (Hash)
(可用的)有點唬爛的方法
Hash: 資料壓縮
INFOR33rd->712233
IOIGold->tmw
不可逆
一對一關係
H(x) = y
可以用的hash方法
多項式表示法:
\((s_0*p^{n - 1} + s_1*p^{n - 2} + ... + s_{n - 2}*p^1 + s_{n - 1}*p^0) \% m\)
簡單來說就是把字串\(s\)
當成一個\(\ p\ \)位數的數字,再取模數
m, p 要怎麼選
通常 \(p\) 需要是一個比字元數還多的質數
如小寫字母者可選\(p = 29\),
ASCII 可使用\(p = 257\)
\(m\)是一個很大的質數
常見的有:\(10^9 + 7, 998244353\)
這樣怎麼比對字串?
如果字串\(s == t\),可以先分別把他們兩個Hash,再看兩者的 Hash 值是否相同就好了!
Hash 一樣 | Hash 不同 | |
---|---|---|
相同 | ||
不同 | ? | ? |
機率盡量小!
各種切字串->前綴
一邊做一邊存
\(O(n)\) 的時間建出所有前綴的Hash 值
各種切字串->子字串
假設要找 \([l, r]\)的 Hash 值,要怎麼從前綴Hash \(h[]\)得到?
區間和-> 前綴相減?
各種切字串->迴文?
迴文:正著念反著念都一樣
就正反Hash 一次!
切好子字串,對應到相同的index
差常數倍
石唑
https://tioj.ck.tp.edu.tw/problems/1306
之後會一直寫這題的喔!
參考程式碼
#include <iostream>
#include <vector>
#define ll long long
using namespace std;
const ll P = 401, M = 998244353;
ll hashes[10005], modp[10005];
ll hashp(string s, bool saveval) {
ll val = 0;
int index = 0;
for (char c:s) {
val = ((val * P) % M + c) % M;
if (saveval) hashes[index] = val;
//cout << val << endl;
++index;
}
return val;
}
void modpow(int base, int exp) {
ll b = 1;
modp[0] = 1;
for (int i = 0; i < exp; i++) {
b = (b * base) % M;
modp[i + 1] = b;
}
}
ll subseq (int l, int r) { //[l, r]
if (l == 0) return hashes[r];
return(((hashes[r] - hashes[l - 1] * modp[r - l + 1]) % M + M) % M);
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
modpow(P, 10001);
for (int i = 0; i < n; ++i) {
string T;
int q;
cin >> T;
ll useless = hashp(T, true);
cin >> q;
for (int j = 0; j < q; ++j) {
string p;
cin >> p;
ll h = hashp(p, false);
int cnt = 0;
for (int k = 0; k < T.size() - p.size() + 1;++k) {
//cout << h << " " << subseq(k, k + p.size() - 1) << endl;
if (subseq(k, k+p.size() - 1) == h) ++cnt;
}
cout << cnt << "\n";
}
}
return 0;
}
給一個字串\(S\),問有多少個環狀位移是迴文。\(|S| \leq 10^5\)
字串匹配 #2:
Z-Algorithm
zzz...
-
確定性的\(O(n + m)\)字串匹配
-
Code 少好做(?
要來建奇怪的陣列了
\(z[i]\)代表從\(s_i\)開始的後綴與\(s\)的最長共同前綴長度
A | A | B | B | A | A | B |
---|---|---|---|---|---|---|
X | 1 | 0 | 0 | 3 | 1 | 0 |
暴力法
直接做?\(O(n^2)\)
while (i + cnt < ps && p[cnt] == p[i + cnt]) {
cnt++;
}
觀察一下...
對於子字串\(s[l, r] = s[0, r - l]\),我們紀錄最大出現過的右界\(r\)
這同時代表著我們目前考慮到的最右邊
假設現在考慮 \(z[i]\)
如果\(i > r\),就直接暴力做
否則...
a a a b a a a c b
\(z[i]\) 至少有 \(z[i - l]\)那麼大!
假設現在考慮 \(z[i]\)
如果\(i > r\),就直接暴力做
否則令\(z[i] = min(r - i + 1, z[i - l])\),繼續暴力做
a a a b a a a c b
這樣複雜度是好的喔?
4
Z-Algorithm 的複雜度分析
觀察在計算的時候,\(r\)遞增
每次計算\(z[i]\)時,有以下情況
1. \(i > r\),則 \(r\)增加,均攤\(O(n)\)
2. \(i \leq r, i + z[i - l] < r\),則不會再往後
搜尋,\(O(1)\)
3. \(i \leq r, i + z[i - l] >= r\),則\(r\)增加,均攤\(O(n)\)
整體複雜度:\(O(n)\)
那要怎麼做字串匹配
把要配對的單字(word) 和文句(paragraph)串在一起做 Z 陣列!
a | b | # | a | c | a | a | b | d |
---|
\(z[i]\)為單字的長度
#include <iostream>
#include <algorithm>
#include <string>
#define ll long long
#define maxn 20005
using namespace std;
int z[maxn];
int main() {
ios_base::sync_with_stdio(0);cin.tie(0);
int t;
cin >> t;
while (t--) {
string s;
cin >> s;
int q;
cin >> q;
while (q--) {
string p;
cin >> p;
int ans = 0;
int l = 0, r = 0, ps = p.size(), ss = s.size();
z[0] = 0;
int cnt;
for (int i = 1;i < ps;i++) {
if (i > r) cnt = 0;
else cnt = min(r - i + 1, z[i - l]);
while (i + cnt < ps && p[cnt] == p[i + cnt]) cnt++;
z[i] = cnt;
cnt--;
if (i + cnt > r) l = i, r = i + cnt;
}
z[ps] = 0;
for (int i = 0;i < ss - ps + 1;i++) {
if (i + ps + 1 > r) cnt = 0;
else cnt = min(r - int(ps + i), z[ps + i + 1 - l]);
while (i + cnt < ss && cnt < ps && p[cnt] == s[i + cnt]) cnt++;
z[ps + i + 1] = cnt;
if (cnt == ps) ans++;
cnt--;
if (i + cnt > r) l = ps + 1 + i, r = ps + 1 + i + cnt;
}
cout << ans << "\n";
}
}
return 0;
}
然後這題丟上去會TLE
字串匹配 #3:
KMPlayer(x
很難看懂的東西
Failure Function
次長共同前後綴
ababccab
babbabba
Failure Function Table FFT(x)
定義 \(p[i]\)為字串第\(i\) 個前綴(s0...i - 1)的次長共同前後綴
ex. j i z j i
j
j i
j i z
j i z j
j i z j i
對應到的數值為
0
0
0
1 -> j
2 -> ji
怎麼找?
爆搜:\(O(n^3)\)
每個前綴都要比較\(n\)次字串,每次\(O(n)\)
484 有點太爛了?
觀察性質 #1
\(p[i + 1] \leq p[i] + 1\)
babbabbab?
^這個不可能再往左邊
觀察性質 #2
判斷的時候只要看一個字元
babbabbab?
如果相同,\(p[i + 1] = p[i] + 1\)
那麼不相同呢?
觀察性質 #2-2
找到最大的 \(j\),使得\(p[i + 1] = j\)
(符合共同前後綴條件)
babbabbab?
\(j = p[p[i] - 1]\) 是可能會對的數值中最大的!
從綠色再檢查一次
演算法總結
-
存一個陣列\(p[i]\),代表字串\(\ s\)第\(\ i\)個前綴的次長共同前後綴
-
邊界條件:\(p[0] = 0\)
-
若已經做完\(\ p[0]...p[i]\),令\(j = p[i]\)
-
若\(\ s[i + 1] = s[j]\),則\(\ p[i + 1] = j + 1\)
-
否則,更新\(\ j = p[j]\),重複以上迴圈直到上述條件成立
-
若最後\(\ j = 0\),則\(\ p[i + 1] = 0\)
複雜度證明
由性質#1得知,\(p[i]\) 的值不會超過\(n\),且每次最多加一,因此\(p[i]\)的變動量不會超過\(2*n = O(n)\) (也就是加了多少才能扣多少),且每次改變時皆為\(O(1)\)字元比較,故得證
那要怎麼找相同子字串呢?
簡單做法:跟Z-Algorithm 一樣,把要匹配的兩個字串串起來做一次Failure Function
如:要在 TTFTFFT 裡面找 FFT 的話
就對 FFT#TTFTFFT 做Failure Function
如果在後面 \(p[i] = len(s)\)那就是有找到
但是這樣484太慢了?
真正的KMP
每次遇到不能匹配時,快速回到下一個可以的位置。
要在 S 中尋找W 的出現
紀錄兩個變數:\(m\) -> W目前第一個字元的位置,\(i\) ->比到W的第幾個字元
此處 \(m = 4, i = 6\)
性質 #1
黑線畫的AB是下一個可能匹配到的開頭。
即m須更新為:大於m的數字中,最小可能匹配到的開頭。
可能匹配到的開頭 -> 剩下的是 W的前綴
性質 #2
當我更新完 W的值之後,AB不用再重複跑一遍。因為已和 S 配對且是 W 的前綴。
因此,只要從原本的位置繼續找就好了。
演算法簡介
1. 對單字\(W\)做Failure Function
2. 紀錄\(W\)在\(S\)中起始的位置為\(m\),
當前比較到\(W[i]\)和\(S[i + m]\)。
3. 增加\(i\)直到\(W[i] \neq S[i + m]\),如
果\(i \geq W.size()\)那就配對到了
4. 令\(m' = m + i - p[i - 1], i = p[i - 1] \)
,重複步驟 3直到 \(m > S.size() -W.size()\)
複雜度證明
1. 注意到由於\(i - p[i - 1] \geq 0\),\(m\)必然遞增
2. \(m + i = (m + i - p[i - 1]) + (p[i - 1]) \)在更新時呈非嚴格遞增
因此均攤複雜度\(O(n)\)
#include <iostream>
#include <algorithm>
#include <string>
#define ll long long
#define maxn 20005
using namespace std;
int kmp[maxn];
int main() {
ios_base::sync_with_stdio(0);cin.tie(0);
int t;
cin >> t;
while (t--) {
string s;
cin >> s;
int q;
cin >> q;
while (q--) {
string p;
cin >> p;
int ps = p.size(), ss = s.size(), ans = 0;
kmp[0] = 0;
for (int i = 1;i < ps;i++) {
kmp[i] = kmp[i - 1];
while (p[i] != p[kmp[i]]) {
if (kmp[i] == 0) {
kmp[i] = -1;
break;
}
kmp[i] = kmp[kmp[i] - 1];
}
kmp[i]++;
}
int ind = 0;
for (int m = 0;m < ss - ps + 1;) {
while (ind < ps && p[ind] == s[m + ind]) ind++;
ind--;
if (ind == ps - 1) ans++;
if (ind == -1) {
m++;
ind = 0;
} else {
m = m + ind - kmp[ind] + 1;
ind = kmp[ind];
}
}
cout << ans << "\n";
}
}
return 0;
}
KMP 的應用
找到字串中有多少的獨特的子字串,複雜度\(O(n^2)\)
每次增加一個字元更新答案
從反的做回來,KMP開下去。
新的子字串->前綴!
a b a c c a b a
0 | 0 | 1 | 0 | 0 | 1 | 2 | 3 |
---|
有 \(max(p[i])\)個字串絕對有重複過!
答案增加 \(len - max(p[i])\)
TIOJ 1531
TIOJ 1091
Copy of 字串演算法
By yennnn
Copy of 字串演算法
- 297