算法学习笔记(2):Trie树【字典树】

Trie树

前言

  • Trie树,又称字典树、前缀树。主要用于解决统计、排序和存储大量的字符串

  • 但是不仅限于字符串,比如一个数,它的二进制形式可以看成一个仅由01组成的字符串,如果一个trie仅由这些二进制的字符串组成,那么这棵树称为01trie

  • 故有很多位运算(异或)的问题也可以用trie树解决

    01tire

核心思想

  1. 根节点为空,除根节点以外每个节点只包含一个字符

  2. 从根节点到某一个有标记的节点,路径上经过的字符连接起来,为该节点对应的字符串

    为什么是有标记?比如我只有一个字符串abcd,这样我的trie树就是一条链,只有d节点有标记,所以abcd存在,abc、ab、a都不存在

  3. 每个节点的所有子节点包含的字符都不相同

【模板】字符串统计

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

  1. 向集合中插入一个字符串s
  2. 查询某个字符串s出现了多少次

操作的总次数不大于2e4

  • 插入操作

    • 建立trie树,在每个节点设置一个num标记,代表从根节点到该节点的路径,所组成的字符串出现了多少次
    • 迭代插入节点:如果该节点u的儿子节点中,没有代表下一个字符的节点,则建立该儿子节点son,然后将"指针"u指向到儿子节点重复上述操作,直至遍历完毕该字符串
    • 最后在u所指向的节点,将其num标记加上1
  • 查询操作

    • 同插入操作,迭代到字符串结束,返回最后u所指向的节点的num标记的值
  • 代码封装后大致如下:

    封装的某个好处是,你写多个数据结构嵌套的时候不会混淆函数名

    另一个好处是莫名的很爽?

struct Trie
{
     int son[N][26], num[N];
     //当前节点的儿子信息,以当前节点为结尾的单词数量
     int idx = 0;
     void build()
     {
         memset(son, 0, sizeof son);
         memset(num, 0, sizeof num);
     }
     void add(string str, int v)
     {
         int u = 0;//当前节点,也是trie树的根节点
         for (int i = 0; str[i]; i ++ )
         {
            int j = str[i] - 'a';
            if(!son[u][j])//儿子节点不存在
                son[u][j] = ++ idx;
            u = son[u][j];                
         }
         num [u] += v;//当前节点的数量
     }
    int query(string str)
    {
        int u = 0;
        for (int i = 0; str[i]; i ++ )
        {
            int j = str[i] - 'a';
            if(!son[u][j])
                return 0;
            u = son[u][j];
        }
        return num[u];
    }
}tr;

注意变量名不能取trie,因为C++库里面有个关键字叫做trie

字符串统计-小拓展

在上一题的基础上面,加上一个操作:

  1. 删除某个字符串s
  • 显然聪明的你已经想到了,因为路径上面的点,很多情况都是num值为0的,所以我们也可以把某些点类比称为路径上面的点
  • 我们直接让其num值减去1即可
  • 所以改变add函数为两个参数的函数即可void add(char *str, int v)
    • v为正数表示插入v
    • v为负数表示删除v个(前提是得有)
struct Trie
{
     int son[N][26], num[N];
     //当前节点的儿子信息,以当前节点为结尾的单词数量
     int idx = 0;
     void build()
     {
         memset(son, 0, sizeof son);
         memset(num, 0, sizeof num);
     }
     void add(string str, int v)
     {
         int u = 0;//当前节点,也是trie树的根节点
         for (int i = 0; str[i]; i ++ )
         {
            int j = str[i] - 'a';
            if(!son[u][j])//儿子节点不存在
                son[u][j] = ++ idx;
            u = son[u][j];                
         }
         num [u] += v;//当前节点的数量
     }
    int query(string str)
    {
        int u = 0;
        for (int i = 0; str[i]; i ++ )
        {
            int j = str[i] - 'a';
            if(!son[u][j])
                return 0;
            u = son[u][j];
        }
        return num[u];
    }
}tr;

例题

洛谷P2580 于是他错误的点名开始了

题目描述

给定一个由n个字符串组成的集合,随后有m次询问。

若是第一次询问且存在该字符串,则输出OK

若询问过该字符串则输出REPEAT

