news 2026/2/24 22:04:31

算法基础-字典树

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
算法基础-字典树

1.字典树的概念

Trie 树⼜叫字典树或前缀树,是⼀种能够快速插⼊查询字符串的数据结构。它利⽤字符串的公共前 缀,将字符串组织成⼀棵树形结构,从⽽⼤ 提⾼了存储以及查找效率。
我们可以把字典树想象成⼀棵多叉树,每⼀条边代表⼀个字符,从根节点到某个节点的路径就代表了
⼀个字符串。例如,要存储"abc""abd""acde"以及"cd"时,构建的字典树如下:


2.字典树的作⽤

当我们在字典树的每⼀个结点位置,额外维护⼀些信息时,就可以做到很多事情:
• 查询某个单词是否出现过,并且出现⼏次;
• 查询有多少个单词是以某个字符串为前缀;
• 查询所有以某个前缀开头的单词;(这个作⽤可以⽤到输⼊法中,输⼊拼⾳的时候,可以提⽰可能 的单词)
当然,除了上述作⽤以外,字典树还可以解决别的问题,后续可以在做题中体会。

3.字典树的实现

实现⼀个能够查询单词出现次数以及查询有多少个单词是以某个字符串为前缀的字典树,默认全是⼩ 写字⺟。

准备⼯作:

#include <iostream> #include <cstring> using namespace std; const int N = 1e6 + 10; int tree[N][26], p[N], e[N]; int idx;

插⼊字符串:

