字串處理と資料結構 II
joe59491 、 samsam2310 2015 年 11 ⽉ 13 ⽇(⾦曜⽇)
1 字串
字串是指⼀堆字元串成⼀串的東東,在資訊領域可說是⾮常實⽤、⾮常重要的⼀個部份。
其中常⾒的問題就是字串匹配,假設有兩字串 A、B,無論是要確定A是否等於 B,或是 A包含 B,或是A 裡⾯出現幾次B 等等,不管是搜尋、分析、還是和諧掉某些不該出現 的天安⾨事件和擋掉不該出現的網站等等,字串演算法和這些事情的效率密切相關,所以 可以從DP中獨⽴出來講。
字串的題⽬都很活,善⽤各個演算法的性質,才能夠找出答案。
1.1 天真匹配
觀察⼀個字串A裡⾯是否有字串 B,我們第⼀個想到的是O(|A|*|B|)的天真做法,也就是
⽤兩層For迴圈去檢查字串是否有匹配到,但這顯然不是個好作法。
1.2 Knuth-Morris-Pratt Algorithm | KMP
概念其實就是,做天真法的時候,匹配失敗時,就可以切掉了,不⽤跑完第⼆層迴圈。
同時,我們把B 往前挪動繼續匹配時,也不⼀定只能移動⼀格,⽽是根據已匹配的部分 (那部分AB相同)做判斷,⼀次移動多⼀點。
不難觀察出,這個優化跟 B的結構有關,當B的某個後綴同時是 B的前綴時,我們就可 以直接挪動B到這個位置。
我們先定義⼀個東西「相同前綴後綴」,如果S是B的相同前綴後綴,代表這個字串S是 B的前綴,同時也是B的後綴。⽽最⻑相同前綴後綴就是⾃⼰,最短的是空字串。
我們可以⽤ DP快速求出B 每個前綴的相同前綴後綴⻑度,當我們要移動B時,我們就 可以透過這個值快速移動B。
I
1.3 Z 算法
透過DP的⽅式處理。⾸先定義⼀個Z(x)函數,代表S[0]到 S[N]和S[x]到S[N]這兩段 字串可以匹配多⻑,也就是某個後綴和S的前綴的最⻑匹配是多⻑。
這個函數的算法可以⽤DP 求出,詳細可以查演算法筆記 String Matching Z算法,或是 專⼼看⽩板。
⾄於怎麼⽤它來匹配字串呢?我們可以把A和B接起來,中間⽤⼀個不在 A也不在B裡 出先的字元隔開,然後做⼀次Z 函數,我們發現,對於所有的Z(x),每出現⼀個 Z(x)等 於A的⻑度,A就在B裡出現了⼀次。
1.4 Hash
Hash對字串匹配來說,真是個輕巧⼜⽅便的做法。在字串中Hash 的應⽤是個重要的概 念。許多問題雖然可以⽤正規的⽅法解決,但是⽤Hash⽅式可能更直覺,⽽且只要問題 不要太過於精巧,Hash硬座的複雜度也不輸給正常的⽅法。
⾄於如何Hash,有個簡單的⽅法,就是把⼀個包含26種不同字⺟的字串 (如果有⼤⼩寫
就是52,不過因為字串⻑度不⼀樣,要補空字元,所以會多⼀種字元)當成⼀個 27進位 數,對於⼀個字串,我們可以將他轉成⼀個⼗進位數字儲存。P進位數的話,第⼀位乘以 P0,第⼆位乘以P1,第三位乘以P2...依此類推,然後加起來。但是這樣很顯然⼀下就超
過long long 範圍了,所以我們可以 MOD⼀個⼤質數,這樣就是⼀種 Hash了。這樣匹
配兩個字串是否相同,只要看Hash值是否相同,複雜度就從O(N)變成O(1)了,原本不 可⾏的⽅式,都可⾏了!
話說有時候剛好會碰撞導致兩個不同的字串被當作⼀樣,遇到這種狀況時,可以⽤兩個或 多個⼤質數同時做Hash,會⼤⼤降低誤判的機率。
1.5 Suffix Array
後綴數組,我們可以把字串拆成⼀堆後綴字串,然後排序,這樣就可以對他們做⼀些像是
⼆分搜這樣的骯髒的⼿段。
但是真的拆成⼀堆後綴空間會變成|S|2,所以我們可以直接紀錄他在字串S中開始的位置 就好。⾄於排序就利⽤像是Counting Sort之類的⽅法就⾏了。
當然不只這樣,在排序的同時,我們可以利⽤類似倍增的算法來優化我們的排序步驟。
1.6 H 數組
在做Suffix Array時,我們可以順便紀錄⼀個H函數。
H函數代表相鄰的兩個後綴S[x-1]和S[x],他們的前綴有多⻑是⼀樣的。通常 H(0)是0,
II
這個東西就可以⽤來做些奇怪的事。
1.7 AC ⾃動機
所謂的⾃動機,就是你有很多狀態,然後你可以依照條件在狀態裡移動,最後停的位置就 是結果。
基本上 AC⾃動機是 KMP的強化版,讓你可以在⼀個⻑字串中⼀次找出很多短字串,有 點像是⼀次搜尋很多筆。
其中,KMP 匹配失敗時,我們會知道要從那裡繼續,⽽AC⾃動機就是告訴你你接下來 要去那些節點,進⽽⼀次搜尋完所有短字串。
1.8 Exercises!!
1. < 字串匹配>
給你字串A、B,問A是不是B的⼦字串。
2. < 不同⼦字串數>
給你字串A,問你A總共有多少種不同的⼦字串。
3. < 近似匹配>
給你字串A、B,問你B在A中的所有近似匹配的位置,近似匹配就是不⼀樣的地⽅
不超過K個。
4. <TIOJ 1735 K-⼝吃⼦字串>
⼀個 K⼝吃字串就是⻑度為K 的字串重複兩次。給你字串S和K,問你總共有多少
⼦字串A滿⾜A是K⼝吃字串。
5. < 最⻑回⽂⼦字串> 給你字串S,要你求出最⻑回⽂⼦字串。
2 進階資料結構
在解題⽬時,常常需要⼀些複雜的資料結構來解決問題。以下稍微介紹幾種資料結構。由 於篇幅的關係,就不附上程式碼,⼤家可以參考網路資源或是參加相關營隊,會對這些進 階資料結構更了解。
或者可以到「⽇⽉掛⻑的模板庫」看看各種資料結構的模板。
III
2.1 Trie
Trie,字典樹,也就是樹的每個節點代表⼀個字元或是狀態,然後底下有其他的字元可以 繼續往下⾛。有點像字典⼀樣,所以叫做字典樹。字典樹可以⽤來解決字串匹配等問題,
像是AC⾃動機等等,⼜或者是可以壓縮成字串的奇怪資料結構題。
這東西吃⾮常多記憶體QAQ。
2.2 樹套樹
當⼀個樹的節點,不是⼀個值⽽是⼀顆樹時,就是樹套樹。
通常在處理⼆維問題時,會需要對某個⼦矩形做查詢,就可以裡⽤⼆維的樹套樹來解決。
2.3 Binary Search Tree
⼆元搜尋樹是⼀個很經典的資料結構,他保證⼀個性質,對於每個節點,他的左⼩孩的值
⼀定⼩於⾃⼰,右⼩孩的值⼀定⼤於等於⾃⼰。有了這個性質,我們可以在 O(lgN)的時 間尋找⼀個值,並且可以O(lgN)插⼊,O(lgN)刪除⼀個值。
但是事情沒有著麼美好,假設我們按照順序插⼊數字,那⼆元搜尋樹將會退化成⼀條練,
操作的複雜度都會變成O(N)。為了防⽌這件事,很多⼈發明了各種⼆元搜尋樹,利⽤個 種旋轉、分割等等⽅法,避免樹退化。
2.4 Treap
樹堆,⼀個可以⽤來解決RMQ問題的樹狀結構。樹是指樹性質,左⼩孩⼩於⾃⼰、右⼩
孩⼤於等於⾃⼰,堆是指推性質,最⼤值在上⾯,每個⼩孩都⽐⾃⼰⼩。
我們給每個節點兩個值,並分別讓這兩個值符合樹性質和堆性質,就稱為⼀顆Treap。⽬
前Treap常常當成⼆元搜尋樹使⽤,只是我們在插⼊值時,隨機給⼀個優先值,並想辦法
讓這個優先值符合堆性質,這樣的話,我們可以保證在運氣不會太差的情況下,Treap可 以有良好的效率。
Treap有多種實作⽅法,⽬前常⾒的是分割法,也就是將樹切開,處理完在合併。
2.5 左偏樹
把Treap的code稍微改⼀改,就可以得到左偏樹。
左偏樹就是盡量讓樹退化成⼀條線,然後快速取得最⼤值的⼀種資料結構。實作時,如果 右邊⼦節點數⽐左邊多,就交換兩個節點,維持左邊節點⽐較多的狀態,然後處理都往右 邊丟,這樣複雜度就可以保證⼩於O(lgN),同時他和 Binary Heap 的不同是,他可以快 速合併,在某些狀況下很⽅便。
IV
2.6 持久化資料結構
持久化資料結構,原始的意義是指,我們常常對⼀些資料結構,⽐如線段樹、Treap做了
⼀些修改操作,如插⼊、刪除等等,但是我們卻有時候⼜想要回復或是查看某個歷史版本 的狀態,⽐如打字打⼀打想要復原等。
這種時候持久化資料結構就派上⽤場了。我們可以每次更新就複製⼀份新的資料結構 出來,這樣⼀來就可以保存所有歷史版本。但是這樣記憶體顯然會爆炸,所以我們利⽤
Copy on Write的技巧,也就是當你有修改時,就複製⼀份出來。
⽐如說有⼀顆線段樹,當你修改了⼀個點,你就複製⼀個新的點,然後讓他只向原本的⼩
孩,這樣其他⼈指到的是舊版本,⽽底下不變的地⽅就不⽤複製了!
後來持久化線段樹也可以⽤來解決相似的資料結構⼤量重複出現的問題,⽐如你想要開很 多顆線段樹在紀錄⼀個序列每個前綴的某個東西,那你可以不⽤開 N顆線段樹,⽽是開
⼀顆持久化線段樹然後不停地修改。
話說,持久化線段樹在⼤陸⼜稱為主席樹。
2.7 Exercises!!
1. <TIOJ 1843 傳說中的編譯器,上集。>
給你⼀張無向圖,三種操作,分別是修改某個點的權重、刪除某條邊、紀錄和某個點 相鄰的每個點中第 K⼤的點,題⽬問你所有被記錄的點權平均。
2. <TIOJ 1840 Coding Daysコーディングデイス >
給你⼀個序列,你有兩種操作,修改⼀個點的值,和查詢⼀個區間第K⼤的數字。
3. <TIOJ 1842 遊戲Game| IOI 2013>
給你⼀個平⾯,你有兩種操作,分別是修改⼀個點,和查詢⼀個矩形區域內的植的 GCD。
4. <TIOJ 1866 ⼩向的魔動⼒捷運系統>
給你⼀棵有根樹,每個點都有權重,問你每個節點到根的路徑上,總 XOR值最⼤的
⼦路徑,總XOR值是多少。
V