Trie树模板与应用

news2024/11/17 7:16:58

文章和代码已经归档至【Github仓库:https://github.com/timerring/algorithms-notes 】或者公众号【AIShareLab】回复 算法笔记 也可获取。

文章目录

    • Trie树(字典树)
      • 基本思想
      • 例题 Trie字符串统计
        • code
        • 关于idx的理解
      • 模板总结
      • 应用 最大异或对
        • 分析

Trie树(字典树)

Trie树是用来快速存储和查找 字符串集合的数据结构。某个字符串集合对应的有根树。树的每条边上对应有恰好一个字符,每个顶点代表从根到该节点的路径所对应的字符串(将所有经过的边上的字符按顺序连接起来)。利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

基本思想

存储若干字符串(通常样本中的字符较少),然后根据字符串中字符出现的先后顺序建立树,把具有相同前缀的字符串按照其前缀归类在一个分支中,并且需要在字符串的最后一个位置进行标记(表明到此为一个完整的字符串)。

查找时只需要寻找是否有匹配的序列,并且是否已标记结尾即可。

例题 Trie字符串统计

维护一个字符串集合,支持两种操作:

  1. I x 向集合中插入一个字符串 x;
  2. Q x 询问一个字符串在集合中出现了多少次。

共有 N 个操作,输入的字符串总长度不超过 1 0 5 10^5 105,字符串仅包含小写英文字母。

输入格式

第一行包含整数 N,表示操作数。

接下来 N 行,每行包含一个操作指令,指令为 I xQ x 中的一种。

输出格式

对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x 在集合中出现的次数。

每个结果占一行。

数据范围

1 ≤ N ≤ 2 ∗ 1 0 4 1≤N≤2∗10^4 1N2104

输入样例:

5
I abc
Q abc
Q ab
I ab
Q ab

输出样例:

1
0
1

code

#include<iostream>
using namespace std;

const int N = 100010;
// 下标0代表根节点和空节点,cnt用于计数,idx代表当前的节点(和单链表一样)相当于是一个独一无二的递增编号,son[N][26]每个节点最多有26条边(小写英文字母)
int son[N][26], cnt[N], idx;
char str[N];
// 插入
void insert(char str[])
{
    int p = 0;// 根节点
    // 遍历字符串,cpp中str最后一位是\0
    for(int i = 0; str[i]; i ++)
    {
        // 映射字母a-z为0-25
        int u = str[i] - 'a';
        // 若不存在该节点则创建一个
        if(!son[p][u]) son[p][u] = ++ idx;
        // 走到该子节点
        p = son[p][u];
    }
    cnt[p] ++ ;// 标记该子节点存在的单词个数 记住这里p = son[p][u];
}
// 查询
int query(char str[])
{
    int p = 0;
    for(int i = 0; str[i]; i++)
    {
        int u = str[i] - 'a';
        if(!son[p][u]) return 0;
        p = son[p][u];
    }
    
    return cnt[p];
}

int main()
{
    //ios::sync_with_stdio(false);
    //cin.tie(0);
    int n;
    scanf("%d", &n);
    while(n --)
    {
        char op[2];
        scanf("%s%s", op, str);
        if(op[0] == 'I') insert(str);
        else printf("%d\n", query(str));
    }
    return 0;
}

关于idx的理解

不管是链表,Trie树还是堆,他们的基本单元都是一个个结点连接构成的,可以成为“链”式结构。这个结点包含两个基本的属性:本身的值和指向下一个结点的指针。按道理,应该按照结构体的方式来实现这些数据结构的,但是做算法题一般用数组模拟,主要是因为比较快。

原来这两个属性都是以结构体的方式联系在一起的,现在如果用数组模拟,如何才能把这两个属性联系起来呢,如何区分各个结点呢?答案是采用idx。

idx的操作总是 idx++,这就保证了不同的idx值对应不同的结点,这样就可以利用idx把结构体内两个属性联系在一起了。因此,idx可以理解为结点。

idx相当于一个分配器,如果需要加入新的结点就用++idx分配出一个下标,输入字符串的总长度不超过 1 0 5 10^5 105,因此最多会用到 1 0 5 10^5 105个idx。

