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]++;
 }

習題

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