前《魔獸》設計師鬼蟹道歉:角色太性感涉嫌性別歧視
Trie樹
Trie樹,又叫字典樹、字首樹(Prefix Tree)、單詞查詢樹 或 鍵樹,是一種多叉樹結構。如下圖:
上圖是一棵Trie樹,表示了關鍵字集合{“a”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”} 。從上圖可以歸納出Trie樹的基本性質:
- 根節點不包括字元,除根節點之外的每一個結點都包括一個字元。
- 從根結點到達某一個結點,把路徑上的字元依次連線起來,就是一個關鍵字,即代表這個結點的字串。
- 每個結點的所有子結點都各不相同。
但是,只給出一棵樹,沒辦法知道到達哪一個結點才形成關鍵字,所以我們通常給出一個變數儲存該結點是否為關鍵字的資訊。
Trie樹一般儲存的關鍵字是字串,並且Trie樹把每個字串分成一個個字元儲存起來,這樣,如果有一個字串u是v的字首,那麼它們是有一條公共路徑的,所以Trie又被稱作字首樹。
Trie是時空複雜度較優秀的字首演算法,他能在O(n)的複雜度下儲存串或者查詢串。相比較strstr(查詢某字元是n個字元中幾個字串的字首,複雜度為O(n^2)),能更快速的解決問題,因為Trie將所有的串都集中在了一個樹上,不需要用for迴圈遍歷所有串。
Trie結構體:
struct trie_node { int cnt; // 標記改結點是否為關鍵字(有多少個),可以靈活運用,如上述問題應該是擁有這個結點串字首的字串數量 trie_node* trie_child[26]; // 儲存各個結點的子結點,假設為a-z,NULL表示不存在該子結點 };
Trie樹的應用:
查詢/檢索是Trie樹最基本的功能,分為兩種情況:
- 沿路比較每個字元,如果沒有對應的子結點,則該串不存在。
- 如果比較完成且該結點的的標誌位不為0,則此串存在。
而插入字串只需要沿路走,沒有結點直接建立即可。
trie_node* creat_trie_node () { // 建立一個trie樹結點 trie_node* node = new trie_node; node -> cnt = 0; for (int i = 0; i < 26; i++) node -> trie_child[i] = NULL; return node; } trie_node* root = creat_trie(); // 定義根節點(在建立函式建立) int trie_search (char key[]) { // 若串不存在,則返回0,否則返回該串的出現次數 trie_node* node = root; while (*key) { // 查詢key的每一個字元 int id = *key - 'a'; if (node -> trie_child[id] == NULL) return 0; node = node -> trie_child[id]; key++; } return node -> cnt; } void trie_insert (char key[]) { trie_node* node = root; while (*key) { int id = *key - 'a'; if (node -> trie_child[id] == NULL) { node -> trie_child[id] = creat_trie_node(); } node = node -> trie_child[id]; key++; } node -> cnt++; }
如果測試資料比較多,好需要在測試完一組後進行銷燬樹的操作,用遞迴銷燬,先銷燬子樹,再銷燬自己。
void destroy_trie (trie_node* root) {
for (int i = 0; i < 26; i++) {
if (root -> child[i] != NULL) destroy_trie(root -> child[i]);
}
delete root;
}
用陣列來模擬Trie樹(更快)
int son[N][26], cnt[N], idx;
// 0號點既是根節點,又是空節點
// son[][]儲存樹種每個節點的子結點
// cnt[]儲存以每個節點結尾的單詞數量
// 插入一個字串
void insert (char* s) {
int p = 0;
while (*s) {
int u = *s - 'a';
if (!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
s++;
}
cnt[p]++;
}
// 查詢字串出現的次數
int query (char* s) {
int p = 0;
while (*s) {
int u = *s - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
s++;
}
return cnt[p];
}
例題
Hat’s Words
如果一個字串由其他兩個字串拼接而成,則稱此字串為Hat's Words,給出n個字串,問其中有幾個字串為Hat's Words。
題解:根據n個字串構造Trie樹,然後對每個字串進行查詢,如果查詢到一個關鍵字,則對字串剩下部分check,如果正好是一個關鍵字,則為Hat's Words。
bool check (char s[]) {
trie_node* node = root;
while (*s) {
int id = *s - 'a';
if (!node -> child[id]) return false;
node = node -> child[id];
s++;
}
return node -> is_key;
}
bool solve (char s[]) {
char *t = s;
trie_node* node = root;
while (*s) {
int id = *s - 'a';
if (node -> child[id] != NULL) {
if (node -> child[id] -> is_key && *(s+1)) {
if (check(s+1)) return true;
}
}
else break;
node = node -> child[id];
s++;
}
return false;
}
Repository
給出n個字串,再給出m次查詢,問查詢的字串是多少個給定字串的子串。
子串可以為任意位置,所以要把給定字串的每個子集都加入到Trie樹中。
題解:Trie高階運用,需要對給定字串每個字尾插入到Trie樹中,插入樹時,要記錄字尾的每個字首的出現次數 --> 這樣就可以把每個子集加入到Trie中,對於add,插入dd時,root->d加一,但插入d時,root->d又加一,會使d的查詢結果加一(add是d的一個超集,但d加了兩次),還需要設定一個f標誌表示對於這個字串而言,某個字元是否前面已經加過,如果加過就不再加。
struct trie {
int f;
int cnt;
trie* child[26];
};
trie* creat_node () {
trie* node = new trie;
node -> cnt = 0;
node -> f = -1;
for (int i = 0; i < 26; i++) node -> child[i] = NULL;
return node;
}
void trie_insert (char *s, int f) { // f為給定字串的順序,十分巧妙
trie* node = root;
while (*s) {
int id = *s - 'a';
if (node -> child[id] == NULL) node -> child[id] = creat_node();
node = node -> child[id];
if (node -> f != f) { // 說明f序號的給定字串對於這個子集沒有賦值過!
node -> cnt++;
node -> f = f; // 把f值賦為f,對於這個字串,子集就不會重複加入,下一個字串f+1,又不一樣了
}
s++;
}
}