# 110 選手班 - 字串 ###### tags: `宜中資訊` `CP` Ccucumber12 2021.08.24 --- ## Outline - Trie - KMP - Z-algorithm - Rolling hash --- ## Trie ---- ### Problem - 給定 $N$ 個字串 - 請支援以下詢問 $Q$ 次 - 給定字串 $S$,請問 $N$ 個字串中出現幾個 $S$ - $N,Q \leq 10^5,|S| \leq L = 100$ ---- ### Naive - 儲存所有字串 - 每次詢問就遍歷一次 - Time: $\mathcal{O}(NQL)$ ---- ### Binary Search - 儲存所有字串並排序 - 對於每個詢問二分搜 - Time: $\mathcal{O}(NL\log N + QL\log N)$ ---- Better? - $\mathcal{O}(NL + QL)$? ---- ### Trie - 命名:Retrieval - 發音: - /tri/: 發明者(tree) - /trai/: 其他人(try) - 字典樹,字首樹 ---- ![](https://i.imgur.com/ZJHnAVo.png) ---- ### Key - 英文字串字母數量有限 (26個) - 將共同前綴合併 - 節省空間 - 加速尋找 ---- - 每個節點(分岔)代表一個字母 - 紀錄每個字串的結尾位置 - 每個從根結點出發的路徑代表一個字串 ---- ### node ```c= struct node { int count ; node *nxt[26] ; }; ``` ---- ```c= struct node { int cnt ; node *nxt[26] ; node () { cnt = 0 ; for(int i=0; i<26; ++i) nxt[i] = nullptr ; } } ; node *root = new node() ; void modify(string s, int val) { node *tmp = root ; for(auto i:s) { i -= 'a' ; if(tmp -> nxt[i] == nullptr) tmp -> nxt[i] = new node() ; tmp = tmp -> nxt[i] ; } tmp -> cnt += val ; } int count(string s) { node *tmp = root ; for(auto i:s) { i -= 'a' ; if(tmp -> nxt[i] == nullptr) return 0 ; tmp = tmp -> nxt[i] ; } return tmp -> cnt ; } ``` ---- ### Time Complexity - Insert: $\mathcal{O}(L)$ - Query: $\mathcal{O}(L)$ - Total: $\mathcal{O}(NL + QL)$ ---- ### Sorting - 依照字母順序 DFS ---- ### Binary - 處理二進位字串 - 二元樹 - 二分搜 --- ## KMP ---- ### Problem - 給定字串 $S$ 與字串 $T$ - 請問 $S$ 在 $T$ 中出現幾次? - $|S| \leq |T| \leq 5\times 10^5$ ---- - 在 **文本(Text)** 中尋找欲求的的 **段落(Pattern)** ---- ### Naive - 枚舉每個 $T_i$ 當作開頭 - 往右檢查是否與 $S$ 相同 - Time: $\mathcal{O}(TS)$ ---- ### KMP - Knuth–Morris–Pratt algorithm - 三個人共同發表 - Morris-Pratt Algorithm (MP) 優化而成 ---- - 減少比對次數 - 如果比對完 $S[0:9]$ 發現第九格不對 - 前面 $S[0:8]$ 有沒有辦法提供甚麼資訊? ---- ### Failure Function - $F[i] :=$ 當成功配對 $s[0, i-1]$ 直到 $s[i]$ 失敗時,將 $F[s[i]]$ 對齊原本 $s[i]$ 的位置 ---- | a | b | a | b | b | a | a | b | a | b | b | a | b | a | a | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | -1 | -1 | 0 | 1 | -1 | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 1 | 2 | 0 | ---- - $F[i] = k$ - $k$ 為滿足 $s[0:k]=s[i-k:i]$ 的最大值 - 若不存在,則 $k = -1$ - 對於每個前綴,尋找其後綴與整個字串最大相同長度 ---- #### Naive - 根據定義,對於每個位置枚舉所有 $k$ 檢查 - 枚舉 $k=[0,i]$ - Time: $\mathcal{O}(|S|^3)$ ---- #### Optimization 1 - 相鄰的 failure function 至多相差 $1$ - 若 $s[i+1]=s[F[i]+1]$,則$F[i+1]=F[i]+1$ - 若不相同,則枚舉 $k=[0,F[i]]$ - Time: $\mathcal{O}(|S|^2)$ ---- #### Optimization 2 - 當 $s[i+1] \neq s[F[i]+1]$ 時 - 希望找到次長 $k'$ 使得 $s[0:k']=s[i-k':i]$ - 若 $s[i+1] = s[k'+1]$,則 $F[i+1]=k'+1$ - 若不同,則找第三長 $k''\dots$ ---- #### Time Complexity - 增長:$O(1)$ - 減少:$O(1)$ - 每格最多增長 $1$,最多減少 $|S|$ 次 - Total:$\mathcal{O}(|S|)$ ---- ```c= void build(string s) { f[0] = -1 ; for(int i=1; i<s.size(); ++i) { f[i] = f[i-1] ; while(f[i] > -1 && s[i] != s[f[i] + 1]) f[i] = f[f[i]] ; f[i] += (s[i] == s[f[i] + 1]) ; } } ``` ---- ### KMP - 拿 $T$ 跟 $S$ 和 Failure Function 比較 - 若成功配對則往前一格 - 若配對師被則用 $F[i]$ 跳轉 - 當成功長度達 $|S|$ 即找到一個出現在 $T$ 中的 $S$ - 本質上跟尋找 Failure Function 一樣 ---- ### Time Complexity - Failure Function:$\mathcal{O}(|S|)$ - 尋找配對:$\mathcal{O}(|T|)$ - Total:$\mathcal{O}(|S|+|T|)$ ---- ```c= int kmp(string s, string t) { int k = -1, ret = 0 ; for(int i=0; i<t.size(); ++i) { while(k > -1 && t[i] != s[k + 1]) k = f[k] ; k += (t[i] == s[k + 1]) ; if(k == s.size() - 1) k = f[k], ++ret ; } return ret ; } ``` --- ## Z-algorithm ---- ### Problem - 給定字串 $S$ 與字串 $T$ - 請問 $S$ 在 $T$ 中出現幾次? - $|S| \leq |T| \leq 5\times 10^5$ ---- - Z-Value - Z 函數 / Z 演算法 - 擴展 KMP (中國) ---- ### Z Value - $z[i] = \max(p: s[0:p) = s[i:i+p))$ - 對於每個後綴,尋找其前綴與整個字串最大相同長度 ---- | a | b | a | b | b | a | a | b | a | b | b | a | b | a | a | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 0 | 0 | 2 | 0 | 0 | 1 | 6 | 0 | 2 | 0 | 0 | 3 | 0 | 1 | 1 | ---- WHY - 將段落(pattern) $S$ 與文本(text) $T$ 連接在一起 - 中間加上特殊字元 (`@`) - `S @ T` ---- - $a = S + @ + T$ - $z[i] \leq |S|$ (有 `@` 卡住) - 當 $z[i] = |S| = k \implies a[0:k)=a[i:i+k)$ - $\implies S = a[i:i+k)$ - $\implies$ $S$ 在 $i$ 這個位置出現 ---- - 維護區間 $[L,R]$,代表從 $L$ 之後與整個字串的最長相同前綴 - 若完全不相等,則 $L=R$ ---- - 對於步驟 $i$ - $i > R$:重置 $L,R = i$,計算最長的 $R$ - $i \leq R$:令 $k = i-L$,$z[i] \geq \min(z[k], R-i+1)$ - $z[k] < R-i+1$:無法再延長了,$z[i]=z[k]$ - $z[k] \geq R-i+1$:有可能延長,設 $L=i$,將 $R$ 往後繼續比對,$z[i]=R-L+1$ ---- 模擬動畫 http://www.utdallas.edu/~besp/demo/John2010/z-algorithm.htm ---- ### Time Complexity - 雙指標:$\mathcal{O}(|S| + |T|)$ ---- ```c= vector<int> zAlgo(string s) { int l = 0, r = 0; vector<int> z(s.size()) ; for(int i = 1, n = s.size(); i < n; ++i) { if(i > r) { l = r = i ; while(r < n && s[r - l] == s[r]) r++ ; z[i] = r - l ; r-- ; } else { int k = i - l ; if(z[k] < r - i + 1) z[i] = z[k] ; else { l = i ; while(r < n && s[r - l] == s[r]) r++ ; z[i] = r - l ; r-- ; } } } return z ; } ``` --- ## Rolling Hash ---- ### Problem - 字串比對 - Time: $\mathcal{O}(|S|)$ - 把字串 hash 成一個整數以達到 $\mathcal{O}(1)$ 比對 ---- ### Hash - 雜湊 / 哈希(中國) - hash值不同 $\implies$ 原始值必定不同 - hash值相同 $\implies$ 原始值不一定相同 - 碰撞:兩個不同的原始值 hash 成相同的值 - 預期碰撞機率 $\frac{n}{C}$ ---- ### Rolling Hash - 多項式 hash 法 - $f(s) = \sum\limits_{i=1}^{l} s[i] \times b^{l-i} \pmod M$ - ex. $s="xyz",f(s)=xb^2+yb+z$ ---- - $M$:大質數 - $b$:$[1,M)$ 間任意正整數 - $f(s)$ 與 $f(t)$ 碰撞機率:$\frac{l-1}{M}$ ---- #### Time Complexity - 預處理:$\mathcal{O}(|S|)$ - 比較:$\mathcal{O}(1)$ ---- ```c= const int N = 500010 ; const int M = 1e9 + 7 ; const int b = 313 ; long long hs[N]; void init(string s) { int n = s.size(); bp[0] = 1 ; for(int i=1; i<=n; ++i) { hs[i] = (hs[i-1] * b + s[i-1]) % M ; } } ``` ---- ### 取得子字串 - 想知道 $s[l,r]$ 的 hash 值 - $f(s[l,r]) = f(s_r) - f(s_{l-1})\times b^{r-l+1}$ - 預處理 $b^i$ ---- ```c= const int N = 500010 ; const int M = 1e9 + 7 ; const int b = 313 ; long long hs[N], bp[N] ; void init(string s) { int n = s.size(); bp[0] = 1 ; for(int i=1; i<=n; ++i) { hs[i] = (hs[i-1] * b + s[i-1]) % M ; bp[i] = bp[i-1] * b % M ; } } long long query(int l, int r) { return (hs[r+1] - hs[l] * bp[r-l+1] % M + M) % M ; } ``` ---- KMP or Z-Algorithm? - $S = T[i, i+|S|)$ - 預處理:$\mathcal{O}(|S| + |T|)$ - 尋找比對:$\mathcal{O}(|T|)$ ---- ### Other - 增刪字元:$\mathcal{O}(1)$ - 拼接字串:$\mathcal{O}(1)$ ---- ### 最長回文 - 枚舉中間點 - 二分搜長度 - $\mathcal{O}(N\log N)$ ---- Best $M$ and $b$? https://codeforces.com/blog/entry/60445 --- ## Advance - AC Automaton (KMP + Trie) - Suffix Array - Manacher - Suffix Automaton --- ## Credit - wikipedia - 資訊之芽 - OI Wiki - https://www.evanlin.com/about-kmp/ - https://wangwilly.github.io/willywangkaa/2018/03/19/Algorithm-Z-%E6%BC%94%E7%AE%97%E6%B3%95/ - http://sunmoon-template.blogspot.com/2015/10/rabin-karp-rolling-hash-rabin-karp-hash.html - http://pisces.ck.tp.edu.tw/~peng/index.php?action=showfile&file=f743c2923f8170798f62a585257fdd8436cd73b6d
{"metaMigratedAt":"2023-06-16T08:12:51.954Z","metaMigratedFrom":"Content","title":"110 選手班 - 字串","breaks":true,"contributors":"[{\"id\":\"45262441-89e4-46ca-959b-c0d6fadb64e2\",\"add\":7821,\"del\":223}]"}
    139 views