蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:字典树前缀树与单词检索统计

一、引言

最近赛事密集,正是刷题备战的好窗口:

  • 第十八届蓝桥杯软件赛 (C/C++ 程序设计、Python 程序设计)报名已于 2026-10-08 10:00---2026-12-18 14:00 开启,省赛定在 2027-04-10 9:00--13:00 ,全国总决赛 2027-06-12;组委会还同步推出了"全国高校备赛季"活动(备赛手册、免费模拟赛、每日一题)。
  • 中国电子学会 2026 年 12 月软件编程等级考试 (C/C++ 一级至十级、Python 一级至六级)报名 2026-09-23---12-02 16:00(即将截止) ,全国测试 12-10 、统考 12-12(C/C++ 8:30--10:30、Python 14:00--15:00)。
  • 全国青少年信息素养大赛·算法应用主题赛初赛考点大纲(参照电子学会《青少年软件编程等级评价指南》)明确把"字符串"作为独立章节,复决赛还包含编程操作题。

字符串处理是这三场考试的公共高频考点。今天我们就讲字符串处理的"地基"结构------字典树(Trie,又称前缀树),用一道原创题把它吃透,并给出 C++ 与 Python 双版本。

二、题目 / 项目目标

原创题:校园智能单词本

学校要做一个背单词小程序,需要支持三种操作(所有单词仅由小写字母组成):

  • 任务 A(插入):把一个个单词加入词典(可重复插入,重复则计数 +1)。
  • 任务 B(精确查询):给定一个单词,判断它是否在词典中出现过。
  • 任务 C(前缀统计 / 前缀列举):给定一个前缀字符串,统计(或列举)词典中以该前缀开头的单词数量。

示例:依次插入 apple、app、apply、bat、batch。

  • 查询 app → 在词典中(出现过 1 次)。

  • 前缀统计 app → 3(apple、app、apply 均以 app 开头)。

  • 前缀列举 app → app、apple、apply。

用哈希表能做 A、B,但前缀类问题(C)它做不了------这正是字典树的用武之地。

三、核心考点

  1. 字符串多叉树建模:把每个单词的字符依次挂成一棵树,从根到某一节点路径上的字符拼起来就是前缀。
  2. 两种计数 :cnt(以某节点结尾的单词数,用于精确匹配)与 pass(经过某节点的单词数,用于前缀统计)。
  3. 字符到子节点下标的映射 :小写字母用 c - 'a' 映射到 0--25;更通用的场景用 map / 字典存子节点。
  4. 时间复杂度 O(单词长度):每次插入 / 查询只沿树走一条长度为词长的链。
  5. 与哈希表、集合的取舍:字典树胜在天然支持"前缀",哈希表胜在平均 O(1) 但无前缀能力。

四、解法 / 拆解

4.1 思路

  • 每个节点挂 26 个(或若干个)子节点指针,外加两个计数器。
  • 插入 :从根出发,逐字符往下走,每经过一个节点 pass++;走到词尾节点 cnt++。
  • 精确查询 :沿词走,中途断链即返回 false;走到词尾看 cnt > 0。
  • 前缀统计 :沿前缀走,中途断链返回 0;走完返回该节点的 pass。
  • 前缀列举(任务 C 进阶) :先走到前缀末端节点,再 DFS 回溯所有 cnt > 0 的节点,沿途拼出单词。

4.2 C++ 实现(数组版,速度快、适合竞赛)

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
using namespace std;

const int ALPH = 26;

struct TrieNode {
    TrieNode* son[ALPH];
    int cnt;   // 以此节点结尾的单词个数(精确匹配)
    int pass;  // 经过此节点的单词个数(前缀统计)
    TrieNode() : cnt(0), pass(0) {
        for (int i = 0; i < ALPH; i++) son[i] = nullptr;
    }
};

struct Trie {
    TrieNode* root;
    Trie() { root = new TrieNode(); }

    // 任务A:插入单词(可重复)
    void insert(const string& w) {
        root->pass++;              // 空串(根)也代表一个前缀,先 +1
        TrieNode* p = root;
        for (char c : w) {
            int idx = c - 'a';
            if (!p->son[idx]) p->son[idx] = new TrieNode();
            p = p->son[idx];
            p->pass++;
        }
        p->cnt++;
    }

    // 任务B:精确查询单词是否存在
    bool search(const string& w) {
        TrieNode* p = root;
        for (char c : w) {
            int idx = c - 'a';
            if (!p->son[idx]) return false;
            p = p->son[idx];
        }
        return p->cnt > 0;
    }

