『学习笔记』字典树

字典树(Trie)是一种用于操作字符串的树型结构,可以用来存储和查询字符串。

例如,在一棵字典树中插入 \(\texttt{iak,ioi,akly,me,fake,ccf}\) 后,这棵树长这样:

图中橙色节点为存在的字符串,必须标记一下。否则,像图中 \(\texttt{a,c,f,i,m,cc,fa,ia,io,akl,fak}\) 这些子串,都会被认为是插入的字符串。

Trie 的根节点始终为空,它的作用就是存储通向子树的边。这些边各有各的权值,都是一个字符。

那么每个节点表示的字符串即为:父节点表示的字符串,末尾添加父节点通向该节点的边权。

例如 \(\texttt{ak}\),它的父节点为 \(\texttt{a}\),通向它的边权为 \(\texttt{k}\),故这个节点的值为 \(\texttt{a}\) 的末尾加上 \(\texttt{k}\),即 \(\texttt{ak}\)

下面来看看具体操作过程吧。

插入

插入过程中,先从根节点开始搜,搜插入字符串的第一个字符,如果第一个字符的儿子不存在,那么创建节点,并进入。否则直接进入。一直这样搜到字符串末尾,于是最后到达的节点就是要去的节点了,标记这个节点,就完成了插入过程。

例如,要在上面的字典树中插入字符串 \(\texttt{meet}\)

首先,从根节点寻找边权为第一个字符 \(\texttt{m}\) 的边,发现已有,则进入,到达 \(\texttt{m}\) 节点。

接着,寻找第二个字符 \(\texttt{e}\),还是有,进入子节点。

接着找第三个字符 \(\texttt{e}\),发现没有这条边,创建儿子,并进入。

最后找最后一个字符 \(\texttt{t}\)\(\texttt{mee}\) 节点同样没有权值为 \(\texttt{t}\) 的出边,创建。

还有不要忘了标记它。

插入完成后的 Trie 长这样:

代码也很好写,就和模拟一样。

void insert(char *s){ // 传入一个字符串
    u=0,l=strlen(s); // u 为当前的节点编号(初始为根节点0),l 为字符串长度
    for(int i=0; i<l; i++){ // 遍历整个字符串
        v=s[i]-'a'; // 获取第 i 个字符的权值
        // son[u][v] 表示第 u 个节点,权值为 v 的边,指向的节点的编号
        // 若 u 节点的权值为当前字符的边指向的儿子为空,说明没有这个儿子,创建
        if(!son[u][v]) son[u][v]=++top;
        u=son[u][v]; // 然后进入子节点
    }
    qt[u]++; // 标记,这里直接记录了相同字符串的个数
}

查询

查询也和插入一样,遍历一遍字符串,若遍历过程中发现要去的儿子为空,那么肯定没有要查询的字符串,直接返回。如果最后存在一个值为要查找的字符串的节点,直接返回 qt[u],即要查询的字符串个数。若以前没有插入过这个字符串,那么 qt[u] 就为 \(0\),不影响答案。

如果要查询 \(\texttt{io}\) 是否存在:

第一个字符为 \(\texttt{i}\),发现有这个儿子,进入。

第二个字符 \(\texttt{o}\) 也有,进入。

这样便找到了 \(\texttt{io}\) 节点,但是,qt 数组告诉我们,之前插入的字符串中并没有出现 \(\texttt{io}\),所以没有 \(\texttt{io}\) 这个字符串。

int find(char *s){
    u=0,l=strlen(s);
    for(int i=0; i<l; i++){
        v=s[i]-'a';
        if(!son[u][v]) return 0; // 没有对应的儿子,退出
        u=son[u][v]; // 进入相应的儿子
    }
    return qt[u]; // 直接返回个数
}

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

题目大意

给定 \(n\) 个字符串,只含小写字母且长度不超过 \(50\),并给出 \(m\) 个询问,每次询问给出一个字符串,若第一次询问这个字符串,且这个字符串存在,输出 \(\texttt{OK}\)。若这个字符串存在且不是第一次询问,输出 \(\texttt{REPEAT}\)。若该字符串不存在,输出 \(\texttt{WRONG}\)

思路

所有字符串依次插入 Trie,对于询问,若第一次询问得到,该字符串存在,那么将对应的 qt 改为 \(2\),并返回 \(1\)。否则返回 \(0\)

那么主函数中,对于每个询问,若返回值为 \(0\),输出 \(\texttt{WRONG}\)。为 \(1\),输出 \(\texttt{OK}\),为 \(2\),就输出 \(\texttt{REPEAT}\)

代码

#include <iostream>
#include <cstring>
using namespace std;
const int N=5e5+5,M=26;
int n,m;
char s[N];

class Trie{
    public:
        Trie(int (*_cvt)(char c)=[](char c)->int{return c-32;}):
            top(0),cvt(_cvt){clear();}
        void clear(){
            for(int i=0; i<N; i++){
                for(int j=0; j<M; j++)
                    son[i][j]=0;
                qt[i]=0;
            }
        }
        void insert(char *s,int l){
            u=0;
            for(int i=0; i<l; i++){
                v=cvt(s[i]);
                if(!son[u][v]) son[u][v]=++top;
                u=son[u][v];
            }
            qt[u]=1;
        }
        int find(char *s,int l){
            int u=0;
            for(int i=0; i<l; i++){
                int v=cvt(s[i]);
                if(!son[u][v]) return 0;
                u=son[u][v];
            }
            if(qt[u]==1){
                qt[u]=2;
                return 1;
            }
            return qt[u];
        }
    private:
        int n,u,v;
        int son[N][M],top;
        int qt[N];
        int (*cvt)(char c);
}t([](char c)->int{return c-'a';});

