by 建國中學 賴昭勳

• 字串匹配

• Hashing

• Z-Algorithm

• KMP

• Trie

• Manacher

aka. 整數序列

# C++ 裡面的 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 < 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

# 可以用的hash方法

多項式表示法：

$$(s_0*p^{n - 1} + s_1*p^{n - 2} + ... + s_{n - 2}*p^1 + s_{n - 1}*p^0) \% m$$

# m, p 要怎麼選

ASCII 可使用$$p = 257$$

$$m$$是一個很大的質數

Hash 一樣 Hash 不同

# 各種切字串->前綴

### 一邊做一邊存

H(i - 1) = (s_0*p^{i - 1} + s_1*p^{i - 2} + ... + s_{i - 1}*p^0) \% m \ ...(1)
H(i) = (s_0*p^i \ + \ s_1*p^{i - 1} + ... + s_{i - 1}*p^1 + s_i*p^0) \% m \\ = ((1) * p + s_i*p^0) \% m

$$O(n)$$ 的時間建出所有前綴的Hash 值

# 各種切字串->子字串

### 假設要找 $$[l, r]$$的 Hash 值，要怎麼從前綴Hash $$h[]$$得到？

h[l - 1] * p^{r - l + 1}

# 各種切字串->迴文？

h[l - 1] * p^{r - l + 1}

# 參考程式碼

#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;
}

## 練習：

https://tioj.ck.tp.edu.tw/problems/1321

### CSA Palindromic Concatenation

$$n, \sum |S| \leq 10^5$$

## 補: 關於雜湊函數

• 雜湊值完全取決於輸入
• 雜湊函數用到所有輸入的資料
• 雜湊函數「平均地」將輸入資料分攤在所有可能數值
• 相近的輸入值通常會出現差很多的雜湊值

Birthday Attack

Z-Algorithm

# 要來建奇怪的陣列了

### $$z[i]$$代表從$$s_i$$開始的後綴與$$s$$的最長共同前綴長度

A A B B A A B
X 1 0 0 3 1 0

$$i$$

# 暴力法

## 直接做？$$O(n^2)$$

while (i + cnt < ps && p[cnt] == p[i + cnt]) {
cnt++;
}

# a d a d c a d a c b

l \ \ \ \ \ \ \ \ \ r
i
i - l

# a d a d c a d a c b

l \ \ \ \ \ \ \ \ \ r
i
i - l

## 實作細節

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

## Z-Algorithm 的複雜度分析

1. $$i > r$$，則 $$r$$增加，均攤$$O(n)$$

2. $$i \leq r, i + z[i - l] < r$$，則不會再往後

3. $$i \leq r, i + z[i - l] >= r$$，則$$r$$增加，均攤$$O(n)$$

## 那要怎麼做字串匹配

a b # a c a a b d

$$z[i]$$為單字的長度

## Atcoder: Iroha Loves Strings

#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 有點太爛了？

^這個不可能再往左邊

i + 1

i + 1

# babbabbab?

i + 1

$$j = p[p[i] - 1]$$ 是可能會對的數值中最大的！

# 演算法總結

1. 存一個陣列$$p[i]$$，代表字串$$\ s$$第$$\ i$$個前綴的次長共同前後綴

2. 邊界條件：$$p[0] = 0$$

3. 若已經做完$$\ 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$$

# 複雜度證明

## 真正的KMP

• 要在 S 中尋找W 的出現

• 紀錄兩個變數：$$m$$ -> W目前第一個字元的位置，$$i$$ ->比到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])$$在更新時呈非嚴格遞增

#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

# TRIE~

aka 字典樹

## 存取字串之間的相似關係！

### 每個節點是一個狀態，每條邊有一個字元，由父節點到葉節點是字串。重複的字元會合併。

$$n \leq 10^5, |S| \leq 10^6, \sum |T| \leq 10^6$$

CHE

CHEESE

CHEIS

CCC

OR

c

c

c

o

r

h

e

e

e

i

s

s

DFS!

# 看看怪問題

$$|S| \leq 10^6$$

# 奇數/偶數回文長度?

abbacbc-> *a*b*b*a*c*b*c*

# 怪陣列

Z: 目前匹配到最右邊的字串與左界

Manacher: 目前找到最右邊的回文與中心

*b*a*b*b*a*a*

$$f[i]$$至少是$$min(r - i, f[mid - (i - mid)])$$

mid
i
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";
}


By justinlai2003

• 3,175