貌似不少人會認爲\(Trie\)是字符串類型,可是這是數據結構!!!。html
詳情見度娘數組
下面開始進入正題。數據結構
PS:本文章全部代碼未經編譯,有錯誤還請你們指出。函數
先來看一個問題學習
給定一本字典中的\(n\)個單詞,還有\(m\)個詢問。每次詢問詢問一個單詞是否出如今這\(n\)個單詞中。優化
最簡單的就是暴力作法啦,咱們直接枚舉去判別對應位置,還能夠再加點優化。spa
即:長度不一樣,確定不是同一個單詞。code
for(int l;m;m--) { bool flg=false; scanf("%s",str); l=strlen(str); for(int i=1;i<=n;i++) { int len=strlen(s[i]) if(l!=len)continue; for(int j=1;j<=len;j++) { if(s[i][j]!=str[j]) { flg=true; break; } } if(flg) { for(int j=1;j<=len;j++) printf("%c",s[i][j]); } putchar('\n'); } }
時間複雜度爲\(O(n\times m\times len)\)htm
很容易發現暴力不是很好寫。並且數據範圍再大一點的話就根本跑不動,並且存不下。blog
當數據範圍增強到剛恰好直接存儲,存儲不下的時候。咱們就要考慮改進。
那麼如何改進呢?
假如給定咱們的\(n\)個單詞是這樣的。
\[ abcaa\\aabca\\abdac\\aabcabd\\bacabd\\ \dots \]
那麼咱們考慮這樣一個問題
A:容易發現的是,在保證單詞不亂序的狀況下,他們的部分前綴是相同的!那麼咱們是否是能夠從這裏入手來減小空間複雜度呢?
固然能夠啊!這個時候咱們須要聯想到樹的性質:
- 相同的能夠壓縮成一個節點(那咱們就能將這些前綴壓縮來減小空間消耗)
- 樹上路徑惟一。(那咱們就能夠保證每個單詞之間的存儲互不影響)
由此,咱們便引出了\(Trie\)樹
Trie是一種多叉樹,在對單詞處理中是常常利用的一種數據結構,是樹結構中一種較爲特殊的樹結構,它在計算機中對字符采用鏈表方式存儲 。
---百度百科
Trie的核心思想是空間換時間,利用字符串的公共前綴來下降查詢時間的開銷以達到提升效率的目的。
它有3個基本性質:
根節點不包含字符,除根節點外每個節點都只包含一個字符。
從根節點到某一節點,路徑上通過的字符鏈接起來,爲該節點對應的字符串。
每一個節點的全部子節點包含的字符都不相同。
---百度百科
通常的Trie有兩種存儲方式:
這裏給出將字符存儲在點上的形式
大概就是這個樣子啦。
這樣從根節點到任何一個子節點就都是一個存儲的單詞啦。
圖中所存儲的就是這些單詞咯
\[ aba\\abbc\\aab \]
(因爲時間問題就沒有做一顆比較大的Trie,見諒。)
知道了\(Trie\)的一些簡單東西。
那麼咱們考慮如何構建一棵\(Tire\)。
顯然咱們須要將一個單詞完整存儲就須要遍歷整個單詞來存儲每一位字母。
而且咱們須要從淺入深地遍歷這棵樹。
寫法能夠爲遞歸版本,也能夠爲非遞歸版本。
這裏給出非遞歸版本。
code
void ins(char *s) { int rt=0; for(int i=0,v;s[i];i++) { int v=s[i]-'a'; if(!trie[rt][v]) trie[rt][v]=++tot;//給每一個節點一個編號,tot爲全局變量 rt=trie[rt][v];//向下一個節點,繼續存儲當前單詞 } }
由咱們構建函數可知,Trie的空間複雜度爲(單詞長度 \(\times\) 字符種類 )。
且這個構建過程的時間複雜度爲\(O(n^2)\)
時間複雜度證實,不會 emmm。
Trie樹通常支持兩種查詢操做:
有些時候,根據寫法的不一樣,咱們還能夠判斷一個單詞是否是某個單詞的前綴。
固然若是你構建後綴的Trie的話,還能夠判斷這個單詞是否是某個單詞的後綴。而這個樹又名爲後綴樹。
對於後綴樹這裏不進行研究。(等我有時間會寫的。)
不管是哪一種查詢操做,咱們都須要遍歷整棵樹來判斷是否存在這些節點。
依舊有兩種寫法,這裏給出非遞歸版。
code
bool find(char *s) { int rt=0; for(int i=0,v;s[i];i++) { v=s[i]-'a'; if(!trie[rt][v])retturn false; rt=trie[rt][v]; } if(ok[rt])return true; //加上這一句能夠判斷當前單詞是否出現過。 //而若是不加,就能夠判斷當前單詞是否是某個單詞的前綴 //若是加上這一句的話就在構建的時候對應的在詞尾標記便可。 return false; }
根據\(m\)個詢問,再進行遍歷,顯然這樣的時間複雜度爲\(O(n^2)\)。
而Trie的應用還有不少。這裏不一一介紹了,畢竟我是個退役選手啊。
具體的完整代碼詳見例題部分。
後面繼續深刻學習的話還有01Trie,可持久化Trie.
下面講解一下01Trie
有一類問題叫作01字典樹問題,它是用來解決xor的有力武器,一般是給你一個數組,問你一段連續的異或和最大是多少,正常思路貪心dp啥的都會一頭霧水,可是用01字典樹就能很快的解決,實現起來也十分方便
(其實和trie樹差很少。
01字典樹的實現能夠當作是把一個數的二進制字符化後插入到一棵通常的字典樹中。
那大家會不會有疑問就是說 當某一個數的二進制位是另外一個數的二進制位的某一前綴的時候 是否是查不到這個數
\[ 7 =111_{(2)} \\3=11_{(2)} \]
而後這個時候 咱們能夠作一個處理 即 從一個特別特別長的位置開始插入(通常會選擇 1<<30位開始處理 ,對應:01字典樹種插入3時 至關於在字典樹中插入00 …..00011)
但仍是要見題而異
當咱們查詢最大異或值的時候,咱們從最高位向下貪心查找。
貪心策略:
當前查找第k位,二進制數位x,若是存在x^1的節點,咱們就進入這個節點,不然進入x節點。
證實:
這個不太會用文字來敘述,直接看一下比較。
\[ 8=1000_{(2)}\\7=0111_{(2)} \]
很明顯,當前對應的最高位上是\(1\),比它後面位上全是\(1\)要大。
所以咱們的策略是正確的。
對應01Trie的性質與Trie的類似,這裏再也不贅述。
而咱們引入一個重要的性質:
若是要求[l,r]的異或和:
由\(X \ xor \ X = 0;0 \ xor\ Y = Y;\)
推知:全部\([l,r] = [1,r] XOR[1,l - 1]\)
一樣會有兩種寫法,這裏給出非遞歸版本的代碼
code
void ins(int x) { int rt=0; for(int i=1<<30;i;i>>=1) { bool c=x&i; //這裏推薦寫bool類型,不推薦寫int類型,由於很容易忘記是對應位爲1 if(!trie[rt][c]) trie[rt][c]=++tot;//記錄節點編號 rt=trie[rt][c]; } }
應該不是很難理解,不做詳細解釋。
這裏是查詢一個數的最大異或和。
一樣給出非遞歸版本的代碼。
code
int query(int x) { int ans=0,rt=0; for(int i=1<<30;i;i>>=1) { bool c=x&i; if(trie[rt][c^1]) ans+=i,rt=trie[rt][c^1]; //若是有咱們就選擇這個較高位來實現咱們的貪心策略.記得累計答案 else rt=trie[rt][c]; } return ans; }
總的來講,在這有限的幾個小時我也就只能寫這麼多吧。
再深刻一些仍是須要你們後期的學習。若是能幫助到你們很高興。有不足之處還請你們多多指出。
這些例題就不作深刻剖析,有些我是寫過題解的,都會給出連接。
若是以爲很麻煩就來我博客園連接好了.
題目:TJOI2010 閱讀理解 題解:這
題目:UVA11362 Phone List 題解:這
這些題目包括可持久化01Trie還有普通01Trie
題目:p4551 最長異或路徑 題解:這
下面兩個是可持久化01Trie
題目:TJOI2018 異或 題解:這
題目:p4735 最大異或和 題解:這
感謝洛谷提供的題目。爲洛谷打call!!
感謝百度百科提供的一些介紹。
原生態顧z創做。