Trie树中有个二维数组 son[N][26],表示当前结点的儿子,如果没有的话,可以等于++idx。Trie树本质上是一颗多叉树,对于字母而言最多有26个子结点。所以这个数组包含了两条信息。比如:son[1][0]=2表示1结点的一个值为a的子结点为结点2;如果son[1][0] = 0,则意味着没有值为a子结点。这里的son[N][26]相当于链表中的ne[N]。当然这里2仅仅是一个节点的编号而已。

参考:https://www.acwing.com/solution/content/5673/

模板总结

int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量

// 插入一个字符串
void insert(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        if (!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
    cnt[p] ++ ;
}

// 查询字符串出现的次数
int query(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        if (!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

应用 最大异或对

在给定的 N个整数 A 1 A_1 A1 A 2 A_2 A2…… A N A_N AN 中选出两个进行 x o r xor xor(异或)运算(一般异或运算是按位计算的),得到的结果最大是多少?

输入格式

第一行输入一个整数 N。

第二行输入 N 个整数 A 1 A_1 A1 A N A_N AN

输出格式

输出一个整数表示答案。

数据范围

1 ≤ N ≤ 1 0 5 1≤N≤10^5 1N105
0 ≤ A i < 2 31 0≤A_i<2^{31} 0Ai<231

输入样例:

3
1 2 3

输出样例:

3

分析

首先是暴力做法BF O ( n 2 ) O(n^2) O(n2)

for (int i = 0; i < n; i++)
{
    for (int j = 0; j < i; j++)
    {
        // 但其实 a[i] ^ a[j] == a[j] ^ a[i], 所以内层循环 j < i 
        // 因为 a[i] ^ a[i] == 0 所以事先把返回值初始化成0 不用判断相等的情况
    }
}

异或也可以理解为不进位加法,相同的话异或值为0。Trie树不仅可以存储整数,也可以存储二进制数。而计算机中所有文件都是以二进制的形式保存的,换句话说Trie数可以存储任何文件。异或后最大,这需要寻找出与原数每位不同的数,为保证最大值,需要从最高位开始依次寻找,过程如下所示:

可以不用先全部插入,因为这是有顺序的,避免多次枚举 a j a_j aj a i a_i ai 以及 a i a_i ai a j a_j aj 的情况。因此可以先查找再插入(可能最开始的情况下要写一个特判,因为最开始没有可以查找的内容),当然也可以先插入再查找(可能存在的问题就是每次自己和自己异或是0,没有意义)。

#include <iostream>
#include <algorithm>

using namespace std;
// N是整数个数,M是树的总宽度
const int N = 100010, M = 3100010;

int n;
int a[N], son[M][2], idx;

void insert(int x)
{
    int p = 0;
    for (int i = 30; i >= 0; i -- )
    {
        // 从高到低依次取每一位
        int u = x >> i & 1;
        // 没有该节点则插入该节点
        if (!son[p][u]) son[p][u] = ++ idx;
        // 指针指向下一层
        p = son[p][u];
    }
}

int query(int x)
{
    int p = 0, res = 0;
    for (int i = 30; i >= 0; i -- )
    {
        // 从最大位开始找
        int u = x >> i & 1;
        // 如果当前层有对应的不相同的数,p指针就指到不同数的地址
        if (son[p][!u])
        {
            p = son[p][!u];
            // 因为这一位不同,异或后为1,这里向前移位并且保留相反数即可。
            res = res * 2 + !u;
        }
        else 
        {
            p = son[p][u];
            // 如果没有相异的数,则只能向前移一位然后保留该数即可。
            res = res * 2 + u;
        }
    }
    return res;
}

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);

    int res = 0;
    for (int i = 0; i < n; i ++ ) 
    {
        insert(a[i]);
        int t = query(a[i]);
        // 最后再进行异或处理
        res = max(res, a[i] ^ t);
    }

    printf("%d\n", res);

    return 0;
}

同时,这里关于代码有两个思路,一个是上面这种query需要寻找的对应的异或的整数,最后 max(res, a[i] ^ t) 得到结果。

此外还可以直接在 query 中提前进行比较计算,最后直接比较结果即可 max(res, t),过程如下:

int query(int x)
{
    int p = 0, res = 0;
    for (int i = 30; i >= 0; i -- )
    {
        // 从最大位开始找
        int u = x >> i & 1;
        // 如果当前层有对应的不相同的数,p指针就指到不同数的地址
        if (son[p][!u])
        {
            p = son[p][!u];
            // 因为这一位不同,异或后为1,只需要向前移并且加1即可
            res = res * 2 + 1;
        }
        else 
        {
            p = son[p][u];
            // 这一位相同,xor后为0,向前移一位然后置0即可。
            res = res * 2 + 0;
        }
    }
    return res;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/634506.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

初探BERTPre-trainSelf-supervise

初探Bert 因为一次偶然的原因&#xff0c;自己有再次对Bert有了一个更深层地了解&#xff0c;特别是对预训练这个概念&#xff0c;首先说明&#xff0c;自己是看了李宏毅老师的讲解&#xff0c;这里只是尝试进行简单的总结复述并加一些自己的看法。 说Bert之前不得不说现在的…

ansible远程执行指令,/bin/sh: java: command not foundnon-zero return code

问题描述&#xff1a;ansible远程执行指令&#xff0c;初选指令加载不全&#xff0c; [rootVM-0-6-centos ~]# ansible all -m shell -a "java -version" 10.206.0.15 | FAILED | rc127 >> /bin/sh: java: command not foundnon-zero return code 解决方案&a…

C++(8):IO 库

IO 类 IO 库类型和头文件 iostream 定义了用于读写流的基本类型&#xff0c;fstream 定义了读写命名文件的类型&#xff0c;sstream 定义了读写内存 string 对象的类型。 其中带 w 前缀的类型用来操作宽字符语言 (wchar_t)。宽字符版本的类型和函数前都有一个 w&#xff0c;如…

SAP从入门到放弃系列之PP/DS-part1

翻译一篇大佬文章&#xff0c;了解一下PPDS前世今生和产品功能出现的业务背景。虽然是15年的&#xff0c;但经典永流传~~~&#xff0c;感谢大佬的文章。 原文地址&#xff1a; #S4HANA 1610 use case series: 9a – Production Planning and Detailed Scheduling – PP/DS (b…

【MySQL学习笔记】子查询与联结(连接)

1.子查询 将一条select语句返回的结果用于另一条select语句的where子句中。 执行时&#xff0c;先执行子查询&#xff0c;再执行主查询。 select sid from sc where cid in (select cid from course where cname数据库应用技术);子查询一般与 IN 操作符结合使用&#xff0…

《微服务实战》 第三十二章 微服务链路跟踪-sleuth zipkin

前言 大型分布式微服务系统中&#xff0c;一个系统被拆分成N多个模块&#xff0c;这些模块负责不同的功能&#xff0c;组合成一套系统&#xff0c;最终可以提供丰富的功能。在这种分布式架构中&#xff0c;一次请求往往需要涉及到多个服务服务之间的调用错综复杂&#xff0c;对…

Lenovo Yoga-710-14IKB电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。&#xff08;下载请直接百度黑果魏叔&#xff09; 硬件配置 硬件型号驱动情况 主板Lenovo Yoga 710 (14") - 14IKB (without dGPU) 处理器Intel i5-7200U (4) 2.50GHz (IKBL)已驱动 内存48 GB ( 海盗船 DDR4 3200…

web worker创建多个 JavaScript 线程 (使用GTP写的文章)

前言 最近在优化公司的一个项目&#xff0c;使用的就是web worker去优化&#xff0c;做了那些优化&#xff0c;一个是状态的优化&#xff0c;&#xff08;通信的状态实时更新&#xff0c;以前的做法是做个定时任务实时获取它的状态&#xff0c;然后让它在页面渲染&#xff0c;这…

【Linux】 -- TCP协议 (一)

TCP协议 Tcp协议可靠性冯诺依曼体系结构 TCP的协议格式序号与确认序号窗口大小六个标志位 确认应答机制 &#xff08;ACK&#xff09;超时重传机制连接管理机制 Tcp协议 TCP全称为 “传输控制协议”&#xff08;Transmission Control Protocol&#xff09; TCP协议被广泛应用…

[linux_C语言_udp的多种实现方法及网络调试中遇到的问题]

linux_C语言_udp的多种实现方法 最基本的方式(不用组播不用sigio信号不使能广播属性)接收端发送端 使用SIGIO信号的方式(使用sigio信号使用广播使能属性)服务端客户端 使用组播模式服务端客户端 tcp和udp的使用区别调试中遇到的问题所有源码下载点这~~ 最基本的方式(不用组播不…

Unix/Linux编程:UDS 流(Stream)

〇、前言 socket 是一种 IPC &#xff08;Inter-Process Communication&#xff0c;进程间通信&#xff09;方法&#xff0c;它允许位于同一主机&#xff08;计算机&#xff09;或使用网络连接起来的不同主机上的应用程序之间交换数据。通过使用Socket&#xff0c;开发人员可以…

【C++】——栈和队列(stack、queue)及优先队列(priority_queue)的介绍和模拟实现

文章目录 1. 前言2. 容器适配器2.1 容器适配器的介绍2.2 STL标准库中stack和queue的底层结构2.3 deque的简单介绍2.4 deque的缺陷2.5 为什么选择deque作为stack和queue的底层默认容器 3. stack3.1 stack的介绍3.2 stack的使用3.3 stack模拟实现 4. queue4.1 queue的介绍4.2 que…

数据分布——长尾分布的处理

前言 长尾分布在分类任务中会提到这个名,这是因为长尾分布这个现象问题会导致在训练过程中会出现出错率高的问题&#xff0c;影响了实验结果。 这里要说的是&#xff0c;长尾分布是一种现象&#xff0c;有的地方说是一种理论或定律&#xff0c;我感觉这样说不太确切&#xff0…

取石子游戏——算法与编程

取石子游戏 目录 问题描述输入输出格式输入格式&#xff1a;输出格式&#xff1a; 输入输出样例输入样例#1&#xff1a;输出样例#1&#xff1a;提示信息 算法尼姆博奕 代码 问题描述 A l i c e Alice Alice和 B o b Bob Bob在玩取石子游戏&#xff0c;摆在他们面前的有 n n n堆…

GIS入门进阶之012

一、引言 空间数据可视化是有效传输与表达地理信息&#xff0c;挖掘空间数据之间的内在联系&#xff0c;揭示地理现象内在规律的重要手段。它通过运用地图学、计算机图形学和图像处理技术&#xff0c;将地学信息的输入、处理、查询、分析与预测的结果采用符号、图形、图像并结合…

OpenGL 材质实现

1.简介 在现实世界里&#xff0c;每个物体会对光产生不同的反应。比如&#xff0c;钢制物体看起来通常会比陶土花瓶更闪闪发光&#xff0c;一个木头箱子也不会与一个钢制箱子反射同样程度的光。有些物体反射光的时候不会有太多的散射&#xff0c;因而产生较小的高光点&#xf…

35岁被淘汰?软件测试工程师职业生涯规划,从技术到管理...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 入门阶段&#xf…

Parallel Desktop中按照的centos在切换root用户时,密码正确,但一直切换不成功,显示su: Authentication failure

目录 一、出现问题二、分析问题三、解决问题四、参考资料 一、出现问题 我的密码明明是输入正确的&#xff0c;但又一直给我报下面的错误 二、分析问题 我怀疑是我密码记错了&#xff0c;所以我点击Log Out&#xff0c;重新去输入了一下密码&#xff0c;发现是正确的我确认…

[学习笔记] [机器学习] 9. 朴素贝叶斯(概率基础、联合概率、条件概率、贝叶斯公式、情感分析)

视频链接数据集下载地址&#xff1a;无需下载 学习目标&#xff1a; 4. 说明条件概率与联合概率 5. 说明贝叶斯公式、以及特征独立的关系 6. 记忆贝叶斯公式 7. 知道拉普拉斯平滑系数 8. 应用贝叶斯公式实现概率的计算 9. 会使用朴素贝叶斯对商品评论进行情感分析 1. 朴素贝叶…

对象进阶-继承、原型-原型链

工厂方法创建对象 我们之前已经学习了如何创建一个对象&#xff0c;那我们要是想要创建多个对象又该怎么办&#xff1f;聪明的同学可能会说&#xff0c;直接在写几个对象不就好了吗&#xff1f;比如下边的代码&#xff1a; var person1 {name: "孙悟空",age: 18,s…