Trie树(字典树,单词查找树)

2023-11-15

【例题引入】

题目传送门于是他错误的点名开始了

题目大意:给出n个单词,有m个询问,每次给出一个单词,如果这个单词出现过且是第一次出现,输出“OK”,如果这个单词没有出现过,输出“WRONG”,如果这个单词出现过但不是第一次出现,输出“REPEAT”,其中n≤10000,m≤100000,每个单词长度l≤50(洛谷 P2580)

对于这道题,暴力是很好做的,每次询问都枚举一下n个单词,再判断一下是否符合题意

但是这样做的时间复杂度是O(n*m*l),很明显会超时

这个时候我们就要用到Trie树了,Trie树每次插入和查询的复杂度都为O(l),总复杂度为O((n+m)*l)

 

【介绍】

Trie树是一种树形结构,是一种哈希树的变种。典型应用是用于统计排序保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

(这是从百度上找来的,本蒟蒻连哈希树是什么都不知道)

 

【基本思想】

那么首先,Trie树长什么样子呢?

上图就是由单词at,bee,ben,bt,q组成的Trie树

很容易可以看出,每个字母的父亲节点就是它的前一个字母

Trie树的三个性质:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
  3. 每个节点的所有子节点包含的字符都不相同

这样看来,对于一个长为l的单词,无论是插入还是查询都是O(l)的时间复杂度

我习惯于用结构体来存储Trie树:

struct Trie
{
	int son[26];   //son[i]记录的当前节点的子节点 
	int num;       //num是当前这个单词在查询中出现的次数(题目要求) 
}a[1000005];       //其实也不用开到这么大,但我一般为了保险都喜欢开大一点 

那么,接下来就是介绍如何插入查询

 

插入:

插入操作就是将单词的每个字母都逐一插入Trie树,插入前看这个字母对应的节点是否存在,若不存在就新建一个节点,否则就共享那一个节点,还是以下图为例:

假如说我们要在原Trie树中新插入一个单词and,那我们的操作为:

  1. 插入第一个字母a,发现根节点存在子节点a,则共享节点a
  2. 插入第二个字母n,发现节点a不存在子节点n,则新建子节点n
  3. 插入第三个字母d,发现节点n不存在子节点d,则新建子节点d

代码如下:

char x[15];        //x是当前的单词 
int t=0;           //t是节点的编号 
void build_trie()
{
	int i,l,p=0;   //p是当前字母的编号 
	l=strlen(x);
	for(i=0;i<l;++i)
	{
		if(a[p].son[x[i]-'a']==0)      //如果这个子节点不存在 
		  a[p].son[x[i]-'a']=++t;      //新建一个子节点 
		p=a[p].son[x[i]-'a'];          //插入下一个字母 
	}
}

 

查询:

查询操作和插入操作其实差不多,就是在Trie树中找这个单词的每个字母,若找到了就继续找下去,若没有找到就可以直接退出了,因为若没找到就说明没有这个单词,还还还是以下图为例:

假如说我们要在原Trie树上查询单词and是否存在,那我们的操作为:

  1. 查询第一个字母a,发现根节点存在子节点a,则继续查询n
  2. 查询第二个字母n,发现节点a不存在子节点n,则直接退出并返回0

代码如下:

char x[15];        //x是当前的单词 
int get_answer()
{
	int i,l,p=0;   //p是当前字母的编号 
	l=strlen(x);
	for(i=0;i<l;++i)
	{
		if(a[p].son[x[i]-'a']==0)      //如果这个子节点不存在 
		  return 0;                    //直接退出并返回0 
		p=a[p].son[x[i]-'a'];          //查询下一个字母 
	}
	a[p].num++;                        //这个单词的查询次数加一(题目要求) 
	return a[p].num;                   //返回它的查询次数 
}

 

【复杂度分析】

Trie树其实是一种用空间换时间的算法,前面也提到过,它占用的空间一般很大,但时间是非常高效的,插入和查询的时间复杂度都是O(l)的,总体来说还是很优秀的

 