void insert(string& s) { int cur = 0; // 从根结点开始 p[cur]++; // 这个格⼦经过⼀次 for(auto ch : s) { int path = ch - 'a'; // 如果没有路 if(tree[cur][path] == 0) tree[cur][path] = ++idx; cur = tree[cur][path]; p[cur]++; } e[cur]++; }

查询字符串出现的次数:

int find(string& s) { int cur = 0; for(auto ch : s) { int path = ch - 'a'; if(tree[cur][path] == 0) return 0; cur = tree[cur][path]; } return e[cur]; }

查询有多少个单词以字符串s为前缀:

int find_pre(string& s) { int cur = 0; for(auto ch : s) { int path = get_num(ch); if(tree[cur][path] == 0) return 0; cur = tree[cur][path]; } return p[cur]; }

4.1【模板】字典树

题⽬来源: 洛⾕
题⽬链接:P8306 【模板】字典树
难度系数: ★★

题目描述

给定 n 个模式串 s1​,s2​,…,sn​ 和 q 次询问,每次询问给定一个文本串 ti​,请回答 s1​∼sn​ 中有多少个字符串 sj​ 满足 ti​ 是 sj​ 的前缀

一个字符串 t 是 s 的前缀当且仅当从 s 的末尾删去若干个(可以为 0 个)连续的字符后与 t 相同。

输入的字符串大小敏感。例如,字符串Fusu和字符串fusu不同。

输入格式

本题单测试点内有多组测试数据

输入的第一行是一个整数,表示数据组数 T。

对于每组数据,格式如下:
第一行是两个整数,分别表示模式串的个数 n 和询问的个数 q。
接下来 n 行,每行一个字符串,表示一个模式串。
接下来 q 行,每行一个字符串,表示一次询问。

输出格式

按照输入的顺序依次输出各测试数据的答案。
对于每次询问,输出一行一个整数表示答案。

输入输出样例

输入 #1复制

3 3 3 fusufusu fusu anguei fusu anguei kkksc 5 2 fusu Fusu AFakeFusu afakefusu fusuisnotfake Fusu fusu 1 1 998244353 9

输出 #1复制

2 1 0 1 2 1

说明/提示

数据规模与约定

对于全部的测试点,保证 1≤T,n,q≤105,且输入字符串的总长度不超过 3×106。输入的字符串只含大小写字母和数字,且不含空串。

说明

std 的 IO 使用的是关闭同步后的 cin/cout,本题不卡常。


【解法】

模板题,仅需维护⼀个 p 数组,查询多少前缀即可

【参考代码】

#include <iostream> #include <string> #include <cstring> using namespace std; const int N = 3e6 + 10; // 字典树节点最大数量(根据数据范围设定) int tr[N][62]; // 字典树:tr[节点][字符编号] = 子节点编号 int p[N]; // p[节点]:以该节点为前缀的模式串数量 int idx; // 字典树节点计数器(初始为0,根节点是0) // 将字符映射为0-61的编号(区分大小写字母+数字) int get_num(char ch) { if (ch >= 'a' && ch <= 'z') { return ch - 'a'; // 小写字母:0-25 } else if (ch >= 'A' && ch <= 'Z') { return ch - 'A' + 26; // 大写字母:26-51 } else { return ch - '0' + 52; // 数字:52-61 } } // 插入模式串到字典树中 void insert(string& s) { int cur = 0; // 从根节点开始 p[cur]++; // 根节点是所有字符串的前缀,计数+1 for (char ch : s) { int path = get_num(ch); // 获取当前字符的编号 if (tr[cur][path] == 0) { // 若该字符的子节点不存在,新建节点 tr[cur][path] = ++idx; } cur = tr[cur][path]; // 移动到子节点 p[cur]++; // 该节点的前缀计数+1 } } // 查询文本串s作为前缀的模式串数量 int find_pre(string& s) { int cur = 0; // 从根节点开始 for (char ch : s) { int path = get_num(ch); if (tr[cur][path] == 0) { // 路径中断,无匹配的前缀 return 0; } cur = tr[cur][path]; // 移动到子节点 } return p[cur]; // 返回最终节点的前缀计数 } int main() { ios::sync_with_stdio(false); // 加速cin/cout cin.tie(nullptr); int T; cin >> T; while (T--) { // 处理多组测试数据 // 清空字典树(只清空用到的节点,避免memset超时) for (int i = 0; i <= idx; i++) { memset(tr[i], 0, sizeof(tr[i])); // 清空当前节点的所有子节点 p[i] = 0; // 清空计数 } idx = 0; // 重置节点计数器 int n, q; cin >> n >> q; // 插入n个模式串 while (n--) { string s; cin >> s; insert(s); } // 处理q次查询 while (q--) { string s; cin >> s; cout << find_pre(s) << '\n'; } } return 0; }

4.2于是他错误的点名开始了

题⽬来源: 洛⾕
题⽬链接:P2580 于是他错误的点名开始了
难度系数: ★★

题目背景

XS中学化学竞赛组教练是一个酷爱炉石的人。

他会一边搓炉石一边点名以至于有一天他连续点到了某个同学两次,然后正好被路过的校长发现了然后就是一顿欧拉欧拉欧拉(详情请见已结束比赛 CON900)。

题目描述

这之后校长任命你为特派探员,每天记录他的点名。校长会提供化学竞赛学生的人数和名单,而你需要告诉校长他有没有点错名。(为什么不直接不让他玩炉石。)

输入格式

第一行一个整数 n,表示班上人数。

接下来 n 行,每行一个字符串表示其名字(互不相同,且只含小写字母,长度不超过 50)。

第 n+2 行一个整数 m,表示教练报的名字个数。

接下来 m 行,每行一个字符串表示教练报的名字(只含小写字母,且长度不超过 50)。

输出格式

对于每个教练报的名字,输出一行。

如果该名字正确且是第一次出现,输出OK,如果该名字错误,输出WRONG,如果该名字正确但不是第一次出现,输出REPEAT

输入输出样例

输入 #1复制

5 a b c ad acd 3 a a e

输出 #1复制

OK REPEAT WRONG

说明/提示

  • 对于 40% 的数据,n≤1000,m≤2000。
  • 对于 70% 的数据,n≤104,m≤2×104。
  • 对于 100% 的数据,n≤104,m≤105。

upd 2022.7.30:新增加一组 Hack 数据。


【解法】

⽤字典树维护字符串,当查询之后,将 e 数组修改成 -1 即可

【参考代码】

#include <iostream> #include <string> using namespace std; // 字典树最大节点数(根据实际数据量调整,5e5足够应对大部分场景) const int N = 5e5 + 10; // n:学生名字数量;m:点名查询次数 int n, m; // tr[i][j]:第i个节点的第j个小写字母(0-25)指向的子节点编号 // e[i]:节点状态(0=非名字结尾;>0=名字结尾且未查询;-1=名字结尾且已查询) int tr[N][26], e[N], idx; // 插入名字到字典树 void insert(string& s) { int cur = 0; // 从根节点(编号0)开始 for (auto ch : s) { // 遍历名字的每个字符 int path = ch - 'a'; // 小写字母映射为0-25的路径 if (tr[cur][path] == 0) { // 路径未创建则新建节点 tr[cur][path] = ++idx; } cur = tr[cur][path]; // 移动到子节点 } e[cur]++; // 标记该节点为名字结尾(初始为未查询状态) } // 查询名字状态:0=不存在;>0=首次查询;-1=重复查询 int find(string& s) { int cur = 0; // 从根节点开始 for (auto ch : s) { // 遍历查询名字的每个字符 int path = ch - 'a'; if (tr[cur][path] == 0) { // 路径不存在,名字不在名单中 return 0; } cur = tr[cur][path]; // 移动到子节点 } // 路径存在,判断节点状态 if (e[cur] > 0) { int t = e[cur]; e[cur] = -1; // 标记为已查询 return t; } return e[cur]; // 返回-1(重复查询) } int main() { // 优化输入输出速度(大数据量必备) ios::sync_with_stdio(false); cin.tie(nullptr); // 输入学生名字数量并插入所有名字 cin >> n; while (n--) { string s; cin >> s; insert(s); } // 输入查询次数并处理所有点名查询 cin >> m; while (m--) { string s; cin >> s; int ret = find(s); if (ret == 0) { cout << "WRONG" << endl; } else if (ret > 0) { cout << "OK" << endl; } else { cout << "REPEAT" << endl; } } return 0; }

4.3最⼤异或对

题⽬来源: 洛⾕
题⽬链接:P10471 最⼤异或对 The XOR Largest Pair
难度系数: ★★★

题目描述

给定 N 个整数 A1​.A2​,⋯,AN​ 中选出两个进行异或计算,得到的结果最大是多少?

输入格式

第一行一个整数 N,第二行 N 个整数 A1​.A2​,⋯,AN​。

输出格式

一个整数表示答案。

输入输出样例

输入 #1复制

3 1 2 3

输出 #1复制

3

说明/提示

对于所有测试数据,1≤N≤105,保证 0≤Ai​<231。


【解法】

将所有数按照⼆进制位存在字典树中,针对每⼀个数x,按照⼆进制匹配最优的⽅案。

【参考代码】

#include <iostream> using namespace std; const int N = 1e5 + 10; int n; int a[N]; // 字典树:每个数32位,1e5个数最多需要1e5*32个节点 int tr[N * 32][2], idx; // 插入数字x的二进制到字典树(从高位到低位) void insert(int x) { int cur = 0; for (int i = 31; i >= 0; i--) { // 取出x的第i位二进制值(0/1) int path = ((x >> i) & 1); // 路径不存在则新建节点 if (tr[cur][path] == 0) tr[cur][path] = ++idx; // 移动到子节点 cur = tr[cur][path]; } } // 找和x异或最大的数,返回异或结果 int find(int x) { int cur = 0; int ret = 0; for (int i = 31; i >= 0; i--) { int path = ((x >> i) & 1); // 优先走相反的位(贪心) if (tr[cur][path ^ 1]) { ret |= (1 << i); // 异或结果的第i位设为1 cur = tr[cur][path ^ 1]; } else { // 无相反位,走相同位 cur = tr[cur][path]; } } return ret; } int main() { cin >> n; for (int i = 1; i <= n; i++) { cin >> a[i]; insert(a[i]); } int ret = 0; for (int i = 1; i <= n; i++) { ret = max(ret, find(a[i])); } cout << ret << endl; return 0; }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/15 22:38:48

在 React 中实现数学公式显示:使用 KaTeX 和 react-katex

在 React 中实现数学公式显示&#xff1a;使用 KaTeX 和 react-katex 前言 在 Web 应用中显示数学公式一直是一个挑战。传统的图片方式不够灵活&#xff0c;而使用 LaTeX 渲染引擎可以在浏览器中直接渲染高质量的数学公式。本文将介绍如何在 React 项目中使用 react-katex 和 …

作者头像 李华
网站建设 2026/2/23 21:44:00

Langflow源码架构解析:前后端技术拆解

Langflow源码架构解析&#xff1a;前后端技术拆解 在AI应用开发日益复杂的今天&#xff0c;LangChain虽然为构建智能体和语言模型工作流提供了强大支持&#xff0c;但其代码驱动的开发模式对新手并不友好。正是在这种背景下&#xff0c;Langflow 应运而生——一个通过拖拽节点…

作者头像 李华
网站建设 2026/2/24 1:53:06

YOLOv5车辆与车牌识别全功能实现

YOLOv5车辆与车牌识别全功能实现 在智能交通系统快速演进的今天&#xff0c;如何让摄像头“看懂”车流、自动识别违章行为&#xff0c;已成为城市治理和园区管理的关键需求。传统方案依赖多模块拼接&#xff0c;稳定性差、延迟高&#xff1b;而我们这套基于YOLOv5构建的端到端…

作者头像 李华
网站建设 2026/2/17 18:22:59

Linly-Talker TTS:如何实现媲美真人的语音合成

Linly-Talker TTS&#xff1a;如何实现媲美真人的语音合成 在虚拟主播24小时不间断带货、AI客服秒回千条咨询的今天&#xff0c;我们早已习惯“会说话”的机器。但真正让人停下脚步的&#xff0c;不是它说了什么&#xff0c;而是——它是怎么“说”的。 一句“欢迎光临”&#…

作者头像 李华
网站建设 2026/2/20 11:55:00

国内首个开源 MCP 协议 + 计费闭环 AI 平台诞生了!

大家好&#xff0c;我是小金。 现在市面上像 Dify、Coze 这样优秀的 Agent 编排平台已经很多了&#xff0c;捏一个好用的 AI 智能体其实并不难。 但很多想做独立开发或者搞 AI 创业的朋友都会卡在一个更现实的问题上&#xff1a;智能体捏好了&#xff0c;怎么把它变成一个独立…

作者头像 李华
网站建设 2026/2/22 0:47:47

最强AI生图工具 Nano Banana Pro 玩法合集:解锁生产力极限

Nano Banana Pro&#xff08;NBP&#xff09;的优势在于它将世界知识、语言理解、图像合成和专业编辑融为一体。玩转 NBP&#xff0c;关键在于从“描述图像”升级到“指令设计”。 一、专业创作核心玩法&#xff1a;突破文字与视觉的鸿沟 NBP 解决了困扰所有 AI 生图工具的文…

作者头像 李华