AcWing 835. Trie字符串统计

本文详细介绍了Trie树的数据结构,包括节点编号、son数组和cnt数组的作用,以及如何通过son数组进行字符串插入和查询的操作过程。通过实例展示了'cat', 'car', 'busy', 'cate', 'bus', 'car'的插入与查询。
摘要由CSDN通过智能技术生成

题目描述


分析:

Trie 树是一种能够快速插入查询字符串的多叉树结构。节点的编号各不相同,根节点编号为 0,其他节点用来标识路径,还可以标记单词插入的次数。

Trie 树最重要的是用 son 这个二维数组模拟多叉树的思想。其中 son[p][u] 表示从节点 p 沿着 u 这条边走到的子节点。边的选择有26(a-z 映射为 0-25) 个。也就是每个节点最多有 26 个分叉。
另外利用 cnt[p] 来存储以结点 p 结尾的单词的插入次数。
多次出现的 idx 用来给节点编号。(根据插入图进行正确理解)

插入
int son[N][26], cnt[N], idx;
char str[N];

void insert(char *str)
{
    int p = 0; // p 指向根节点
    for (int i = 0; str[i]; i ++ ) // 遍历字符串的每一个有效字符
    {
        int u = str[i] - 'a'; // u 为当前字符在(0-25)的映射
        
		// 从上个节点沿 u 这条边找不到子节点,借助这条边创建子节点,更新 idx 并赋值
        if (!son[p][u]) son[p][u] = ++ idx; 
        p = son[p][u]; // p 指针移动到该子节点,继续插入后面的字符
    }
    cnt[p] ++ ; // 整个字符串插入结束,p 指向最后一个字符的节点,cnt[p] 存储末尾字符出现的次数
}

在空 Trie 中仅有一个根节点,编号为 0。从根开始插,枚举字符串的每个字符:① 如果有子节点(之前有边能通到),则 p 指针顺着之前出现的边走到该节点;② 如果没有子节点,则先创建子节点,根据当前字符映射的值创建边,p 指针再走到该子节点。在单词结束点记录插入次数。
下面模拟一下插入 “cat” 时树的形状及其参数变化:

  1. 遍历到 ‘c’ 时,c - ‘a’ 为 2,c 又是第 1 个插入的节点 idx 为 1,因此 son[0][2] = 1,也就是说第 1 个节点是根节点沿 2(c - ‘a’) 这条边走到的子节点。
  2. 遍历到 ‘a’ 时,a - ‘a’ 为 0,a 是第 2 个插入的节点 idx 为 2,因此 son[1][0] = 2,也就是说第 2 个节点是第 1 个节点沿 0(a - ‘a’) 这条边走到的子节点。
  3. 遍历到 ‘t’ 时,t - ‘a’ 为 19,t 是第 3 个插入的节点 idx 为 3,因此 son[2][19] = 3,也就是说第 3 个节点是第 2 个节点沿 19(t - ‘a’) 这条边走到的子节点。

下面画出以此插入 “cat” “car” “busy” “cate” “bus” “car”。用红色的星号来表示字符串结尾处。

在这里插入图片描述
这里需要注意的是 idx 的值的变化,只有当前插入的字符串字符是之前没有边到达过的,才会生成节点并更新 idx。这就方便了我们重复字符串的插入后的操作。

查询
int query(char *str)
{
    int p = 0; // p 指向根节点
    for (int i = 0; str[i]; i ++ ) // 遍历字符串的每一个有效字符
    {
        int u = str[i] - 'a'; // u 为当前字符在(0-25)的映射
        if (!son[p][u]) return 0; // 从上个节点沿 u 这条边找不到子节点,该字符串没出现过
        p = son[p][u]; // 出现过,p 指针移动到该子节点,继续判断后面的字符
    }
    return cnt[p]; // 返回以 p 为结尾的字符串的出现的次数
}

查询操作时,从根开始查扫描字符串,简单来说就是有字母 str[i],则走下来一直到词尾,返回插入次数;若无字母,返回0。


代码(C++)

#include <iostream>

using namespace std;

const int N = 100010;

int son[N][26], cnt[N], idx;
char str[N];

void insert(char *str)
{
    int p = 0; // p 指向根节点
    for (int i = 0; str[i]; i ++ ) // 遍历字符串的每一个有效字符
    {
        int u = str[i] - 'a'; // u 为当前字符在(0-25)的映射
        
		// 从上个节点沿 u 这条边找不到子节点,借助这条边创建子节点,更新 idx 并赋值
        if (!son[p][u]) son[p][u] = ++ idx; 
        p = son[p][u]; // p 指针移动到该子节点,继续插入后面的字符
    }
    cnt[p] ++ ; // 整个字符串插入结束,p 指向最后一个字符的节点,cnt[p] 存储末尾字符出现的次数
}

int query(char *str)
{
    int p = 0; // p 指向根节点
    for (int i = 0; str[i]; i ++ ) // 遍历字符串的每一个有效字符
    {
        int u = str[i] - 'a'; // u 为当前字符在(0-25)的映射
        if (!son[p][u]) return 0; // 从上个节点沿 u 这条边找不到子节点,该字符串没出现过
        p = son[p][u]; // 出现过,p 指针移动到该子节点,继续判断后面的字符
    }
    return cnt[p]; // 返回以 p 为结尾的字符串的出现的次数
}

int main()
{
    int n;
    cin >> n;
    
    while (n --)
    {
        char op;
        cin >> op >> str;
        if (op == 'I') insert(str);
        else cout << query(str) << endl;
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值