1. 程式人生 > 實用技巧 >演算法總結篇---字典樹(Trie)

演算法總結篇---字典樹(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\) 的取值與字符集有關,可根據題目具體要求來定)

相信大家已經發現 hehers 的路徑上有重疊,那麼如何區分呢?
為了標記插入字典樹的字串,只需要每次插入完成時標記其所在的結點即可

放一個結構體封裝的模板

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)\)
稍微優化一下,在插入時判斷。發現一個數是另一個數的字首有兩種可能,一是遍歷過程中經過了其他標記過的結點,二是遍歷結束後沒有新建結點

Code

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]\),遍歷過程中統計答案即可,最後對所有答案取最大值

Code

L語言

給定由 \(n\) 個單片語成的字典,有 \(m\) 段文章,輸出一段文章從前向後理解最多能理解多少。
規定一段字串被理解當且僅當這一段字串是字典中的某整個單詞

Solution:

建樹不多說了,
在理解一段文章時,因為每當一段字元是字典中的整個單詞,都可以被理解,那麼從前向後遍歷,對於某個位置,如果它是某個單詞的結尾,那麼它的下一個位置可以重新從根節點中開始匹配。在匹配過程中如果發現遍歷到的結點是某個單詞的結尾,將其標記,方便下一次匹配。匹配過程中順便記錄最後一個被標記的單詞的結尾的位置。

Code