自学内容网 自学内容网

数据结构与算法学习笔记----Trie树

数据结构与算法学习笔记----Trie树

@@ author: 明月清了个风

@@ last edited: 2024.11.25

🐔挖坑待填:

  • Trie树的存储优化方法

Acwing 835. Trie字符串统计

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

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

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

输入格式

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

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

输出格式

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

每个结果占一行。

数据范围

1 ≤ N ≤ 2 ∗ 1 0 4 1 \leq N \leq 2 * 10^{4} 1N2104,

思路

Trie树也就是字典树,是一种树形数据结构,主要的作用是能够高效地存储和查找字符串集合,适用的场景是需要进行前缀匹配的地方,例如自动补全、拼写检查等等。

插入和查询的时间复杂度都和字符串长度 L L L相关,都为O(L)

下面给出一个Trie树实例,树中存储四个单词:apple,app,bat,ball。图中黑色实线表示真实建立的节点,黑色虚线表示潜在节点;蓝色节点为字符串内部节点,橙色节点表示字符串结尾节点。可以发现,Trie树的占用空间大小实际和每一层可能出现的字符串内部节点的多少相关,例如本题中限定字符串内部仅有小写字母,那么第 n n n层的最大节点数就是 2 6 n − 1 26^{n-1} 26n1个,因此如果需要存储的字符串长度越长,需要的存储空间也就越大(此题中限制了所有节点数量不超过 1 0 5 10^{5} 105)
在这里插入图片描述

代码

#include <iostream>

using namespace std;

const int N = 100010;

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

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];
}

int main()
{
    cin >> n;
    
    while(n --)
    {
        char op[2];
        scanf("%s%s", op, str);
        
        if(op[0] == 'I') insert(str);
        else cout << query(str) << endl;
    }
    
    return 0;
}

Acwing 143. 最大异或对

在给定的 N N N个整数 A 1 , A 2 . . . . . . A N A_{1},A_{2}......A_{N} A1,A2......AN中选出两个进行 x o r xor xor(异或)运算,得到的结果最大是多少?

输入格式

第一行输入一个整数 N N N

第二行输入 N N N个整数 A 1   A N A_{1}~A_{N} A1 AN.

输出格式

输出一个整数表示答案。

数据范围

1 ≤ N ≤ 1 0 5 1 \leq N \leq 10^{5} 1N105,

0 ≤ A i ≤ 2 31 0 \leq A_{i} \leq 2^{31} 0Ai231

思路

首先思考暴力做法,从N个数中选两个进行计算,两重循环即可完成,伪代码如下:

...
    int maxv = -inf;
    for(int i = 0; i < n; i ++)
        for(int j = i + 1;  j < n; j ++)
            maxv = max(maxv, a[i] ^ a[j]);
...

暴力做法的时间复杂度是O(n2)

考虑优化方法首先要知道异或操作的性质:对于两个数的二进制表示而言,若同为 1 1 1或同为 0 0 0则为 0 0 0,否则为1

那么为了使得到的树最大,则两个数的较高位应尽量不同, 这样才能得到高位的1,这里举个例子:
在这里插入图片描述
可以看出对于 A 1 A_1 A1而言,为了得到最大的异或值 A 2 A_2 A2的二进制表示应与 A 1 A_1 A1相反,但这是理想情况,若在某一位不存在一个数在这一位上与其相反,那也只能这样选择了。

因此可以得出对暴力做法的优化方法:将每个数的二进制表示插入Trie树中,那么对于每个数而言都会从根节点开始有一条路径,求与其异或最大的数就是找一条从根节点开始的路径,这条路径上的每一位尽量与原数的路径不同。

优化后第二重循环就变成了一个基于Trie树的逐位匹配,对于每个数最大长度为31,因此,时间复杂度变为O( 31 ∗ N 31 * N 31N)

代码

#include <iostream>

using namespace std;

const int N = 100010, M = 31 * N;

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

void insert(int x)
{
    int p = 0;
    
    for(int i = 30; ~i; i --)
    {
        int &s = son[p][x >> i & 1];
        if(!s) s = ++ idx;
        p = s;
    }
}

int query(int x)
{
    int res = 0, p = 0;
    
    for(int i = 30; ~i; i --)
    {
        int t = x >> i & 1;    // 取出每一位是0还是1
        if(son[p][!t])   // 判断相反的节点是否存在,存在就走上去,答案就加上2的i次方。
        {
            res += 1 << i;
            p = son[p][!t];
        }
        else p = son[p][t];   // 如果只有相同的节点,那么也只能走上去,但是res没有增加。
    }
    
    return res;
}

int main()
{
    cin >> n;
    
    for(int i = 0; i < n; i ++)
    {
        cin >> a[i];
        insert(a[i]);
    }
    
    int res = 0;
    
    for(int i = 0; i < n; i ++) res = max(res, query(a[i]));
    
    cout << res << endl;
    
    return 0;
}

原文地址:https://blog.csdn.net/weixin_60278491/article/details/144042227

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!