【代码】

下面是例题的完整代码(洛谷 P2580):

#include<cstdio>
#include<cstring>
#include<algorithm> 
using namespace std;
struct Trie
{
	int son[26];
	int num;
}a[1000005];
int t=0;
char x[15];
void build_trie()
{
	int i,l,p=0;
	l=strlen(x);
	for(i=0;i<l;++i)
	{
		if(a[p].son[x[i]-'a']==0)
		  a[p].son[x[i]-'a']=++t;
		p=a[p].son[x[i]-'a'];
	}
}
int get_answer()
{
	int i,l,p=0;
	l=strlen(x);
	for(i=0;i<l;++i)
	{
		if(a[p].son[x[i]-'a']==0)
		  return 0;
		p=a[p].son[x[i]-'a'];
	}
	a[p].num++;
	return a[p].num;
}
int main()
{
	int n,m,i,ans;
	scanf("%d",&n);
	for(i=1;i<=n;++i)
	{
		scanf("%s",x);
		build_trie();
	}
	scanf("%d",&m);
	for(i=1;i<=m;++i)
	{
		scanf("%s",x);
		ans=get_answer();
		if(ans==1)  printf("OK\n");
		if(ans==0)  printf("WRONG\n");
		if(ans>=2)  printf("REPEAT\n");
	}
    return 0;
}

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Trie树(字典树,单词查找树) 的相关文章

  • 使用Kotlin做一个简单的HTML构造器

    最近在学习Kotlin 看到了Kotlin Koans上面有一个HTML构造器的例子很有趣 今天来为大家介绍一下 最后实现的效果类似Groovy 标记模板或者Gradle脚本 就像下面 这是一个Groovy标记模板 这样的 html lan

