Trie树(字典树)

作用

看下面两个题:

  1. 给出n个单词和m个询问,每次询问一个单词,回答这个单词是否在单词表中出现过。

答: 简单!map,短小精悍。

  1. 给出n个单词和m个询问,每次询问一个前缀,回答询问是多少个单词的前缀

答: map !TLE警告!

这就需要字典树

概念

单词查找树,Trie树,是一种树形结构,是一种
哈希树的变种。

优点

  1. 利用字符串的公共前缀来节约存储空间
  2. 最大限度地减少无谓的字符串比较,查询效率比哈希表

先放一张字典树的图:
image
可以发现,这棵字典树用边来代表字母,而从根结点到树上某一结点的路径就代表了一个字符串。
比如说字符串 caa,就是1->4->8->12。

知道思想,建树就很简单了。

代码

我们定义几个变量

  1. son[N][26]:存放子节点对应的idx。
    其中第一维是指:节点对应的idx
    第二维是指:子节点('a' - '0')的下标。(或者说是指向下一个节点的边)
    比如: son[1][0]=2表示1结点的一个值为a的子结点为结点2

  2. cnt[N]:存放该idx对应的个数

  3. idx: 记录每一个节点的位置。

建树

思路:
从左到右扫这个单词,如果字母在相应根节点下没有出现过,就插入这个字母;否则沿着字典树往下走,看单词的下一个字母。
代码:

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];  //使“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]) return 0;  //该节点不存在,即该字符串不存在
        p = son[p][u];
    }
    return cnt[p];  //返回字符串出现的次数
}

模板代码

int son[N][26],cnt[N],idx;
void insert(string s){
    int p=0;
    for(int i=0;i<s.length();i++){
        int u=s[i]-'a';
        if(!son[p][u]) son[p][u]=++idx;
        p=son[p][u];
    }
    cnt[p]++;
}
int query(string s){
    int p=0;
    for(int i=0;i<s.length();i++){
        int u=s[i]-'a';
        if(!son[p][u]) return 0;
        p=son[p][u];
    }
    return cnt[p];
}

应用

检索字符串

字典树最基础的应用——查找一个字符串/前缀是否在“字典”中出现过。

字典树 模板题

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct ooo
{
    int next[26];   
    bool mark;  //标记
}dd[505555];
int top,n,m;
char a[20],b[20];
int creat()  //分配新的节点
{
    memset(dd[top].next,-1,sizeof(dd[top].next));  //表示指向空
    dd[top].mark=false;//表示无对应字符串
    return top++;
}
int xiab(char c)
{
    return c-'a'; //对应下标
}
void insert(int root,char *s)   //将S插入到字典树中
{ 
    int i,len=strlen(s);
    for(i=0;i<len;i++)
    {
        if(dd[root].next[xiab(s[i])]==-1)
        dd[root].next[xiab(s[i])]=creat();
        root=dd[root].next[xiab(s[i])];
    }
    dd[root].mark=true;
}
 
bool search(int root,char *s)  //询问是否出现在字典树中
{
    for(int i=0;s[i]!='\0';i++)
    {
        if(dd[root].next[xiab(s[i])]==-1)
        return false;
        root=dd[root].next[xiab(s[i])];
    }
    return dd[root].mark;
}
int main()
{
    int i,j,root;
 
    while(scanf("%d %d",&n,&m)&&(n||m))
    {
        top=0;
        root=creat();
        for(i=0;i<n;i++)
        {
        scanf("%s",a);
        insert(root,a);
        }
        for(i=0;i<m;i++)
        {
           scanf("%s",b);
           printf("%s\n",search(root,b)?"Yes":"No");
        }
    }
    return 0;
}

迷之好奇

