# 字串 Author: guagua0407 --- - Hashing - KMP - Z - Trie --- ## Hashing ---- 比對字串是 $O(n)$ 太慢了! ---- ### 定義 維基百科:**雜湊函數**(Hash function),是一種從任何一種資料中建立小的數字「指紋」的方法。雜湊函數把訊息或資料壓縮成摘要,使得資料量變小,將資料的格式固定下來。該函數將資料打亂混合,重新建立一個叫做雜湊值。 ---- ![image.png](https://hackmd.io/_uploads/rJGS0IUQT.png) ---- ### 性質 - 不可逆 - $f(A) \neq f(B) \implies A \neq B$ - 有可能會**碰撞** ---- ### 碰撞 - $(A \neq B) \land (f(A) = f(B))$ - 燒雞 - 目標是把碰撞機率降低 ---- ### Rolling Hash $\sum_{i=0}^{n-1} (S[i] * x^{n-i-1}) \pmod{M}$ $x,M$ 要自己決定好 ---- ### 例子 假設 a-z 分別對應 1-26 則 banana 的 Hash Value 是 $2x^5+x^4+14x^3+x^2+14x^1+1 \pmod{M}$ ---- 這個公式有一個最重要的性質: $f(A+B)=f(A)x^{|B|}+f(B)$ ---- 移向後變成 $f(B)=f(A+B)-f(A)x^{|B|}$ ---- 可以發現我們只要把所有前綴的 Hash Value 都記起來 就可以 O(1) 查詢任意子字串的 Hash Value 了! ---- 可以把 Rolling Hash 想像成一個三角形 ![image.png](https://hackmd.io/_uploads/SywYTw87p.png) ---- 兩個子字串合併之後就會變成這樣 ![image.png](https://hackmd.io/_uploads/rJ290PUQa.png) ---- $x$ 和 $M$ 的值要取多少? 可以發現 $x$ 只要大於你要 hash 的字元的種類就好,所以假設你是要 hash 英文字母可以取個 29 之類的,不過通常其實隨機就好了。 $M$ 就是選一個大小合適的質數$(1e9+7,2^{61}-1)$,~~或是你直接讓他 overflow~~ ---- 重點來了,$M$ 的大小代表你 Hash 完之後總共會有幾種值,也就直接影響了碰撞的機率。 那機率怎麼算呢? ---- ### [Birthday Paradox](https://en.wikipedia.org/wiki/Birthday_problem) $N$ 個人中有兩人生日同天機率是多少? ---- 把生日換成 Hash Value 可以發現大概需要 $O(n^2)$ 級別的 $M$ 才能保證兩兩不相同。 所以看到 $10^5$ 級別以上的 $N$ 就得開砸 int_128 或做超多個 hash? ---- 現在給你這個問題:在長度為 $10^6$ 的 $T$ 裡找有沒有子字串等於長度為 $10^5$ 的 $S$ 會發現在 $10^6$ 級別的 hash 中不需要滿足兩兩不同,他們只是要和 $S$ 比較而已。 ---- 所以其實重要的是實際上會用到的比較次數,在上面那題中比較次數只為 $10^6$ 級別,所以 $10^9$ 級別的 $M$ 就足夠了。 所以不要一興奮就開砸了,太複雜的 hash 是會讓你常數破台的 ---- ### 用途 包括但不限於: - $O(n)$ 找 $A$ 中有沒有 $B$ - $O(n^2logn)$ 找有幾種子字串 - $O(nlogn)$ 找最長回文子字串 - $O(nlogn)$ 找字典序最小字串橫移 ---- ![image.png](https://hackmd.io/_uploads/Sk84VfPQT.png =400x) ---- ### 習題 [CSES Finding Periods](https://cses.fi/problemset/task/1733/) [USACO Bovine Genetics](http://www.usaco.org/index.php?page=viewproblem2&cpid=741) [COCI Sateliti](https://evaluator.hsin.hr/tasks/HONI202135sateliti/) [BOI Genetics](https://oj.uz/problem/view/BOI18_genetics) --- ## KMP ---- ### 定義 $\pi[i]= S[0...i]$ 中最長的前綴滿足他同時也是後綴 當然, $\pi[i]$ 要小於 $i+1$ ---- ### 例子 | $S[i]$ | a | a | b | a | a | a | b | | --- | --- | --- | --- | --- | --- | --- | --- | | $\pi[i]$ | 0 | 1 | 0 | 1 | 2 | 2 | 3 | ---- 暴力? $O(n^2)$ 還能更好! ---- ### 觀察一 $\pi[i+1]<=\pi[i]+1$ ---- 最好情況: $(S[ \pi[i] ]=S[i+1]) \implies (\pi[i+1]=\pi[i]+1)$ ---- ![image.png](https://hackmd.io/_uploads/S1fDxQPm6.png=800x) ---- ### 觀察二 如果 $S[\pi[i]] \neq S[i+1]$ ? 那現在就要試一個最大的 $j<\pi[i]$ ,試得 $j$ 滿足 $S[0...j-1]$ 同時是 $S[0...i]$ 的前綴和後綴。 ---- ![image.png](https://hackmd.io/_uploads/BJv_WXDQa.png=800x) ---- 找到 $j$ 之後,一樣看 $S[j]$ 有沒有等於 $S[i+1]$,有的話你就找到答案了,否則就在往下找一個最大滿足條件的 $k$。 一直重複上面的步驟到找到為止就好了。 ---- ![image.png](https://hackmd.io/_uploads/BJr8-nwXp.png=700x) ---- 可以發現 $k=\pi[j-1]$! ---- ### 總結 - 先算出 $\pi[0...i-1]$ - 令 $j=\pi[i-1]+1$ - 如果 $S[j]=S[i]$ 則 $\pi[i]=j$ ,否則讓 $j=\pi[j-1]$ - 如果 $j$ 變成 $0$ 了那 $\pi[i]=0$ ---- 時間複雜度呢? 看起來貌似是 $O(n^2)$,但因為每移動一格 $\pi$ 值最多只會多一,所以其實只有 $O(n)$ ---- ```cpp= vector<int> pi(const string &s) { int n = (int)s.size(); vector<int> pi_s(n); for (int i = 1, j = 0; i < n; i++) { while (j > 0 && s[j] != s[i]) { j = pi_s[j - 1]; } if (s[i] == s[j]) { j++; } pi_s[i] = j; } return pi_s; } ``` ---- ### 習題 [POI Template](https://szkopul.edu.pl/problemset/problem/PT4yHRX9Mmz85ndhNPGCi_WB/site/?key=statement) --- ## Z ---- ### 定義 $z[i]= S$ 中以 $i$ 為左界最長的子字串長度使得這個子字串也是 $S$ 的前綴 ---- ### 例子 | $S[i]$ | a | a | b | a | a | a | b | | --- | --- | --- | --- | --- | --- | --- | --- | | $z[i]$ | 7 | 1 | 0 | 2 | 3 | 1 | 0 | ---- 暴力?$O(n^2)$ 還能更好! ---- 我們可以維護一個 range $(x,y)$ 代表目前所有 $S[x...y]=S[0...y-x]$ 中 $y$ 最大的 ---- ![image.png](https://hackmd.io/_uploads/H1EZ9Jdm6.png) ---- 要算第 $i$ 格時,我們可以先看 $z[i-x]$ 如果 $i+z[i-x]<y$ 那 $z[i]=z[i-x]$ ---- 如果 $i+z[i-x]>=y$ 就代表說 $S[0...y-i]=S[i...y]$ 這時候就因為已經用不了前面算過的資訊了,所以就要從 $y+1$ 開始一個一個比對到不一樣為止,同時更新 $y$。 ---- 時間複雜度呢? 因為每次比對 $y$ 都會增加 $1$,所以最多只會有 $n$ 次比對,$O(n)$! ---- ```cpp= vector<int> z(n,0); int x=0,y=0; for(int i=0;i<n;i++){ z[i]=max(0,min(z[i-x],y-i+1)); while(i+z[i]<n and str[z[i]]==str[i+z[i]]){ x=i; y=i+z[i]; z[i]++; } } ``` ---- ### 習題 [CF Vasya and Big Integers](https://codeforces.com/contest/1051/problem/E) [CF Prefixes and Suffixes](https://codeforces.com/problemset/problem/432/D) --- ## Trie ---- ![image.png](https://hackmd.io/_uploads/rJ77zed7p.png) ---- 每個字串都被當成一條鍊來存在樹上 ---- ### 可以幹嘛 - O(n) 插入,移除字串 - O(n) 查找字串 - O(n) 詢問有幾個前綴為 $S$ 的字串 ---- 插入 ```cpp= void insert(string str){ int v=0,i=0; while(i<str.length()){ if(trie[v][str[i]-'a']==-1){ trie[v][str[i]-'a']=cur; v=cur; c[cur]=str[i]; cur++; } else{ v=trie[v][str[i]-'a']; } i++; } cnt[v]++; } ``` ---- 詢問 ```cpp= int query(string str){ int v=0,i=0; while(i<str.length()){ v=trie[v][str[i]-'a']; i++; } return cnt[v]; } ``` ---- ### 01trie 可以把數表示成一個 01 字串(二進位) 把這些字串丟到 trie 裡就是一個 01trie ---- 可以幹嘛? 假設數字的值域是 $X$ 那剛剛那些操作的時間複雜度就會都變成 $O(logX)$ ---- ## [例題](https://judge.yosupo.jp/problem/set_xor_min) 題目:現在有一堆數,有三個操作 - 加入一個數 $x$ - 移除一個數 $x$ - 給一個數 $x$ ,求這些數中跟 $x$ xor 完的最小值可以是多少 ---- 因為 trie 有查詢前綴的特性,所以你可以從最大的 bit 枚舉到最小的 bit。 設 $x$ 的第 $k$ 個 bit 為 a ,那就看說如果在目前的前綴中的第 k 個 bit 設成 ${(a \oplus 1)}$ 變成新的前綴,包含這個新的前綴的個數是不是為 0。 ---- 不是的話就可以成功把第 k 個 bit 設成 ${(a \oplus 1)}$。 否則就只能設成 ${a}$,並且 $ans$ 要加上 $2^k$ ---- ### 習題 [JOIOC Selling RNA Strands](https://oj.uz/problem/view/JOI16_selling_rna) [AC XOR Game](https://atcoder.jp/contests/arc122/tasks/arc122_d) --- END
{"title":"字串","description":"@","contributors":"[{\"id\":\"f6c1f4c6-eaa6-44aa-a181-7ce7c40dc9d5\",\"add\":6333,\"del\":530}]"}
    586 views