by 建國中學 賴昭勳
字串匹配
Hashing
Z-Algorithm
KMP
Trie
Manacher
字母
數字
軟軟
上古漢語
aka. 整數序列
語法複習
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 < min(b.size(), a.size());i++) {
if (a[i] != b[i]) {
same = 0;
break;
}
}
if (same) {
cout << "JIZZ" << endl;
} else {
cout << 7122 << endl;
}
}
Hash (雜湊)
原則上:
不可逆
多對一關係
H(x) = y
多項式表示法:
(s0∗pn−1+s1∗pn−2+...+sn−2∗p1+sn−1∗p0)%m
簡單來說就是把字串s
當成一個 p 進位的數字,再取模數
通常 p 需要是一個比字元數還多的質數
如小寫字母者可選p=29,
ASCII 可使用p=257
m是一個很大的質數
常見的有:109+7,998244353
還有1015+37,712271227,1000696969
如果字串s==t,可以先分別把他們兩個Hash,再看兩者的 Hash 值是否相同就好了!
Hash 一樣 | Hash 不同 | |
---|---|---|
相同 | ||
不同 | ? | ? |
機率盡量小!
O(n) 的時間建出所有前綴的Hash 值
區間和-> 前綴相減?
迴文:正著念反著念都一樣
就正反Hash 一次!
切好子字串,對應到相同的index
差常數倍
之後會一直寫這題的喔!
#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∣≤105
給n個字串,問有多少對(i,j)使得Si,Sj合併起來是迴文。
n,∑∣S∣≤105
好的雜湊函數會有這些條件:
在此前提下,可以把它想成一種隨機分布
隨機挑選O(C)個輸入就有高機率出現碰撞,其中C是輸出範圍。
如果想要用 Hash 找 n個字串是否有一組相同的字串,那就要注意此問題。
Z-Algorithm
A | A | B | B | A | A | B |
---|---|---|---|---|---|---|
X | 1 | 0 | 0 | 3 | 1 | 0 |
i
while (i + cnt < ps && p[cnt] == p[i + cnt]) {
cnt++;
}
int n = s.size();
int l = 0, r = 0;
z[0] = 0;
for (int i = 1;i < n;i++) {
if (i <= r) {
z[i] = min(z[i - l], r - i + 1);
}
while (i + z[i] < n && s[z[i]] == s[i+z[i]]) z[i]++;
if (i + z[i]-1 > r) {
l = i, r = i+z[i]-1;
}
}
4
觀察在計算的時候,r遞增
每次計算z[i]時,有以下情況
1. i>r,則 r增加,均攤O(n)
2. i≤r,i+z[i−l]<r,則不會再往後
搜尋,O(1)
3. i≤r,i+z[i−l]>=r,則r增加,均攤O(n)
整體複雜度:O(n)
把要配對的單字(word) 和文句(paragraph)串在一起做 Z 陣列!
a | b | # | a | c | a | a | b | d |
---|
z[i]為單字的長度
題解 by baluteshih
#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;
}
KMP
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
484 有點太爛了?
^這個不可能再往左邊
如果相同,p[i+1]=p[i]+1
那麼不相同呢?
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
如:要在 TTFTFFT 裡面找 FFT 的話
就對 FFT#TTFTFFT 做Failure Function
如果在後面 p[i]=len(s)那就是有找到
但是這樣484太慢了?
每次遇到不能匹配時,快速回到下一個可以的位置。
要在 S 中尋找W 的出現
紀錄兩個變數:m -> W目前第一個字元的位置,i ->比到W的第幾個字元
此處 m=4,i=6
黑線畫的AB是下一個可能匹配到的開頭。
即m須更新為:大於m的數字中,最小可能匹配到的開頭。
可能匹配到的開頭 -> 剩下的是 W的前綴
當我更新完 W的值之後,AB不用再重複跑一遍。因為已和 S 配對且是 W 的前綴。
因此,只要從原本的位置繼續找就好了。
1. 對單字W做Failure Function
2. 紀錄W在S中起始的位置為m,
當前比較到W[i]和S[i+m]。
3. 增加i直到W[i]=S[i+m],如
果i≥W.size()那就配對到了
4. 令m′=m+i−p[i−1],i=p[i−1]
,重複步驟 3直到 m>S.size()−W.size()
1. 注意到由於i−p[i−1]≥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;
}
0 | 0 | 1 | 0 | 0 | 1 | 2 | 3 |
---|
有 max(p[i])個字串絕對有重複過!
答案增加 len−max(p[i])
資訊科學的自動機(Automaton)是一個有向圖。有一個節點為起點,每一條有向邊上有某個輸入(字元)。
aka 字典樹
把字串問題用圖論觀點解釋!
例: 給定n個字串S1,...,Sn,每次詢問一個字串T,輸出有幾個字串有T是他的前綴。
n≤105,∣S∣≤106,∑∣T∣≤106
CHE
CHEESE
CHEIS
CCC
OR
c
c
c
o
r
h
e
e
e
i
s
s
DFS!
輸入一個字串,輸出最長的回文子字串長度。
∣S∣≤106
暴力做: 對每個回文的中心找最長回文。∣S∣2
在每兩個原本字串的字元間放一個特殊字元"*"
abbacbc-> *a*b*b*a*c*b*c*
偶數回文的中心會在特殊字元。
因此只要考慮以每個字元為中心就好!
令f[i]代表以第i個字元為中心時延伸最長的回文長度 (也就是說[i−f[i],i+f[i]]是回文)
另外維護r,mid代表目前看到的i+f[i]最大值以及對應的i。
假設當前的i>r,就暴力匹配。
否則...?
從左做到右考慮回文中心...
跟Z Algorithm 一樣,考慮某個非嚴格遞增的量!
Z: 目前匹配到最右邊的字串與左界
Manacher: 目前找到最右邊的回文與中心
*b*a*b*b*a*a*
f[i]至少是min(r−i,f[mid−(i−mid)])
然後再暴力匹配!
//Challenge: Accepted
#include <iostream>
#include <algorithm>
#include <vector>
#include <utility>
#define maxn 2000005
#define ll long long
using namespace std;
int p[maxn];
string s;
int main() {
ios_base::sync_with_stdio(0);cin.tie(0);
int n;
cin >> n;
string in;
cin >> in;
s += '.';
for (int i = 0;i < n;i++) {
s += in[i];
s += '.';
}
//cout << s << endl;
n = 2 * n + 1;
int l = -1, r = -1, ans = 0;
for (int i = 0;i < n;i++) {
int len = min(i, n - 1 - i);
if (i <= r) {
p[i] = min(p[2 * l - i], r - i);
}
for (int j = p[i] + 1;j <= len;j++) {
if (s[i + j] != s[i - j]) break;
p[i]++;
}
ans = max(ans, p[i]);
if (i + p[i] > r) {
r = i + p[i];
l = i;
}
//cout << p[i] << " " << l << " " << r << endl;
}
cout << ans << "\n";
}