Strings
-校培 edition-
關於講師與他的廢話
- 王以安
- 又藍又笨,甚麼都做不好
- 不會字串所以當字串講師
- 過一年還是不會字串
- 段考要燒了
- 簡報字體大小詭譎,請多見諒
藍色的都會講到
一些約定
- 字串的代號會用\(s,t\)
- 字串的長度會用\(|s|或n\)表示
- 字串和陣列的第i項會用\(a[i]或a_i\)表示
- \(s的k\)前綴代表\(s[0\)~\(k-1]\)
- \(s的k\)後綴代表\(s[k\)~\(n-1]\)
- 真前綴就是除了|s|前綴以外的所有前綴
Hash
中文叫雜湊
為什麼需要這個
字串太大不好比較
我們希望透過一個函數把一個字串變成一個數字
這樣看到兩個函數值不一樣,就知道兩個字串不一樣
$$a=f(s)$$
$$這個f就是雜湊函數,a就是雜湊值$$
兩個不同字串對到同個值,則稱兩個字串碰撞了
所以怎麼做?
$$把字串壓成數字(加密),a是1,z是26$$
$$p進位,模M$$
$$p是>26的小(質)數(如31,127,257),M是大(質)數(如998244353)$$
a b c b g
$$例 : 設p=31, M=127$$
1 2 3 2 7
× × × × ×
\(p^0\) \(p^1\) \(p^2\) \(p^3\) \(p^4\)
\(1*1+2*31+3*961+2*29791+7*923521=6527175\)
這個字串的hash值就是6527175模127,也就是10
"abcbg"和"j"碰撞了
a b c b g
反過來也是行的,但值不一樣
1 2 3 2 7
× × × × ×
\(p^4\) \(p^3\) \(p^2\) \(p^1\) \(p^0\)
\(1*923521+2*29791+3*961+2*31+7*1=986055\)
\(986055 ≡ 27 (mod 127)\)
子字串
用前綴和的方式
再除(or乘)上一個\(p\)的冪次使得每個子字串第一個字元乘的數是固定的
子字串
用前綴和的方式
再除(or乘)上一個\(p\)的冪次使得每個子字串第一個字元乘的數是固定的
舉例: 問[1,3]和[0,2]是否一樣,[1,3]是誰?
子字串
用前綴和的方式
再除(or乘)上一個\(p\)的冪次使得每個子字串第一個字元乘的數是固定的
舉例: 問[1,3]和[0,2]是否一樣,[1,3]是誰?
前綴和
子字串
用前綴和的方式
再除(or乘)上一個\(p\)的冪次使得每個子字串第一個字元乘的數是固定的
舉例: 問[1,3]和[0,2]是否一樣,[1,3]是誰?
前綴和
子字串
用前綴和的方式
再除(or乘)上一個\(p\)的冪次使得每個子字串第一個字元乘的數是固定的
舉例: 問[1,3]和[0,2]是否一樣,[1,3]是誰?
前綴和
除冪次
子字串
用前綴和的方式
再除(or乘)上一個\(p\)的冪次使得每個子字串第一個字元乘的數是固定的
舉例: 問[1,3]和[0,2]是否一樣,[1,3]是誰?
前綴和
除冪次
發生碰撞怎麼辦
碰撞的機率可以用生日悖論概算
會發現\(M\)(下面公式的\(N\))越大(值域越大),越難碰撞
所以就把\(M\)調大一點
還是碰撞呢?
\(n = 0.83\sqrt{M}\)時,便約50%機率會碰撞了
還是碰撞呢?
一個便當不夠,就吃兩個
一個hash不夠,就用兩個
用兩組不同的\(p和M\),hash值用pair表示
$$值域從M變成M_1 M_2$$
參考code
//判斷兩個子字串是否一樣
#include <bits/stdc++.h>
using namespace std;
const int M=8e7+23, P=31;
long long pp[1010000];
int n, pre[1010000], q;
string s;
int f(int l, int r){
int res = pre[r];
if(l-1>=0) res-=pre[l-1];
res=(res+M)%M;
res=res*pp[n-l]%M; //把子字串第一個字元乘的p^(l-1)變成p^n
return res;
}
int main(){
pp[0]=1;
for(int i=1; i<1000010; i++) pp[i] = pp[i-1]*P%M;
cin >> n >> s;
for(int i=0; i<n; i++){
pre[i] = ( pre[i]+(s[i]-'a'+1)*pp[i] )%M;
pre[i+1]=pre[i];
}
cin >> q;
while(q--){
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
if(f(l1,r1)==f(l2,r2)) cout << "Same!\n";
else cout << "qq!\n";
}
return 0;}
習題
Trie
中文叫字典樹
就是把很多字串用樹的結構存起來
右圖就是把Rad, Rau, Rand, Raum, Rose, Java放在樹上
$$有長度為n(<5000)的字串t和k個總長小於 10^6 的字串 s_1,s_2,...,s_k$$
$$問有幾種組合方式用s裡面的字串組出t$$
直接DP看看
$$設dp[0]=1$$
$$看到t的第i個字元的時候,看過所有s,如果 s_j可以i-1這個位置開始接$$
則從\(i-1\)轉移到 \( i-1+|s_j| \)
時間複雜度\(O(n*(s的總長))\),會TLE
放到Trie上
枚舉i,從\(t_i\)開始在Trie上走路,發現這個節點是\(s\)中的字串就轉移
每個i最多跟著走到n,所以時間複雜度\(O(n^2)\)
dp | 1 | 0 | 0 | 0 | 0 | 0 |
---|
a
b
a
b
c
a
c
b
b
a
b
dp | 1 | 0 | 0 | 0 | 0 | 0 |
---|
a
b
a
b
c
a
c
b
b
a
b
dp | 1 | 0 | 1 | 0 | 0 | 0 |
---|
a
b
a
b
c
a
c
b
b
a
b
dp | 1 | 0 | 1 | 0 | 0 | 0 |
---|
a
b
a
b
c
a
c
b
b
a
b
dp | 1 | 0 | 1 | 0 | 1 | 0 |
---|
a
b
a
b
c
a
c
b
b
a
b
dp | 1 | 0 | 1 | 0 | 1 | 0 |
---|
a
b
a
b
c
a
c
b
b
a
b
沒辦法繼續在Trie上跑
dp | 1 | 0 | 1 | 0 | 1 | 0 |
---|
a
b
a
b
c
a
c
b
b
a
b
b沒辦法在Trie上跑...
dp | 1 | 0 | 1 | 0 | 1 | 0 |
---|
a
b
a
b
c
a
c
b
b
a
b
dp | 1 | 0 | 1 | 0 | 2 | 0 |
---|
a
b
a
b
c
a
c
b
b
a
b
dp | 1 | 0 | 1 | 0 | 2 | 0 |
---|
a
b
a
b
c
a
c
b
b
a
b
沒辦法繼續在Trie上跑
dp | 1 | 0 | 1 | 0 | 2 | 0 |
---|
a
b
a
b
c
a
c
b
b
a
b
b沒辦法在Trie上跑...
dp | 1 | 0 | 1 | 0 | 2 | 2 |
---|
a
b
a
b
c
a
c
b
b
a
b
dp | 1 | 0 | 1 | 0 | 2 | 2 |
---|
a
b
a
b
c
a
c
b
b
a
b
已經是最後一個位元了,不用繼續跑了
答案就是2
Trie也很適合做位元操作,尤其是xor
給你一個長\(n(<10^5)\)的陣列\(a\),你可以隨便選個子陣列並得到所有元素xor起來的值
請找到能得到的最大值
可以先做前輟xor
然後就從看子陣列變成看兩個值
$$怎麼求\max_ {0 \le i,j \le n} a_i \oplus a_j ,其中a_0=0 $$
把二進位數字放到Trie上面,高位的靠近根節點
依序把數字放上去
放上去前看一下與已經放上去的數字xor的最大值
如果最高位可以選不一樣,那就選不一樣
放001上去得到答案為5
參考code
//假設有n個字串要放上Trie, 0是根節點
int m=0, nxt[1010000][26], cnt[1010000]={0}; //cnt[i]紀錄有幾個字串在i這個節點結束
for(int i=0; i<1000010; i++) for(int j=0; j<26; j++)nxt[i][j]=-1;
for(int i=0; i<n; i++){
string s;
cin>>s;
int now=0;
for(char c:s){
c-='a';
if(nxt[now][c]==-1) nxt[now][c]=++m;
now=nxt[now][c];}
cnt[now]++;
}
習題
- 剛剛那兩題
- CF 1895D
- CF 37C
- CF 965E
- CF 655E
- luogu P7502
KMP
Knuth - Morris - Pratt Algorithm
有兩個字串t,s,問s在t中出現幾次
例:bb在bbabbb出現3次
這種問題叫做「字串匹配」,s叫模式字串,t叫主字串
暴力做很爛,\(O(|s||t|)\)
最大的困境在於下面這種case
t為一百萬個連續的a,s為十萬個連續的a接一個b
每次都配了十萬個a才發現這個b配不起來,又要重配
有沒有辦法可以重複利用已經配好的地方?
\(\pi\)函數
$$\pi[i]存的就是i前輟的「最長共同真前後輟」(次長共同前後輟)$$
這個函數又叫失配函數
也有人叫共同真前後綴border
\(\pi\)函數怎麼用
t為aabaabaaa,s為aabaaa
a | a | b | a | a | a | |
---|---|---|---|---|---|---|
π | 1 | 0 | 1 | 2 | 2 |
\(\pi\)函數怎麼用
a | a | b | a | a | a | |
---|---|---|---|---|---|---|
π | 1 | 0 | 1 | 2 | 2 |
aabaabaaa
aabaaa
配失敗了QQ
\(\pi\)函數怎麼用
a | a | b | a | a | a | |
---|---|---|---|---|---|---|
π | 1 | 0 | 1 | 2 | 2 |
aabaabaaa
aabaaa
\(\pi\)函數怎麼用
a | a | b | a | a | a | |
---|---|---|---|---|---|---|
π | 1 | 0 | 1 | 2 | 2 |
aabaabaaa
aabaaa
\(\pi\)函數怎麼用
a | a | b | a | a | a | |
---|---|---|---|---|---|---|
π | 1 | 0 | 1 | 2 | 2 |
aabaabaaa
aabaaa
換個位置繼續配(不用從頭),這個位置是由\(\pi\)決定的
\(\pi\)函數怎麼用
a | a | b | a | a | a | |
---|---|---|---|---|---|---|
π | 1 | 0 | 1 | 2 | 2 |
aabaabaaa
aabaaa
換個位置繼續配(不用從頭),這個位置是由\(\pi\)決定的
\(\pi\)函數怎麼求
暴力做比原本更爛
每個前綴\(|s|\)個要和自己的後綴(最多\(|s|\)個,最長長\(|s|\))比對
時間複雜度 \(O(|s|^3)\)
\(\pi\)函數怎麼求 - 觀察1
\( \pi[i+1] \leq \pi[i]+1 \)
簡易證明:
令\(\pi[i+1]=k\),則s[0 ~ k-1] = s[i+2-k ~ i+1]成立
顯然s[0 ~ k-2] = s[i+2-k ~ i]也成立
s[\(\pi\)[i]] = s[i+1]時,小於等於的等於才成立
\(\pi\)函數怎麼求 - 觀察2
那如果s[\(\pi\)[i]] != s[i+1] 呢?
"i+1前綴的最長共同真前後綴" 無法由 "i前綴的最長共同真前後綴" 推得
\(\pi\)函數怎麼求 - 觀察2
那如果s[\(\pi\)[i]] != s[i+1] 呢?
"i+1前綴的最長共同真前後綴" 無法由 "i前綴的最長共同真前後綴" 推得
ABCABB...ABCABC
\(\pi\)函數怎麼求 - 觀察2
那如果s[\(\pi\)[i]] != s[i+1] 呢?
"i+1前綴的最長共同真前後綴" 無法由 "i前綴的最長共同真前後綴" 推得
試圖由次長的推得
ABCABB...ABCABC
\(\pi\)函數怎麼求 - 觀察2
那如果s[\(\pi\)[i]] != s[i+1] 呢?
"i+1前綴的最長共同真前後綴" 無法由 "i前綴的最長共同真前後綴" 推得
試圖由次長的推得
ABCABB...ABCABC
次長的共同真前後綴
其實就是"最長共同真前後綴"的
最長共同真前後綴
\(\pi\)函數怎麼求 - 觀察2
那如果s[\(\pi\)[i]] != s[i+1] 呢?
"i+1前綴的最長共同真前後綴" 無法由 "i前綴的最長共同真前後綴" 推得
試圖由次長的推得
ABCABB...ABCABC
\(\pi\)函數怎麼求 - 觀察2
那如果s[\(\pi\)[i]] != s[i+1] 呢?
"i+1前綴的最長共同真前後綴" 無法由 "i前綴的最長共同真前後綴" 推得
試圖由次長的推得
ABCABB...ABCABC
\(\pi\)函數怎麼求 - 觀察2
那如果s[\(\pi\)[i]] != s[i+1] 呢?
"i+1前綴的最長共同真前後綴" 無法由 "i前綴的最長共同真前後綴" 推得
試圖由次長的推得
$$會發現次長的長度就是\pi[\pi[i]]$$
\(\pi\)函數怎麼求 - 觀察2
那如果s[\(\pi[i]\)] != s[i+1] 呢?
"i+1前綴的最長共同真前後綴" 無法由 "i前綴的最長共同真前後綴" 推得
試圖由次長的推得
$$會發現次長的長度就是\pi[\pi[i]-1]$$
次長還是配不到呢?
\(\pi\)函數怎麼求 - 觀察2
$$那如果s[\pi[i]] != s[i+1] 呢?$$
"i+1前綴的最長共同真前後綴" 無法由 "i前綴的最長共同真前後綴" 推得
試圖由次長的推得
$$會發現次長的長度就是\pi[\pi[i]-1]$$
次長還是配不到呢?
$$就再從第三長的配,也就是看s[\pi[\pi[\pi[i]-1]-1]]是不是等於s[i+1]$$
完整演算法
$$初始化\pi[0]=0$$
假設已經做完\(\pi[0\)~\(i]\)了
先令\(j=\pi[i]\)
$$看s[j]是否等於s[i+1],是就讓\pi[i+1]=j+1,否則讓j=\pi[j-1]$$
$$如果j=0還配失敗,那\pi[i+1]=0$$
時間複雜度
$$\pi最多增加|s|次(觀察一)$$
$$因為減少的一定\leq增加的(勢能),所以最多減少|s|次$$
$$時間複雜度O(|s|)$$
$$如果是s匹配t那\pi最多增加|t|次$$
$$時間複雜度O(|t|)$$
參考code
//CSES String Matching
#include <bits/stdc++.h>
using namespace std;
int n, m, pi[2100000];
string t, s;
int main(){
ios_base::sync_with_stdio(false); cin.tie(0);
cin >> t >> s;
n=t.size(), m=s.size();
pi[0]=0; int now=0; //匹配成功now個,要嘗試配s[now]和s[i]
for(int i=1; i<m; i++){
while(now>0 and s[i]!=s[now]) now=pi[now-1];
if(s[i]==s[now]) now++;
pi[i]=now;
}
int ans=0; now=0;
for(int i=0; i<n; i++){
while(now>0 and t[i]!=s[now]) now=pi[now-1];
if(t[i]==s[now]) now++;
if(now==m){
ans++;
now=pi[now-1];
}
}
cout << ans << '\n';
}
應用
$$如果一個前綴重複數次後,前|s|個字元組起來=s,則此前綴的長度是s的週期$$
$$該如何找到s的所有週期呢?$$
hash是一個辦法
但其實有非隨機性的算法
$$周期的另一個定義 : 若s[i]=s[i+p]對所有i\in[0,n-p-1]成立則p為s的週期$$
$$共同前後綴的定義 : 若s[i]=s[n-l+i]對所有i\in[0,l-1]成立則l前綴為s的共同前後綴$$
$$周期的另一個定義 : 若s[i]=s[i+p]對所有i\in[0,n-p-1]成立則p為s的週期$$
$$共同前後綴的定義 : 若s[i]=s[n-l+i]對所有i\in[0,l-1]成立則l前綴為s的共同前後綴$$
$$會發現每個共同求後綴(長l)都對應到字串的一個週期p=n-l$$
$$所以s的所有週期就是 n-\pi[n-1], n-\pi[\pi[n-1]-1]...$$
KMP自動機
也較做前綴自動機(Prefix Automaton)
有一個字串s,有q次詢問
$$每次詢問給你一個字串t,問s+t最後|t|個前綴的"最大共同前後綴"$$
- \(|s|\leq10^6\)
- \(q\leq10^5\)
- \(|t|\leq10\)
每次詢問都做一次KMP很燒雞
現在沒得均攤了(可能會+1然後-8)
$$可以開一個|s|\times26的二維陣列pa,pa[i][c]代表已匹配到長度i時,再匹配一個c時會匹到的長度$$
$$pa[i][c]可以從前面的pa得到$$
$$如果c=s[i],則pa[i][c]=i+1 (匹配成功繼續配下去)$$
$$否則pa[i][c]=pa[\pi[i-1]][c](匹配失敗)$$
$$\pi[i]也很好求了,\pi[i]就是pa[\pi[i-1]][s[i]]$$
abab
a | b | a | b | ||||||
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | ||||||
pi[i] | 0 | ||||||||
pa[i]['a'] | 1 | ||||||||
pa[i]['b'] | 0 | ||||||||
pa[i]['c'] | 0 |
s
abab
a | b | a | b | ||||||
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | ||||||
pi[i] | 0 | 0 | |||||||
pa[i]['a'] | 1 | ||||||||
pa[i]['b'] | 0 | ||||||||
pa[i]['c'] | 0 |
s
abab
a | b | a | b | ||||||
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | ||||||
pi[i] | 0 | 0 | |||||||
pa[i]['a'] | 1 | 1 | |||||||
pa[i]['b'] | 0 | 2 | |||||||
pa[i]['c'] | 0 | 0 |
s
abab
a | b | a | b | ||||||
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | ||||||
pi[i] | 0 | 0 | 1 | ||||||
pa[i]['a'] | 1 | 1 | |||||||
pa[i]['b'] | 0 | 2 | |||||||
pa[i]['c'] | 0 | 0 |
s
abab
a | b | a | b | ||||||
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | ||||||
pi[i] | 0 | 0 | 1 | ||||||
pa[i]['a'] | 1 | 1 | 3 | ||||||
pa[i]['b'] | 0 | 2 | 0 | ||||||
pa[i]['c'] | 0 | 0 | 0 |
s
abab
a | b | a | b | ||||||
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | ||||||
pi[i] | 0 | 0 | 1 | 2 | |||||
pa[i]['a'] | 1 | 1 | 3 | ||||||
pa[i]['b'] | 0 | 2 | 0 | ||||||
pa[i]['c'] | 0 | 0 | 0 |
s
abab
a | b | a | b | ||||||
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | ||||||
pi[i] | 0 | 0 | 1 | 2 | |||||
pa[i]['a'] | 1 | 1 | 3 | 1 | |||||
pa[i]['b'] | 0 | 2 | 0 | 4 | |||||
pa[i]['c'] | 0 | 0 | 0 | 0 |
s
abab
a | b | a | b | a | |||||
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | |||||
pi[i] | 0 | 0 | 1 | 2 | |||||
pa[i]['a'] | 1 | 1 | 3 | 1 | |||||
pa[i]['b'] | 0 | 2 | 0 | 4 | |||||
pa[i]['c'] | 0 | 0 | 0 | 0 |
s
a
t
abab
a | b | a | b | a | |||||
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | |||||
pi[i] | 0 | 0 | 1 | 2 | 3 | ||||
pa[i]['a'] | 1 | 1 | 3 | 1 | |||||
pa[i]['b'] | 0 | 2 | 0 | 4 | |||||
pa[i]['c'] | 0 | 0 | 0 | 0 |
s
a
t
可以用pa算pi
abab
a | b | a | b | a |
---|