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點:
- 根結點不包含字符,除根結點外的每一個結點都包含一個字符;
- 從根結點到某一個結點,路徑上字符連接起來就是該節點的前綴,是該節點對應的字符串;
- 每個結點的所有子結點包含的字符互不相同,使用後代結點的指針目錄,即固定長度的指針數組,對應可能出現的所有字符。
字典樹的用途:
- 基本用途,字符串檢索。一般從字符串數組中檢索一個字符串,需要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 = ≜
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 = ≜
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;
}