【字典樹】HDU C++ 1251 統計難題

HDU和PAT,很多時候都還需要使用C風格的字符串和輸入輸出函數,有點麻煩…

  • Problem Description
    Ignatius最近遇到一個難題,老師交給他很多單詞(只有小寫字母組成,不會有重複的單詞出現),現在老師要他統計出以某個字符串爲前綴的單詞數量(單詞本身也是自己的前綴).

  • Input
    輸入數據的第一部分是一張單詞表,每行一個單詞,單詞的長度不超過10,它們代表的是老師交給Ignatius統計的單詞,一個空行代表單詞表的結束.第二部分是一連串的提問,每行一個提問,每個提問都是一個字符串.
    注意:本題只有一組測試數據,處理到文件結束.

  • Output
    對於每個提問,給出以該字符串爲前綴的單詞的數量.

  • Sample Input

    banana
    band
    bee
    absolute
    acm
    
    ba
    b
    band
    abc
    

方法一:使用map,很簡單。主要在於分解一個單詞的前綴,這裏是一個不錯的方法。

#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#include <map>
using namespace std;

int main() {
	map<string, int> mp;  
	char str[13];
	while (gets(str)) {
		int len = strlen(str);  
		if (!len) break; // 爲空行時退出循環,不再輸入單詞表 
		for (int i = len; i > 0; --i) { // 從後往前依次刪除這個字符串的字符,得到所有的前綴 
			str[i] = '\0'; // 單詞本身也是自己的前綴,""不是前綴 
			mp[str]++; // 統計相應前綴的數量 
		}
	}
	while (gets(str)) cout << mp[str] << endl; // 輸出以該字符串爲前綴的單詞的數量 
	return 0;
}

方法二:字典樹Trie,或者說前綴樹,又稱單詞查找樹,是一種基於哈希表Trie樹的快速內容查找算法,是一種哈希樹的變種。它的優點是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,查詢效率比哈希樹高。

字典樹的基本性質有3點:

  1. 根結點不包含字符,除根結點外的每一個結點都包含一個字符;
  2. 從根結點到某一個結點,路徑上字符連接起來就是該節點的前綴,是該節點對應的字符串;
  3. 每個結點的所有子結點包含的字符互不相同,使用後代結點的指針目錄,即固定長度的指針數組,對應可能出現的所有字符。

字典樹的用途:

  • 基本用途,字符串檢索。一般從字符串數組中檢索一個字符串,需要O(N*M)的時間,N是數組長度,M是該字符串的長度,效率很低。原因在於比起搜索整數或實數(耗常數時間)來說,字符串比較需要正比於字符串長度的時間。而使用字典樹,可以降低到O(M)的時間,查找次數最多只需要這個單詞的字符個數;
  • 詞頻統計,統計一個單詞出現次數;
  • 字符串排序,先序遍歷Trie,可以得到其排序;
  • 前綴匹配,字典樹按照公共前綴建樹,很適合前綴匹配。

字典樹的複雜度:

  • 時間複雜度優秀;
  • 需要的空間太過龐大,只保存小寫字母,每個結點26個指針,64位指針爲8位,那一個結點就是200bytes,100萬長度的字符串,就需要200MB的內存空間。

在這裏插入圖片描述

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

struct trieNode {
	int num;
	trieNode *next[26];
	trieNode() { // 默認構造
		num = 0;
		memset(next, 0, sizeof(next));
	}
	~trieNode() { // 析構
		for (int i = 0; i < 26; ++i) {
			if (next[i]) 
			    delete next[i];
		}
	}
};

trieNode trie;

void insert(char s[]) {
	trieNode *p = &trie; 
	for (int i = 0; s[i]; ++i) { // 遍歷每一個字符 
		if (p->next[s[i] - 'a'] == nullptr)   // 結點不包括這個字符 
			p->next[s[i] - 'a'] = new trieNode; 
		p = p->next[s[i] - 'a']; // 進入下一層 
		p->num++; // 包含該前綴的字符串+1
	}
}

int find(char s[]) { // 返回以該字符串爲前綴的單詞的數量 
	trieNode *p = &trie;
	for (int i = 0; s[i]; ++i) { // 在字典樹中找到該單詞的結尾位置 
	    if (!p->next[s[i] - 'a']) return 0;  
		else p = p->next[s[i] - 'a'];
	}
	return p->num;
}

int main() {
    char strs[12];
	while (gets(strs)) {
		int len = strlen(strs);
		if (!len) break;
		insert(strs);
	}
	while (gets(strs)) cout << find(strs) << endl;
	return 0;
}

MLE了:
在這裏插入圖片描述

更簡單、更常用的方法是使用數組來實現字典樹,更加保險。使用pos給每個前綴標號,在num數組中使用對應前綴的pos值取其數量。

注意:這裏開到100萬的數組時也會MLE,如果只開10萬的數組會Runtime Error,估計是數組越界,開到50萬左右可以過。

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

int trie[500010][26];  // 用數組定義字典樹,存儲下一個字符的位置
int num[500010] = {0}; // 以某一字符串爲前綴的單詞的數量
int pos = 1;            // 當前新分配的存儲位置 

void insert(char s[]) { 
	int p = 0; // 字典樹第幾層
	for (int i = 0; s[i]; i++) {
		int n = s[i] - 'a';
		if (trie[p][n] == 0) // 對應字符沒有值 
			trie[p][n] = pos++;
		p = trie[p][n]; 
		num[p]++; // 每個前綴的單詞數量+1 
	}
}

int find(char s[]) { 
	int p = 0;
	for (int i = 0; s[i]; i++) {
		int n = s[i] - 'a';
		if (trie[p][n] == 0) // 對應字符沒有值
		    return 0;
		p = trie[p][n];
	}
	return num[p];
}

int main() {
	memset(trie, 0, sizeof(trie));
    char strs[12];
	while (gets(strs)) {
		int len = strlen(strs);
		if (!len) break;
		insert(strs);
	}
	while (gets(strs)) cout << find(strs) << endl;
	return 0;
}
發佈了166 篇原創文章 · 獲贊 59 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章