1. 程式人生 > 遊戲 >前《魔獸》設計師鬼蟹道歉:角色太性感涉嫌性別歧視

前《魔獸》設計師鬼蟹道歉:角色太性感涉嫌性別歧視

Trie樹

Trie樹,又叫字典樹字首樹(Prefix Tree)單詞查詢樹鍵樹,是一種多叉樹結構。如下圖:

上圖是一棵Trie樹,表示了關鍵字集合{“a”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”} 。從上圖可以歸納出Trie樹的基本性質:

  1. 根節點不包括字元,除根節點之外的每一個結點都包括一個字元。
  2. 從根結點到達某一個結點,把路徑上的字元依次連線起來,就是一個關鍵字,即代表這個結點的字串。
  3. 每個結點的所有子結點都各不相同。

但是,只給出一棵樹,沒辦法知道到達哪一個結點才形成關鍵字,所以我們通常給出一個變數儲存該結點是否為關鍵字的資訊。

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++;
	}
}