    // 任务C:统计以 pre 为前缀的单词数量
    int prefixCount(const string& pre) {
        TrieNode* p = root;
        for (char c : pre) {
            int idx = c - 'a';
            if (!p->son[idx]) return 0;
            p = p->son[idx];
        }
        return p->pass;
    }
};

// 使用示例
int main() {
    Trie t;
    t.insert("apple"); t.insert("app"); t.insert("apply");
    t.insert("bat");   t.insert("batch");
    cout << boolalpha;
    cout << t.search("app") << "\n";   // true
    cout << t.search("ap")  << "\n";   // false(app 是前缀,不是完整单词)
    cout << t.prefixCount("app") << "\n"; // 3
    cout << t.prefixCount("ba")  << "\n"; // 2
    return 0;
}

提示:search("ap") 返回 false 但 prefixCount("ap") 返回 3------这正是 cnt 与 pass 的区别,也是新手最容易踩的坑。

4.3 Python 实现(字典版,省空间、写法直观)

python 复制代码
class TrieNode:
    __slots__ = ('son', 'cnt', 'passcnt')
    def __init__(self):
        self.son = {}        # 子节点:字符 -> TrieNode
        self.cnt = 0         # 以此节点结尾的单词个数
        self.passcnt = 0     # 经过此节点的单词个数

class Trie:
    def __init__(self):
        self.root = TrieNode()

    # 任务A:插入单词(可重复)
    def insert(self, w: str):
        self.root.passcnt += 1   # 空串(根)也代表一个前缀,先 +1
        p = self.root
        for ch in w:
            if ch not in p.son:
                p.son[ch] = TrieNode()
            p = p.son[ch]
            p.passcnt += 1
        p.cnt += 1

    # 任务B:精确查询
    def search(self, w: str) -> bool:
        p = self.root
        for ch in w:
            if ch not in p.son:
                return False
            p = p.son[ch]
        return p.cnt > 0

    # 任务C:前缀统计
    def prefix_count(self, pre: str) -> int:
        p = self.root
        for ch in pre:
            if ch not in p.son:
                return 0
            p = p.son[ch]
        return p.passcnt

    # 任务C进阶:列举所有以 pre 为前缀的单词
    def collect(self, pre: str):
        p = self.root
        for ch in pre:
            if ch not in p.son:
                return []
            p = p.son[ch]
        res = []
        self._dfs(p, pre, res)
        return res

    def _dfs(self, node, path, res):
        if node.cnt > 0:
            res.append(path)
        for ch, nxt in node.son.items():
            self._dfs(nxt, path + ch, res)


# 使用示例
t = Trie()
for w in ["apple", "app", "apply", "bat", "batch"]:
    t.insert(w)
print(t.search("app"))        # True
print(t.search("ap"))         # False
print(t.prefix_count("app"))  # 3
print(t.collect("app"))       # ['app', 'apple', 'apply']

五、时间 / 空间复杂度

操作 时间复杂度 说明
插入 insert O(L) L 为单词长度,只走一条链
精确查询 search O(L) 同上
前缀统计 prefixCount O(L) 同上
前缀列举 collect O(L + K) K 为匹配单词总长度(最坏遍历整棵子树)

空间复杂度 :节点总数不超过 所有单词字符数之和,即 O(ΣL)。数组版每个节点固定 26 个指针(空间略大但访问极快),字典版按需开节点(稀疏时更省)。

六、七个易错点

  1. 字符映射范围 :c - 'a' 只适用小写字母。含大写、数字、中文时必须改用 map / 字典,或先统一 tolower 再插入,否则下标越界或漏匹配。
  2. cnt 与 pass 混淆 :cnt 只在词尾 节点 +1(回答"是否完整出现过");pass 在每个经过 节点 +1(回答"前缀有多少")。search 看 cnt,prefixCount 看 pass,千万别看反。
  3. 前缀≠单词 :prefixCount("ap")=3 但 search("ap")=false------"ap" 只是别人前缀,自己从没被插入。判题时看清要的是"前缀数量"还是"完整单词存在"。
  4. 空串 / 空前缀 :prefixCount("") 返回根节点的 pass,即总单词数(含重复),这往往是题目的隐藏用例。
  5. 节点存储方式选型 :海量"短而密"的单词用数组 son[26] 更快;海量"长而稀"或多语种字符用 map / 字典更省内存,但常数更大。
  6. 多测 / 多组数据未清空 :数组指针版多次使用要整树 delete 重建(或加 used 标记重置),否则上一组的数据会串进来。Python 字典版直接新建 Trie() 即可。
  7. 指针空 / 递归深度 :C++ 访问子节点前必须判 nullptr;Python 用递归 collect 时,极端长单词可能触发递归深度限制,可改成显式栈(迭代)遍历。

