【字典树】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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章