若不存在该字符串则输出WRONG

数据范围

n ≤ 1 0 4 , m ≤ 1 0 5 n\le10^4,m\le 10^5 n104,m105,字符串集合中的字符串长度不超过50

  • 显然这就是一道模板题,唯一与上面不同的区别就是第二次询问同一个字符串开始,输出不同
  • 若询问过该字符串,我们则在query函数中,先用tmp储存num的值,标记该节点的num[u]=-1表示询问过,然后返回tmp即可
  • 根据返回值为1-10,即可对应的输出题意所给定的内容
struct Trie
{
     int son[N][26], num[N];
     //当前节点的儿子信息,以当前节点为结尾的单词数量
     int idx = 0;
     void build()
     {
         memset(son, 0, sizeof son);
         memset(num, 0, sizeof num);
     }
     void add(string str, int v)
     {
         int u = 0;//当前节点,也是trie树的根节点
         for (int i = 0; str[i]; i ++ )
         {
            int j = str[i] - 'a';
            if(!son[u][j])//儿子节点不存在
                son[u][j] = ++ idx;
            u = son[u][j];                
         }
         num [u] += v;//当前节点的数量
     }
    int query(string str)
    {
        int u = 0;
        for (int i = 0; str[i]; i ++ )
        {
            int j = str[i] - 'a';
            if(!son[u][j])
                return 0;
            u = son[u][j];
        }
        int tmp = num[u];
        if(num[u])
            num[u] = -1;//表示已经询问过了
        return tmp;
    }
}tr;

【01tire模板】Acwing145最大异或对

PS:该题不需要money也可交题

给定一个数组a[1-n],求解a[i] XOR a[j]的最大值,其中 0 ≤ a [ i ] < 2 31 , 1 ≤ i < j ≤ n 0\le a[i]<2^{31}, 1\le i < j \le n 0a[i]<231,1i<jnXOR表示两个数按位异或的结果

  • 因为是小于2的31次方,约定第 i i i位的位权为 2 i 2^i 2i,则我们需要枚举的位为 0 − 30 0-30 030

  • 贪心思想

    • 我们想要让结果尽量大,那么就需要由高位往低位贪心,01000B>00111B,故我们优先让高位异或的结果为1
  • 按位考虑思想

    • 设需要求解的为x的最大异或结果res=x xor y的值,初始化res = 0
    • 从高位向低位,按位考虑,考虑当前节点u(初始u为根节点)代表第i-1位(初始i=0,若i=-1则代表没有),如果当前节点中,存在代表跟当前位相反的儿子节点(即son[u][!(x >> i & 1)]),则选择son[u][!(x >> i & 1)],否则选择son[u][x >> i & 1]

    若没有遍历完毕30位,则每个节点必然存在其中一个儿子,因为a[1-n]里面的每个数的二进制形式都是30

    故所有标记只会在叶子结点产生,这样其实我们也就没有必要打标记了。即,不会存在一个字符串是另一个字符串前缀的情况。

    故必然非叶子结点则每个节点必然存在son[u][!(x >> i & 1)]son[u][!(x >> i & 1)]其中一个儿子

    • 如果找到son[u][!(x >> i) & 1],说明当前位置能产生异或值的贡献,即res = res + (1 << i),反之则表示这一位只能强制跟原本的相同,即异或结果为0
  • 然后将每个a[i]都在tire树中询问一遍,求max{res}即可

  • 最后代码如下:

struct _01trie{
    int son[N * 16][2];//表面一个数有31个节点
    int num[N * 16]; int idx;
    void build()
    {
        memset(son, 0, sizeof son);
        memset(num, 0, sizeof num);
    }
    void add(int x)
    {
        int u = 0;
        for(int i = 30; i >= 0; i -- )
        {
            int &s = son[u][x >> i & 1];
            if(!s) //儿子节点不存在
                s = ++ idx;
            u = s;
        }
    }
    int search(int x) //返回(x xor y)的最大值,而不是y
    {
        int u = 0, res = 0;
        for(int i = 30; i >= 0; i -- )
        {
            int s = x >> i & 1;
            if(son[u][!s]) //另一个儿子存在
            {//说明该位异或可以为1
                res += 1 << i;
                u = son[u][!s];
            }
            else //否则说明这一位只能强制跟原本的相同 
                u = son[u][s];
        }
        return res;
    }
}tr;

