Trie(字典树)

Trie(字典树)

[字典树](Trie Tree) 是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串)。
它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
                                                   ——百度 · 百科

image

image

理解son[] []:存储子节点的位置,最多26条分支

  • Tire树本质上一个多叉树,最多可以分多少叉呢?因为此题存的都是小写字母,所以是26叉.
  • 这里就解释了son这个二维数组的第二维的含义,就是他最多有26个孩子,那么他是谁呢,他当然是结点了,那结点之间怎么区分,或者这些孩子的爸爸叫啥,爸爸们用下标来区别,所以第一维就是爸爸们的id,son[0] [1]含义就是0号爸爸有个儿子b ,那son[0] [1] = 2,就是0号爸爸有个儿子b,儿子的id是2; 这些id就是由idx 来统一管理分配

son[0] [u] 这个的含义就是根节点它的孩子u结点的下标,跟单链表里的next指针其实是一样的。
不过有区别的一点是我们的单链表它的孩子只有一个,是一条链子。
但是我们的Trie是后面可能有26个孩子(因为英文字母只有26个),即有26种路径可以选的单链表。

理解idx:

idx:同链表,表示当前要插入的节点是哪一个,每创建一个节点idx+1(idx 当前用到了哪个下标 初值为0 即为空节点 也是根节点)

acwing:Trie统计字符串

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

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

共有 NN 个操作,输入的字符串总长度不超过 105105,字符串仅包含小写英文字母。

输入格式

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

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

输出格式

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

每个结果占一行。

数据范围

1≤N≤2∗1041≤N≤2∗104

输入样例:

5
I abc
Q abc
Q ab
I ab
Q ab

输出样例:

1
0
1

#include<iostream>

using namespace std;

const int N = 1e5 + 10;
// son[N][26] 存储子节点的位置,最多26条分支
// cnt[] 存储以某节点为结尾的字符串的个数(同时起到标记作用)
// idx:同链表,表示当前要插入的节点是哪一个,每创建一个节点idx+1(idx 当前用到了哪个下标 初值为0 即为空节点 也是根节点)
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++) // 字符串以'/0'结尾
    {
        int u = str[i] - 'a'; // 将字符映射为数字(字典树)0~25
        if(son[p][u] == 0) son[p][u] = ++idx; // 如果该元素还没有创建则创建节点
        p = son[p][u]; // 指针p移动
    }
    
    // 插入完之后p指向了该字符串的最后一个元素(节点)
    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] == 0) return 0; // 树中未找到该元素,则表明无此串
        p = son[p][u]; // 找到则p移动
    }
    // 最后都存在(找到)返回该串出现的次数
    return cnt[p];
    
}

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

acwing:最大异域对

在给定的 NN 个整数 A1,A2……ANA1,A2……AN 中选出两个进行 xorxor(异或)运算,得到的结果最大是多少?

输入格式

第一行输入一个整数 NN。

第二行输入 NN 个整数 A1A1~ANAN。

输出格式

输出一个整数表示答案。

数据范围

1≤N≤1051≤N≤105,
0≤Ai<2310≤Ai<231

输入样例:

3
1 2 3

** 输出样例:**

3

字典树不光可以用来存储字符,还可能存储整数!

1、 son数组定义是二维数组,son[ n ] [ m ]我在初学Trie树的时候很难理解,可以先理解它的第二维度,只有两种状态0/1,是因为这一位表示的是某个数字的的某一位是0 / 1,而第一维的大小是我们用的十位二进制表示下一共有多少位数,在本题中,数字一共有N个数字,N是小于10^5的,一个数在int下是32位,则我们需要至少3200000的一维坐标,而p = son[n] [0] / p = son[n] [1]其实存的值就是他的两个子节点的一维坐标的值。
那么x >> i & 1 其实就是我想知道x的二进制表示中的第i位(二进制位从第0位开始表示第0位 - 第 31 位),本题的题目范围到2^31那么就是i从30 - 0。

2、构建逻辑其实相对简单,就是将数的二进制表示插入到son数组中,如果没有那么就将他的值附上++idx即可
3、查询的时候为了异或大小最大,那么就应该从根节点开始遍历最高位开始尽量让异或结果为1,因此在遍历查询数字的时候,尽量找与它自己不相同的数。取到第i位的时候第i位的值为u,查看son[u] [!s](不同方向上)是否存在,如果存在那么将res = (res << 1) + !u(这一位异或存在最大值为1),如果没有那么就让p = son[p] [u]。

暴力:超时

【参考代码】

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;
const int M = N * 2;
int son[M][2], a[N];
int idx;

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

字典树:O(n*logn)

【参考代码】

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010, M = 3100010;

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


void insert(int x) // 插入操作同字符串
{
    int p = 0;
    for(int i = 30; i >= 0; i--)
    {
        int u = x >> i & 1;
        if(son[p][u] == 0) son[p][u] = ++ idx;
        p = son[p][u];
    }
}

int query(int x)
{
    int p = 0, res = 0;
    for(int i = 30; i >= 0; i--)
    {
        int u = x >> i & 1;
        
        if(son[p][!u])  //如果另外一个方向存在的话就走到另外一个方向上去
        {
            p = son[p][!u];
            res = (res << 1) + !u; // 左移一位然后加上当前位的数
        }
        else //如果另外一个方向不存在的就只能接着往下走(找不到只好将就了)
        {
            p = son[p][u];
            res = (res << 1) + u; // 左移等价于乘2 : res = res * 2 + u
        }
    }
    
    // 返回整个路径记录的答案
    return res;
}

int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ ) cin>>a[i];
    
    int ans = 0;
    for(int i = 0; i < n; i++)
    {
        // 为了避免边界条件的判断(树为空时),因此我们先插入再查询与当前a[i]最匹配的数
        insert(a[i]);
        int t = query(a[i]);
        // 边插入边查询
        ans = max(ans, a[i] ^ t);
    }
    
    cout << ans;
    
    return 0;
}

关于res = (res << 1) + !u理解:res是用来记录最好配对数的路径,例如,假设当前res的路径res = 1101,如果下一位的数是1,更新res,则将1101左移一位(11010),然后加上1(11011);如果下一位的数是0,更新res,则将1101左移一位(11010),然后加上0(11010)

posted @ 2021-11-12 15:50  时间最考验人  阅读(698)  评论(0编辑  收藏  举报