思路:
题目要求后缀,反转成前缀即可。
但是...数据量太大,字典树会超时,题目就变成了思维题。

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int N=1e5+10;
int col[N];
int a[N][15];
int flag[N];
int k;
string s1,s2;
void Insert()
{
    int p=0,i;
    for(i=0;i<s1.size();i++)
    {
        if(!a[p][s1[i]-'0']) a[p][s1[i]-'0']=++k;
        col[p]++;//记录几个单词经过了他。
        p=a[p][s1[i]-'0'];
    }
    flag[p]=1;//表示这是一个单词的末尾。
}
int search1()
{
    int p=0,i;
    for(i=0;i<s2.size();i++)
    {
        if(!a[p][s2[i]-'0']) return 0;
        p=a[p][s2[i]-'0'];
    }
    return col[p];
}
int main()
{
    int n,m,i;
    ios::sync_with_stdio(false);
    while(cin>>n)
    {
        for(i=1;i<=n;i++)
        {
          cin>>s1;
          reverse(s1.begin(),s1.end());
          Insert();
        }
         cin>>m;
        for(i=1;i<=m;i++)
        {
            cin>>s2;
            reverse(s2.begin(),s2.end());
            int num=search1();//找到单词段然后返回值。
            printf("%d\n",num);
        }
        memset(flag,0,sizeof(flag));
        memset(a,0,sizeof(a));
        memset(col,0,sizeof(col));
        k=0;//初始化。
    }
    return 0;
}

AC 自动机

trie 是 AC 自动机 的一部分。

维护异或极值

将数的二进制表示看做一个字符串,就可以建出字符集为{0,1} 的 trie 树,称为\(01-trie\)
如果将所有数以二进制形式插入到一棵 trie 中,就可以快速求出和 数字T 的异或和最大的数:
从 trie 的根开始,如果能向和 T 的当前位不同的子树走,就向那边走,否则走相同位的方向。

143. 最大异或对
题意:
在给定的N个整数\(A_1,A_2 …… A_N\)中选出两个进行xor(异或)运算,得到的结果最大是多少?
思路:
将每个数以二进制方式存入字典树,找的时候从最高位去找有无该位的异.
代码:

#include <bits/stdc++.h>
#define ins 0x3f3f3f3f
using namespace std;
const int N = 100010, M = 3100010;
#define pii pair<int, int>
int n;
int son[M][2],idx;
int a[N];
void insert(int x){
    int p=0;
   
    for(int i=30;i>=0;i--){
         int k=(x>>i)&1;
         if(!son[p][k])  son[p][k]=++idx;
         
         p=son[p][k];
    } 
}
int query(int x){
    int p=0;
     int  res = 0;
    for(int i=30;i>=0;i--){
        int k=(x>>i)&1;
        if(son[p][!k]) {
             res += 1 << i;
            p=son[p][!k];
        }
        else p=son[p][k];
    }
    return res;
}
void solve()
{
    int n;
    cin >> n;
    for(int i=0;i<n;i++){
        cin>>a[i];
        insert(a[i]);
    }
    int ans=-1;
    for(int i=0;i<n;i++)
       ans=max(query(a[i]),ans);
    
    cout<<ans<<endl;   
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    solve();
    return 0;
}

01-trie 维护异或和

01-trie 是指字符集为 {0,1}的 trie。
01-trie 可以用来维护一些数字的异或和,支持

  1. 修改(删除 + 重新插入)
  2. 全局加一(即:让其所维护所有数值递增 1,本质上是一种特殊的修改操作)。

如果要维护异或和,需要按值从低位到高位建立 trie。

插入 & 删除

如果要维护异或和,我们 只需要 知道某一位上 0 和 1 个数的 奇偶性 即可,而不需要知道 trie 到底维护了哪些数字。
也就是对于数字 1 来说,当且仅当这一位上数字 1 的个数为奇数时,这一位上的数字才是 1。+
其余看oi-wiki

全局加一

所谓全局加一就是指,让这棵 trie 中所有的数值 +1。

我们思考一下二进制意义下 +1 是如何操作的:

我们只需要从低位到高位开始找第一个出现的 0,把它变成 1,然后这个位置后面的 1 都变成 0 即可。

对应 trie 的操作,其实就是交换其左右儿子,顺着 交换后 的 0 边往下递归操作即可。

01-trie 合并

01 trie 的合并和分裂和线段树没啥区别。

可持久化字典树

和其他可持久化数据结构没啥区别。
https://oi-wiki.org/ds/persistent-trie/

引用1
引用2
引用3

posted @ 2022-07-27 21:38  kingwzun  阅读(162)  评论(0编辑  收藏  举报