洛谷每日一题(P1481 魔族密码)字典树解法

原题目链接:

P1481 魔族密码 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

原题目截图:

思路分析:

这道题的话其实有很多种方法,可以用动态规划做,不过我一看到这道题,脑子里不禁蹦出一个数据结构:“字典树”!

字典树+深度优先搜索。

那么在这之前,我们先来了解一下什么是字典树吧!

什么是字典树:

字典树(Trie),也称为前缀树或Ternary Search Tree,是一种用于快速检索字符串数据集中的键的数据结构。它的优点在于可以快速地插入和查找字符串,并且可以有效地用于实现自动补全和拼写检查等功能。

字典树的特点:(有颜色的部分是重点)

  1. 共同前缀:字典树利用字符串的共同前缀来减少存储空间和提高查找效率

  2. 节点:字典树由多个节点组成,每个节点表示一个字符。

  3. :节点之间的连接表示从父节点到子节点的字符。

  4. 根节点:字典树的根节点通常表示一个空字符串。

  5. 结束符:通常用一个标志(如isEndOfWord)来表示某个节点是一个单词的结束。

字典树的工作原理:

  • 插入:从根节点开始,对要插入的字符串的每个字符,检查是否存在相应的子节点。如果不存在,则创建一个新的节点。最后,标记该字符串的结束节点。

  • 查找:从根节点开始,按照要查找的字符串的字符顺序遍历节点。如果某个字符的子节点不存在,则表示该字符串不在字典树中。

  • 删除:从根节点开始,按照要删除的字符串的字符顺序遍历节点。对于每个节点,检查是否有其他子节点。如果没有,则可以删除该节点。

字典树的应用场景:

  1. 自动补全:在用户输入时,根据已输入的字符串提示可能的完整单词。

  2. 拼写检查:检查用户输入的单词是否拼写正确。

  3. IP 路由:在网络路由中,IP地址可以通过字典树快速匹配到相应的路由策略。

  4. 文本搜索:实现文本搜索算法,如字符串搜索和模式匹配。

怎么构建字典树:

我们知道了,字典树其实就是把字符串统计在一个树状结构里面,每个节点储存一个字符。

那么我们以题目所给的案例,来试着构建一颗字典树吧:

剩下的插入操作大同小异,我们这里直接看最终结果:

构建字典树的代码实现:

这道题的话,其实只涉及到插入操作。

我们先看看字典树应该有一个怎样的节点结构:

#include<iostream>
#include<unordered_map>
#include<vector>
#include<string>
using namespace std;

// 定义字典树的节点
struct trienode {
    char val;               // 节点代表的字符
    unordered_map<char, trienode*> ump;  // 存储子节点的哈希表,键是字符,值是指向该字符对应节点的指针
    bool iswordend;         // 标记该节点是否为一个单词的结束位置

    // 构造函数,初始化节点的值和结束标志
    trienode(char v) : val(v), iswordend(false) {}

    // 插入单词到字典树
    void insertword(string word) {
        trienode* cur = this;  // 从当前节点(根节点或父节点)开始
        for (char ch : word) {  // 遍历单词中的每个字符
            if (!cur->ump.count(ch)) {  // 如果当前节点没有该字符的孩子节点
                cur->ump[ch] = new trienode(ch);  // 创建一个新的节点并添加到哈希表中
            }
            cur = cur->ump[ch];  // 移动到下一个节点
        }
        cur->iswordend = true;  // 将单词的最后一个字符对应的节点标记为单词结束
    }
};

代码解释:
  • 结构体定义trienode是字典树的节点,包含一个字符val,一个哈希表ump用于存储子节点,以及一个布尔值iswordend用于标记单词的结束。
  • 构造函数:初始化节点的字符和结束标志。结束标志默认为false
  • insertword函数
    • 参数word是需要插入到字典树中的单词。
    • 过程
      • 使用一个指针cur从当前节点开始遍历。
      • 对于单词中的每个字符,检查当前节点是否有该字符的子节点。
      • 如果没有,创建一个新的节点并添加到哈希表中。
      • cur更新为该子节点,继续遍历下一个字符。
      • 遍历完成后,将单词的最后一个字符对应的节点标记为单词结束。

解决代码:

