演算法總結篇---字典樹(Trie)
寫在前面
字典樹是一種清新通俗的資料結構(還是演算法?)
顧名思義,字典樹就是一棵像字典一樣的樹,可以用來查詢某個單詞是否出現過,查詢過程就像查字典一樣每個字元挨個找,看看是否有這個單詞
具體實現
引例:
給你兩個整數 \(n\) 和 \(m\) ,表示有 \(n\) 個單詞和 \(m\) 次詢問
在詢問過程中,如果某個單詞第一次被查到輸出OK
,如果不是第一次被查到輸出REPEAT
,如果沒有該單詞輸出WRONG
先看一個樣例
5
i
he
his
she
hers
3
hi
sheself
love
貼一個字典樹成品圖:
可以發現,生成的這棵字典樹可以從根節點 \(0\) 開始,找到所有給出的單詞。舉個栗子,\(0 \to 2 \to 4 \to 5\)
his
字典樹的結構還是比較簡單的,我們用 \(tr_{u,c}\) 表示結點 \(u\) 通過 \(c\) 字元指向的下一個結點,或者說在結點 \(u\) 所代表的字串中加一個字元 \(c\) 後所在的新節點(\(c\) 的取值與字符集有關,可根據題目具體要求來定)
相信大家已經發現 he
在 hers
的路徑上有重疊,那麼如何區分呢?
為了標記插入字典樹的字串,只需要每次插入完成時標記其所在的結點即可
放一個結構體封裝的模板
struct Trie{ int tr[MAXN][26], node_cnt = 0;//字典樹以及結點個數 bool cnt[MAXN];//標記是否是某個字串的結尾 void insert(char *s){//插入操作 int now = 0, len = strlen(s + 1);//now表示當前所在的結點,len表示字串長度 for(int i = 1; i <= len; ++i){ int ch = s[i] - 'a';//取出要插入的字元 if(! tr[now][ch]) tr[now][ch] = ++node_cnt; //如果這個字元未被插入,新建一個結點將其插入 now = tr[now][ch];//now指標跳向tr[now][ch]指向的位置 } cnt[now] = true;//在字串完成時所在的結點處打上標記 } int find(char *s){//查詢操作 int now = 0, len = strlen(s + 1);//意義同上 for(int i = 1; i <= len; ++i){ int ch = s[i] - 'a'; if(!tr[now][ch]) return false;//如果沒有遍歷到的字元,直接返回false now = tr[now][ch];//now指標跳向tr[now][ch]指向的位置 } return cnt[now]; //注意這裡不能直接返回true,有可能查詢的只是某個串的字首,比如在樣例中查詢her } } trie;
具體解釋在註釋裡講的很清楚了
引例程式碼:
只需要在查詢返回時做一下標記處理即可
/* Work by: Suzt_ilymics Knowledge: ?? Time: O(??) */ #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define LL long long #define orz cout<<"lkp AK IOI!"<<endl using namespace std; const int MAXN = 1e6+4; const int INF = 1; const int mod = 1; int n, m; char s[100]; int read(){ int s = 0, f = 0; char ch = getchar(); while(!isdigit(ch)) f |= (ch == '-'), ch = getchar(); while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar(); return f ? -s : s; } struct Trie{ int tr[MAXN][26], node_cnt = 0; int cnt[MAXN]; void insert(char *s){ int now = 0, len = strlen(s + 1); for(int i = 1; i <= len; ++i){ int ch = s[i] - 'a'; if(! tr[now][ch]) tr[now][ch] = ++node_cnt; now = tr[now][ch]; } cnt[now] = 1; } int find(char *s){ int now = 0, len = strlen(s + 1); for(int i = 1; i <= len; ++i){ int ch = s[i] - 'a'; if(!tr[now][ch]) return 0; now = tr[now][ch]; } if(cnt[now] == 1){ cnt[now] = 2; return 1; } return 2; } } trie; int main() { n = read(); for(int i = 1; i <= n; ++i) cin >> s + 1, trie.insert(s); m = read(); for(int i = 1; i <= m; ++i){ cin >> s + 1; int ans = trie.find(s); if(ans == 0) printf("WRONG\n"); else if(ans == 1) printf("OK\n"); else printf("REPEAT\n"); } return 0; }
例題
Phone List
T組資料,每組資料給出n個長度不超過10數字串,問是否有一個串是另一個串的字首
Solution:
樸素做法是 \(n^{2}\) 判斷,
考慮如何用字典樹做,把n個數字串插入字典樹,在從頭遍歷一遍看看是否是其他字串的字首,複雜度 \(O(\sum\mid S \mid)\)
稍微優化一下,在插入時判斷。發現一個數是另一個數的字首有兩種可能,一是遍歷過程中經過了其他標記過的結點,二是遍歷結束後沒有新建結點
The XOR Largest Pair
在給定的 \(N\) 個整數 \(A_1,A_2,···A_n\) 中選出兩個進行異或運算,得到的結果最大是多少? $(0 \le n \le 2^{31} ) $
Solution
使用類似貪心的方法,先把 \(n\) 個數插進去時,將其拆成二進位制,先插高位再插低位
在 \(O(n)\) 掃一遍所有數查詢最大值,如果對應位數 \(x \ xor \ 1\) 存在,就走 \(tr[now][x \ xor \ 1]\) ,否則走 \(tr[now][x]\),遍歷過程中統計答案即可,最後對所有答案取最大值
L語言
給定由 \(n\) 個單片語成的字典,有 \(m\) 段文章,輸出一段文章從前向後理解最多能理解多少。
規定一段字串被理解當且僅當這一段字串是字典中的某整個單詞
Solution:
建樹不多說了,
在理解一段文章時,因為每當一段字元是字典中的整個單詞,都可以被理解,那麼從前向後遍歷,對於某個位置,如果它是某個單詞的結尾,那麼它的下一個位置可以重新從根節點中開始匹配。在匹配過程中如果發現遍歷到的結點是某個單詞的結尾,將其標記,方便下一次匹配。匹配過程中順便記錄最後一個被標記的單詞的結尾的位置。