算法總結篇---字典樹(Trie)

寫在前面

字典樹是一種清新通俗的數據結構(仍是算法?)node

顧名思義,字典樹就是一棵像字典同樣的樹,能夠用來查詢某個單詞是否出現過,查詢過程就像查字典同樣每一個字符挨個找,看看是否有這個單詞ios

具體實現

引例:

給你兩個整數 \(n\)\(m\) ,表示有 \(n\) 個單詞和 \(m\) 次詢問
在詢問過程當中,若是某個單詞第一次被查到輸出OK,若是不是第一次被查到輸出REPEAT,若是沒有該單詞輸出WRONGgit

先看一個樣例算法

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\) 的取值與字符集有關,可根據題目具體要求來定)spa

相信你們已經發現 hehers 的路徑上有重疊,那麼如何區分呢?
爲了標記插入字典樹的字符串,只須要每次插入完成時標記其所在的結點便可指針

放一個結構體封裝的模板code

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;

具體解釋在註釋裏講的很清楚了blog

引例代碼:

只須要在查詢返回時作一下標記處理便可

/*
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

相關文章
相關標籤/搜索