当你把字典树建好之后,你会发现这其实就是一道很简单的深度优先计数题了:

#include<iostream>
#include<unordered_map>
#include<vector>
#include<string>
using namespace std;

// 定义字典树的节点
struct trienode {
    char val;               // 节点代表的字符
    unordered_map<char, trienode*> ump;  // 存储子节点的哈希表,键是字符,值是指向该字符对应节点的指针
    bool iswordend;         // 标记该节点是否为一个单词的结束位置

    trienode(char v) : val(v), iswordend(false) {}  // 构造函数

    // 插入单词到字典树
    void insertword(string word) {
        trienode* cur = this;  // 从当前节点(根节点或父节点)开始
        for (char ch : word) {  // 遍历单词中的每个字符
            if (!cur->ump.count(ch)) {  // 如果当前节点没有该字符的孩子节点
                cur->ump[ch] = new trienode(ch);  // 创建一个新的节点并添加到哈希表中
            }
            cur = cur->ump[ch];  // 移动到下一个节点
        }
        cur->iswordend = true;  // 将单词的最后一个字符对应的节点标记为单词结束
    }
};

// 深度优先搜索(DFS)函数,用于找到最长的词链
void dfs(trienode* root, int &currentLength, int& maxn) {
    if (!root) return;  // 如果当前节点为空,则返回

    if (root->ump.empty()) {
        // 如果当前节点没有子节点,说明到达了叶子节点,即一条链路的终点
        maxn = max(maxn, currentLength);  // 更新最长链长度
        return;
    }

    for (auto& it : root->ump) {
        // 遍历当前节点的所有子节点
        if (it.second->iswordend) {
            // 如果子节点是单词的结尾,则增加当前链的长度
            currentLength++;
        }
        // 递归地对每个子节点进行深度优先搜索
        dfs(it.second, currentLength, maxn);
        if (it.second->iswordend) {
            // 如果子节点是单词的结尾,在回溯时减少当前链的长度
            currentLength--;
        }
    }
}

int main() {
    int n;
    cin >> n;
    trienode* root = new trienode('*'); // 头结点
    for (int i = 0; i < n; i++) {
        string word;
        cin >> word;
        root->insertword(word);
    }

    int maxn = 0;
    int c = 0;
    dfs(root, c, maxn); // 进行DFS搜索
    cout << maxn << endl; // 输出最长链长度

    // 释放内存
    // 注意:实际应用中需要递归释放字典树的所有节点内存
    return 0;
}

对于这里dfs的写法,开始我没有留意到符号&,在出错之后,我把这个小细节注意到了。

各位读者不妨比较一下上下两段代码中dfs函数的细微区别。

当然这里给出的两段代码都是正确的可运行代码。

#include<iostream>
#include<unordered_map>
#include<vector>
#include<string>
using namespace std;

// 定义字典树的节点
struct trienode {
    char val;
    unordered_map<char, trienode*> ump;
    bool iswordend;
   

    trienode(char v) : val(v), iswordend(false) {}

    // 插入
    void insertword(string word) {
        trienode* cur = this;
        for (char ch : word) {
            if (!cur->ump.count(ch)) {
                cur->ump[ch] = new trienode(ch);
            }
            cur = cur->ump[ch];
        }
        cur->iswordend = true;
    }
};

void dfs(trienode* root, int currentLength, int& maxn) {//currentLenth表示当前递归已有单词数
    if (!root) return;

    if (root->ump.empty()) {
        maxn = max(maxn, currentLength);//到叶子结点,即一条链路完
        return;
    }

    for (auto& it : root->ump) {
        if (it.second->iswordend) currentLength++;   
       // cout << "dfs(" << it.second->val << "," << currentLength << "," << maxn << ")" << endl;
            dfs(it.second, currentLength , maxn);
            
    }
}





int main() {
    int n;
    cin >> n;
    trienode* root = new trienode('*'); // 头结点
    for (int i = 0; i < n; i++) {
        string word;
        cin >> word;
        root->insertword(word);
    }

    int maxn = 0;
    int c = 0;
    dfs(root, c, maxn); // 进行DFS搜索
    cout << maxn << endl; // 输出最长链长度

   
  

    return 0;
}


今天的博客就写到这里了,很快就要国庆假期了,预祝诸君国庆快乐!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值