CodeForce1625D最大异或集合

问题描述

给定一个数组a[1-n],求解一个集合,使得集合内任意两个元素的异或值都不小于k

输入格式

第一行,包含两个整数 n , k n,k n,k

第二行,包含 n n n个整数,表示数组 a [ 1 − n ] a[1-n] a[1n]

0 ≤ a [ i ] < 2 30 , 2 ≤ n ≤ 3 × 1 0 5 , 0 ≤ k < 2 30 0\le a[i]<2^{30}, 2\le n\le 3 \times 10^5,0\le k < 2 ^ {30} 0a[i]<230,2n3×105,0k<230

输出格式

第一行,一个整数m表示最大异或集合的大小

第二行,m个整数,表示集合中的元素在a[1-n]中的下标

tags

位运算、DS、树、CF2300分

  • 从高位到低位,按位考虑

    • ,对于一个集合,如果当前位为0则将其划分给左半边的集合,反之则将其划分给右半边的集合
    • 反复这个划分操作,知道当前位k出现的最高位才停止划分(k的最高位也不划分)
  • 于是,我们经过上述操作的时候,就得到若干个集合了

  • 因为 n m a x = 3 × 1 0 5 n_{max}=3\times 10^5 nmax=3×105,所以最后划分出来的集合数量不会超过 2 log ⁡ n 2\log n 2logn个,即 36 36 36

  • 以下我就称这些集合为:位集合

    无意冒犯bitset

  • 划分所得到的位集合的性质

    • 不同集合之间的两个元素异或,答案一定大于等于k
      • 显然,按照我们上面的划分方式,因为他们一定是因为在某一位i不同,然后被划分到不同的集合中
      • 故在第i位,两个集合中取出的数 x , y x,y x,y,在第i位一定是一个0一个1相异或得到的值,至少为 2 i 2^i 2i,因为我们的划分规则是在k的最高位就停止,故必然有 x ⊕ y ≥ 2 i > k x\oplus y \ge 2^i > k xy2i>k
    • 同一个集合中选出到最后答案里面的数,最多不超过两个
      • 我们学过了一个集合内求出最大异或对01trie的做法,那么我们就可以将这个集合丢进01trie中,然后查询集合内的每一个数,若查询到a[i]最大异或值大于k,则将再线性遍历一遍集合(因为需要输出的是下标,而不是数值),找出跟a[i]配对的那个最大异或值的下标
      • 这样我们就找到了这个集合里对最后答案贡献的两个数
      • 那么是否有可能是三个数或者更多呢?
      • 答案是不可能是,既然他们在同一个位集合内,那么就说明他们在k的最高位以前全部相同,只有在最高位及其以后才会出现不同。它们异或出来的结果,在k的最高位必须为1,这样才有可能会不小于k,即一个数该位为0,一个数该位为1
      • 如果我们加入第三个数,鸽笼原理(抽屉原理、染色原理)得,必然存在一对该位相同,这样他们的异或值在k的最高位必然为0,即必然小于k
      • 故同一个集合中选出到最后答案里面的数,最多不超过两个
  • 本题中01trie维护的细节

    • 因为我们上次插入一个集合,又删掉它们的时候,有些节点已经建立完成了,导致他们的son值不为0,但是不存在于当前位集合中。
    • 每次用完后trie都是空的,但是memsetson数组效率过低,所以采用delete字符串的方式
    • 所以我们采用一下方式来解决:新加入num数组,节点存在且num值大于0才表示这个前缀存在于这个位集合中
    • 我们需要在路径上面的点,也给它们的num标志加上v,这样就能和不存在的点区分开来(num=0),才能继续向下迭代遍历
  • 最后注意边界条件k=0时候直接输出所有数字即可

  • 最后博主全部的代码大致如下

#include <bits/stdc++.h>
#include <bits/extc++.h>
#include <unordered_map>
#include <unordered_set>
using namespace std;
using namespace __gnu_cxx;
using namespace __gnu_pbds;