int main(){
    scanf("%d",&n);
    for(int i=1; i<=n; i++){
        scanf("%s",s+1);
        t.insert(s+1,strlen(s+1));
    }
    scanf("%d",&m);
    while(m--){
        scanf("%s",s+1);
        switch(t.find(s+1,strlen(s+1))){
            case 0: puts("WRONG"); break;
            case 1: puts("OK"); break;
            case 2: puts("REPEAT"); break;
            default: break;
        }
    }
    return 0;
}

01Trie

有这么一类问题:给定一个数,求某些数中,与这个数的最大异或值。

那么什么情况下两个数异或值最大?

首先我们要知道,异或运算,可以理解为两个二进制数是否不一样。

也就是说,它会把两个十进制数转换为二进制,并逐位比较,若不一样,则这一位为 \(1\)。否则为 \(0\)

那么很显然,这两个数的二进制位不一样的位越多,得到的结果就越大。

于是我们就可以用 01Trie 来维护各个数的二进制。

从根节点开始的儿子,就是数的二进制最高位了。

01Trie 的边的权值只有两种:\(0\)\(1\)

查询

先来说说查询吧。

那么,给你了一棵构造好的 01Trie 和一个数,让你求出 01Trie 中的数,与这个数的最大异或值,怎么求?

肯定要尽量让每一位都不一样啦!

高位肯定是优先要不一样的,就算后面 \(n\) 位全是 \(1\),也没有第 \(n+1\) 位是 \(1\) 要大。

那么我们从高位开始贪心,看看有没有权值与当前位不一样的边。如果有,那么就进这条边指向的子节点搜。否则,就只能搜另一条了。

比如,有这么一棵 01Trie:

我们要插入一个二进制数 \(\texttt{111}\)

首先,这个数的第一位为 \(1\),取反,得到 \(0\),发现有权值为 \(0\) 的出边,进入。

第二位也是 \(1\),但是发现没有权值为 \(0\) 的出边,那就只好走 \(1\) 了。

第三位还是 \(1\),有权值为 \(0\) 的出边,直接走,并结束搜索。

在搜索过程中,如果能走反边,那么就需要记录答案。

能走反边,意味着这一位肯定为 \(1\),答案加上对应的 \(2^i\) 即可。

ll find(ll x){
    u=0,ans=0;
    // 循环初始值由值域决定的,为 log_2(值域)
    for(int i=32; i>=0; i--){
        v=(x>>i)&1; // x 右移 i 位再 &1,就可以得到
        if(son[u][!v]) u=son[u][!v],ans+=1<<i; // 可以走反边
        else u=son[u][v];
    }
    return ans;
}

插入

也就和普通 Trie 的差不多,就不多说了。

void insert(ll x){
    u=0;
    // 这个循环初始值必须与查询的相同,这个值等于这棵 01Trie 的高度
    for(int i=32; i>=0; i--){
        v=(x>>i)&1;
        if(!son[u][v]) son[u][v]=++top;
        u=son[u][v];
    }
}

U109895 [HDU4825]Xor Sum

题目大意

给定一个包含 \(n\) 个正整数的集合 \(a\),然后给出 \(m\) 个询问。

对于每个询问,给出一个数 \(s\),求一个正整数 \(k \in a\),使得 \(s \oplus k\) 最大。

思路

\(a\) 建一棵 01Trie,但查询不是求异或和了,而是求另一个参与异或的数。

统计答案的思路和上面差不多,每向下搜一层,就将当前层的数代表的值求出来,加入答案即可。

代码

#include <iostream>
#include <cstring>
using namespace std;
template<typename T=long long>
inline T read(){
    T X=0; bool flag=1; char ch=getchar();
    while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
    while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
    if(flag) return X;
    return ~(X-1);
}

template<typename T=int>
inline void write(T X){
    if(X<0) putchar('-'),X=~(X-1);
    T s[20],top=0;
    while(X) s[++top]=X%10,X/=10;
    if(!top) s[++top]=0;
    while(top) putchar(s[top--]+'0');
    putchar('\n');
}

typedef long long ll;
const int N=1e8+5;
ll n,m,x;

class Trie01{
    public:
        Trie01():top(0){clear();}
        void clear(){
            for(int i=0; i<n; i++)
                son[i][0]=son[i][1]=0;
        }
        void insert(ll x){
            u=0;
            for(int i=32; i>=0; i--){
                v=(x>>i)&1;
                if(!son[u][v]) son[u][v]=++top;
                u=son[u][v];
            }
        }
        ll find(ll x){
            u=0,ans=0;
            for(int i=32; i>=0; i--){
                v=(x>>i)&1;
                if(son[u][!v]) u=son[u][v=!v]; // 这里要写 v=!v,如果要去反边,那么要加上答案的数也有变动
                else u=son[u][v];
                ans+=v<<i;
            }
            return ans;
        }
    private:
        int son[N][2];
        ll top,u,v,ans;
}t;

int main(){
    n=read(),m=read();
    while(n--)
        t.insert(read());
    while(m--)
        write(t.find(read()));
    return 0;
}

推荐习题

posted @ 2022-06-30 16:32  仙山有茗  阅读(28)  评论(0编辑  收藏  举报