随机推荐

  • Linux 下查看进程运行时长

    因为有一个Java程序运行中会持续输出大量的日志文件 可能导致磁盘空间不足 为了规避这个风险 需要根据程序运行时长估算磁盘使用量 1 查看进程的PID ps ef grep java 2 指定进程查看运行时长 PID ps o etime
  • SpringBoot优化

    一 SpringBoot全局异常处理 任何项目发生异常是不可避免的 使用全局异常捕获发生的异常是十分必要的 SpringBoot框架对全局异常捕 获提供了很好的支持 并且操作非常简单 我们只需要创建一个类和一个方法 并添加两个注解 Cont
  • LSTM分类模型

    LSTM文本分类模型 本文主要固定一个文本分类的流程 分为三个部分 数据处理 对分类文本数据集做简单的预处理 模型数据准备 处理上一步的结果 得到模型的输入样本 模型搭建和训练流程 模型使用BiLSTM 训练过程可以使用cpu或者GPU t
  • shell中的for循环的用法(C语言式)

    C语言式的for循环 用法 exp1 exp2 exp3 是三个表达式 其中exp2是判断条件 for循环根据exp2的结果来决定是否继续下一次的循环 statements是循环体语句 可以有一条 也可以有多条 do和done是shell中
  • Kotlin和Java中的IO操作

    Kotlin的特性 1 Kotlin提供了非常多 File Stream Reader Writer的拓展方法 2 使用use拓展自动关闭资源 3 小文件一次性读写操作 一 首先来看看繁琐的JavaIO操作 来读取一个文件 package
  • 有1、2、3、4四个数字,可以组成多少个互不相同且无重复的三位数?都是多少?

    这个题呢 顾名思义 就是说一个三位数的每一位都是1 2 3 4 个位十位百位上的数字不能重复 编程原理很简单 分别定义三个变量代表个位十位百位 然后使用for循环嵌套每一层循环代表一位数 如果个位十位百位都不相同 则输出 程序如下 incl
  • 微信订阅消息模板推送报错47003 data.time.value i,及解决方案

    今天又是枯燥的一天 依然敲着代码 客户有个微信消息推送的需求 找了下官方文档 微信消息推送文档 大致看了一下 需要模板ID和微信后台的小卡片参数名 随即便敲起了代码 首先定义模板类 代码如下 public class Template pr
  • 微信小程序实现一些炫酷的loading动画

    1 实现效果 2 实现原理 伪元素 css3动画 transform 3 实现代码 从上到下 从左到右依次的代码如下
  • 三款记事本替代工具 哪个最好用?

    三款记事本替代工具 哪个最好用 http www sina com cn 2008年08月27日 08 35 IT168 com Windows操作系统中自带了不少的实用小程序 但是它们大都功能简陋 有时无法满足我们的使用 此外还有一些Wi
  • MatplotLib 第二部分

    1 import numpy as np 2 import pandas as pd 3 import matplotlib pyplot as plt 4 5 导入数据 6 df pd read excel d test xlsx 7 p
  • 在VS中使用命令行参数

    在VS工具中 若要运行带有命令行参数的程序 有两种方法 方法一 在命令提示符中输入要运行的exe的文件名和要输入的参数 各参数之间用空格隔开 如exe文件为test exe 则输入 test 参数1 参数2 参数n 注意 exe文件应放在C
  • 我的软件渲染器终于初步完成了~

    记录一个大好事 在 2021年第一个月的上旬 我的软件着色器终于初具雏形了 中间参考了 很多 资料 最初是 知乎上的系列教程 https zhuanlan zhihu com p 141210744 这个教程是基于 OpenGL 右手坐标系
  • 什么是准双向口,双向口?

    C51的说明书上说 Because Ports 1 2 and 3 have fixed internal pullups they are sometimes called quasi bidirectional ports When c
  • golang 杂技

    Swap 记录一个骚操作 交换数组的两个元素 package main import fmt func main m int 1 2 Swap m 0 1 fmt Println m 2 1 func Swap i int a b int
  • C语言方波转换正弦波,方波转换成正弦波电路

    方波转换成正弦波电路 即利用RDD104可选的4各十进制CMOS除法器和一个MSFS5 开关电容滤波器来构建一个双芯片 失真率为0 2 的正弦波源 RDD104有两个引脚 可以从四个除法器divide by 10 divide by 100
  • 离线数仓经验之谈三-数仓流程规范

    数仓流程规范 目录 1 目的 2 适用范围 3 总体流程 3 1 ETL开发流程 3 1 1 需求分析 3 1 2 数据来源与数据探查 3 1 3 数据模型设计 3 1 4 ETL开发 3 1 5 测试 3 1 6 ETL上线 3 1 7
  • 想入手显示器,恳请粉丝带我推荐,必有重谢!

    坏了一个显示器 本来家里好好的两个显示器 其中1个有点雪花亮线 当时特地买的EIZO 考虑已无维修价值 打算换一个显示器 但是某宝搜了一圈 已经被各种参数和品牌搞晕掉 2K 4K 准4K IPS 60hz 144HZ 高刷 曲面屏 带鱼屏
  • 队列数据类型及Python实现

    1 队列的实现 队列是一种有次序的数据集合 其特征是 新数据项的添加总发生在一端 通常称为尾端 rear 而现存数据类型的移除总发生在另一端 通常称为首段 front 当数据项加入队列 首先出现在队尾 随着队首数据项的移除 它逐渐接近队首
  • NEZHA知识点

    1 华为NEZHA 主要是将bert之后预训练模型的长处拼接在一起 1 相对位置编码 Bert的位置编码是直接初始化一个embedding 然后通过预训练去学的 是固定位置编码 NEZHA使用函数式相对位置编码 在qk时 加在k上 表示q和
  • Trie树(字典树,单词查找树)

    例题引入 题目传送门于是他错误的点名开始了 题目大意 给出n个单词 有m个询问 每次给出一个单词 如果这个单词出现过且是第一次出现 输出 OK 如果这个单词没有出现过 输出 WRONG 如果这个单词出现过但不是第一次出现 输出 REPEAT