#define debug(x) cerr << #x << ": " << x << '\n';
#define bd cerr << "----------------------" << el;
#define el '\n'
#define cl putchar('\n');
#define pb push_back
#define eb emplace_back
#define x first
#define y second
#define rep(i, a, b) for (int i = (a); i <= (b); i++)
#define lop(i, a, b) for (int i = (a); i < (b); i++)
#define dwn(i, a, b) for (int i = (a); i >= (b); i--)
#define ceil(a, b) (a + (b - 1)) / b
#define ms(a, x) memset(a, x, sizeof(a))
#define INF 0x3f3f3f3f
#define db double
#define all(x) x.begin(), x.end()
#define reps(i, x) for (int i = 0; i < x.size(); i++)
#define cmax(a, b) a = max(a, b)
#define cmin(a, b) a = min(a, b)

typedef long long LL;
typedef long double LD;
typedef pair<int, int> PII;
typedef pair<db, db> PDD;
typedef vector<int> vci;

template <typename T>
inline void read(T &x)
{
    x = 0;
    T f = 1;
    char c = getchar();
    while (!isdigit(c))
    {
        if(c == '-')
            f = -1;
        c = getchar();
    }
    while (isdigit(c))
    {
        x = (x << 1) + (x << 3) + (c ^ 48);
        c = getchar();
    }
    x *= f;
}

constexpr int N = 3e5 + 5;

int T, n, m, k;
int a[N];
vci ans;

struct _01trie{
    
    int son[N * 16][2];//表面一个数有30个节点
    int num[N * 16]; int idx;
    void build()
    {
        ms(son, 0);
        ms(num, 0);
    }
    void add(int x, int v)
    {
        int u = 0;
        dwn(i, 29, 0)
        {
            int &s = son[u][x >> i & 1];
            if(!s) //儿子节点不存在
                s = ++ idx;
            u = s; 
            num[u] += v;
            //这句话得写在里面,不能写在外面
        }
       
    }
    int query(int x) //返回(x xor y)的最大值,而不是y
    {
        int u = 0, res = 0;
        dwn(i, 29, 0)
        {
            int s = x >> i & 1;
            if(son[u][!s] && num[son[u][!s]] > 0) //另一个儿子存在
            {//说明该位异或可以为1
                res += 1 << i;
                u = son[u][!s];
            }
            else //否则说明这一位只能强制跟原本的相同 
                u = son[u][s];
        }
        return res;
    }
}tr;

void work(vci v)//看一下该集合能出几个数
{//一个集合最多出两个数
    if(!v.size())//v为空则没必要计算了
        return ;
    for(auto i : v)
        tr.add(a[i], 1);//将当前集合插入
    bool ok = false;//该集合是否插入两个数
    for(auto i : v)
        if(tr.query(a[i]) >= k) //a[i]对应的最大异或的结果>=k
        {
            ok = true;
            for(auto j : v)
                if((a[i] ^ a[j]) >= k)//找到对应的异或对
                {
                    ans.pb(i), ans.pb(j);
                    break;
                }
            break;
        }
    if(!ok)//若不能插入两个数,则插入一个数
        ans.pb(v[0]);
    for(auto i : v)
        tr.add(a[i], -1);//删除掉
    return ;        
}

void divide(vci v, int bit) //划分位集合
{
    if( !v.size())
        return ;
    if(k & (1 << bit)) //到k的最高位1停止划分
    {
        work(v);
        return ;
    }
    vci v0, v1;
    for(auto i : v)
    {
        if(a[i] & (1 << bit))
            v1.pb(i);//a[i]的bit位是否为1
        else 
            v0.pb(i);
    }
    divide(v0, bit - 1);
    divide(v1, bit - 1);
    return ;
}

int main()
{
    read(n), read(k);
    vci v(n);
    iota(v.begin(), v.end(), 1);
    rep(i, 1, n)
        read(a[i]);
    if(k == 0)
    {
        cout << n << el;
        rep(i, 1, n)
            cout << i << ' ';
        return 0;
    }
    divide(v, 29);
    if(ans.size() <= 1)
        cout << -1 << el;
    else 
    {
        cout << ans.size() << el;
        for(auto it : ans)
            cout << it << ' ';
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值