七、进阶

  1. 01-Trie 与最大异或数对 :把每个数的二进制位当字符建 Trie,能 O(位数) 求"与某数异或最大"的数------本号此前位运算篇(10-26)已详细拆解。
  2. 最长公共前缀(多词版):多单词的最长公共前缀,等价于在 Trie 上从根往下走,直到出现"分叉"(某节点有两个及以上非空子节点)或"到达某个单词结尾"为止。
  3. 字典树排序 / 去重:按字典序对 Trie 做 DFS 遍历,天然得到有序、去重的单词列表,比先排序再去重更优雅。
  4. 与哈希表 / 集合对比 :哈希表平均 O(1) 查询但无法回答前缀类问题;Trie 是 O(L) 且天然支持前缀------凡是"找以 xxx 开头"的题,优先想 Trie。
  5. AC 自动机(多模式匹配) :本质就是在 Trie 上为每个节点补一条 fail 指针,把多模式串匹配变成一次文本遍历------本号AC 自动机篇(10-05)有完整讲解,Trie 就是它的底座。
  6. 可持久化 Trie:给 Trie 的每个版本"快照"复用未修改分支,可用于"区间内异或最值"等离线 / 在线问题(对应可持久化线段树思想)。

八、小结与互动

字典树是字符串处理的"地基"结构:用 O(L) 的插入 / 查询,换来了哈希表给不了的前缀能力 。记住两个计数 cnt(结尾)与 pass(路过),就能稳稳拿下精确查询、前缀统计、前缀列举三连击。它在蓝桥杯字符串题、电子学会考级字符串章节、信息素养大赛算法应用赛项里都频繁出场,务必练熟。

动手练一练 :在上面的 Trie 加一个 delete(w)(删除单词,注意 cnt 与 pass 都要正确 −1,且可释放孤儿子树),再想想"统计词典里有多少个单词恰好是另一个单词的前缀"该怎么一句话算出来?欢迎在评论区贴出你的实现~

如果觉得有用,点个关注,后续继续拆解蓝桥杯 / 信息素养 / 电子学会考级的高频算法考点。


📚 免费少儿编程资料(夸克网盘领取)

以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:

  1. 全国青少年信息素养大赛复赛集训题目Python&C++.docx
    https://pan.quark.cn/s/93995d3cb150
  2. 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
    https://pan.quark.cn/s/da97b5dbf75d
  3. Python背记手册.pdf
    https://pan.quark.cn/s/7568ae9ca92b
  4. Python课程
    https://pan.quark.cn/s/a94bf02d00c6
  5. 2024信息素养大赛图形化复赛集训题答案3-9
    https://pan.quark.cn/s/6ccab7ec3cbc
  6. 2025年03月份电子学会考级真题
    https://pan.quark.cn/s/4403c4228912
  7. 2025全国青少年信息素养大赛赛项说明
    https://pan.quark.cn/s/d9d0df4a9f29
  8. 青少儿信息素养大赛编程资料
    https://pan.quark.cn/s/4ab6bd83be8a

资料持续更新,关注获取最新分享。

相关推荐
田里的水稻1 小时前
FA_CNN卷积神经网络十八---感知机到多层感知机PyTorch
人工智能·pytorch·python
Nuanyt1 小时前
LeetCode Hot 100 链表专题2 3 题 (Java)
java·笔记·算法·leetcode·链表
終不似少年遊*1 小时前
【Skill】为中型研发团队制作的敏捷迭代AI副驾Skill
人工智能·python·华为云·agent·ai编程·skill
呆萌的代Ma1 小时前
python使用ThreadPoolExecutor自带队列将多线程变串行执行
python
echome8881 小时前
Python 正则表达式:5 个日常够用的模式
python·正则表达式
海绵宝宝转agent2 小时前
LeetCodehot100题解
java·笔记·学习·算法·leetcode·深度优先
octopus_c2 小时前
C++ 运算符重载详解 && 日期类的实现
开发语言·c++
吴名氏.2 小时前
Java转AI Agent教程(第二周)超时重试与错误分类
java·人工智能·python·分类·ai agent
青少儿编程课堂2 小时前
2026蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:堆排序与优先队列解析
c++·python·算法·bfs·信息学竞赛