Trie字典树

文章目录

  • 什么是 T r i e Trie Trie
    • 一般条件
  • AcWing 835. Trie字符串统计
    • CODE
      • 解释一下 i n s e r t ( ) insert() insert() 函数
      • i d x idx idx 的意义
  • AcWing 143. 最大异或对
    • 异或
    • 思路解析
    • CODE
      • 代码解析
  • 给点思考



什么是 T r i e Trie Trie

一种树结构,用来存储字符串,能够查询某字符串是否存在

Trie字典树_第1张图片

  • 由一个统一的根节点 r o o t root root 发散开,存储字符
    • 如果下一个字符之前有用过,就顺着之前的路线往后走
    • 如果下一个字符与之前的某串不重合,就另开一个路线继续走下去
  • 最后如果串存完了在末尾打个标记
    • 比如:之前存过字符串 ′ a b c d e f ′ 'abcdef' abcdef,我们再存 ′ a b c ′ 'abc' abc 就会发现后者是前者的子串,如果不打标记就发现不了这个串

一般条件

一般题目中都会说明是全大写字母或者全小写字母,所以说数组开的范围不会太大,当然也有大范围。(我在写啥???《-_-》)


AcWing 835. Trie字符串统计

板子题:https://www.acwing.com/activity/content/problem/content/883/
Trie字典树_第2张图片

CODE

#include 

using namespace std;

// 定义常量N为100010,这是我们预设的最大节点数量
const int N = 100010;

// son数组用于存储每个节点的子节点,每个节点有26个子节点,对应26个英文字母
// cnt数组用于存储每个节点结束的单词数量
// idx用于节点编号,每当我们创建一个新节点时,idx会加1
// str用于存储输入的字符串
int son[N][26], cnt[N], idx;
char str[N];

// 插入函数,用于将一个字符串插入到字典树中
void insert(char *str)
{
    // p是当前节点的编号,一开始我们在根节点,所以p=0
    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];		// 不能标记为idx,因为可能前面部分串有过记录,用idx就不对了
    }
    // 遍历完字符串后,我们在一个节点结束,所以该节点的单词数量加1
    cnt[p] ++ ;				// 这个也是同理,不能用idx
}

// 查询函数,用于查询一个字符串在字典树中出现的次数
int query(char *str)
{
    // p是当前节点的编号,一开始我们在根节点,所以p=0
    int p = 0;
    // 遍历输入字符串的每个字符
    for (int i = 0; str[i]; i ++ )
    {
        // 计算当前字符对应的编号
        int u = str[i] - 'a';
        // 如果当前节点没有对应的子节点,说明字符串不存在,返回0
        if (!son[p][u]) return 0;
        // 转移到子节点
        p = son[p][u];
    }
    // 返回当前节点的单词数量,即字符串在字典树中出现的次数
    return cnt[p];
}

// 主函数
int main()
{
    // n是操作数量
    int n;
    scanf("%d", &n);
    // 处理每个操作
    while (n -- )
    {
        // op是操作类型,str是操作的字符串
        char op[2];
        scanf("%s%s", op, str);
        // 如果操作类型是"I",则插入字符串
        if (*op == 'I') insert(str);
        // 否则,查询字符串并打印出现次数
        else printf("%d\n", query(str));
    }

    return 0;
}

解释一下 i n s e r t ( ) insert() insert() 函数

  • 首先读入了待插入的字符串str
  • 找到根节点 r o o t root root —> p = 0,也就是在son[0]位置是我们的根节点位置,由这个位置往后寻找
    • 先读入下一个字符,然后在后面 26 26 26 个字符空间中看这个字符是否被标记过
      • 如果被标记过则说明之前的某一字符串的这部分跟插入的串重合,拿着标记号去跟着之前的串走,也就是代码:
      p = son[p][u];
      
      • 如果未标记则说明之前没有串跟它重合,则需要自己开一条线往后走,同时给这个字符赋予新的编号++idx,代表我走过这条路,索引号是idx,也就是代码:
      if (!son[p][u]) son[p][u] = ++ idx;
      
  • 最后串读完了,打个标记,就是字符串最后的字符拿到的编号为索引的数量数组cnt[p]++
  • 对于 q u e r y ( ) query() query() 函数也是一样的思路

i d x idx idx 的意义

idx

Trie字典树_第3张图片
Trie字典树_第4张图片

素材来源:https://www.acwing.com/solution/content/5673/ の评论区
还是评论区大神多啊,orz %%%


AcWing 143. 最大异或对

题目链接:https://www.acwing.com/activity/content/problem/content/884/
Trie字典树_第5张图片

异或

  • 异或俗称不进位加法,操作就是将两个二进制数的每一位对比,如果两位不一样(一个 0 0 0 一个 1 1 1 ),那么就记为 1 1 1 ,否则记为 0 0 0
  • 这种运算等价于将两个二进制数加起来,但是每位不进位

思路解析

  • 既然求异或最大,先固定一个数,遍历另一个数,那么最完美的情况就是每一位都不一样,此时异或值最大,虽然这个数存在是有可能的,但是存在一个这样的数不太可能。
  • 那么虽然每一位完全不同的数可能不存在,那我们就利用贪心思想,每一位尽可能选择不一样的,最后得到的就是我们需要的数
  • 由于是随机两个数进行组合,我们只需要固定一个数,然后枚举之前插入过 T r i e Trie Trie 树里面的数即可,最后再把这个数插入字典树

Trie字典树_第6张图片

CODE

万事可暴力:

int main(){
	int n;
	cin >> n;
	for(int i = 0; i < n; ++i) scanf("%d", &a[i]);

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

充斥了暴力美学啊,可惜我猪脑不动的,暴力都没想到打。

T r i e Trie Trie

#include 
#include 
#include 

using namespace std;

const int N = 1e5 + 10, M = 31 * N; // 定义数组的大小
int n; // 元素的数量
int son[M][2], idx; // Trie数据结构

// 将一个数字插入到trie中的函数
void insert(int a){
    int p = 0;
    
    // 遍历每个位
    for(int i = 30; i >= 0; --i){
        int u = a >> i & 1;
        if(!son[p][u]) son[p][u] = ++idx;
        p = son[p][u];
    }
}

// 查询函数
int query(int a){
    int p = 0, num = 0;
    
    for(int i = 30; i >= 0; --i){
        int u = a >> i & 1;
        
        if(son[p][!u]){
            num = (num << 1) + !u;
            p = son[p][!u];
        }else{
            num = (num << 1) + u;
            p = son[p][u];
        }
    }
    
    return num;
}

int main()
{
    cin >> n;

    int a, res = 0;
    while (n -- ){
        scanf("%d", &a);
        
        insert(a);
        int t = query(a);
        
        res = max(res, a ^ t);
    }
    
    cout << res << endl;
}

代码解析

  • 读入数a,先将其插入到字典树中,然后寻找树中插入过的数字与其“最大配偶”最接近的一个
    • i n s e r t ( ) insert() insert():将a按位插入字典树中,从高位到低位

      T i p Tip Tip i n t int int 类型只有 31 31 31 位表示数据的值,最高位的 0   o r   1 0\ or\ 1 0 or 1 代表了正负

    • q u e r y ( ) query() query():按位搜寻,如果存在位的值相反的数,那么就随着相反的值走,不存在就随相同的值走
  • 将查询得到的配偶取异或,存最大值

给点思考

  • 不管是 T r i e Trie Trie 还是字符哈希,他们好像都是查询作用的结构
    • T r i e Trie Trie 树:按每个字符进行查询
    • 字符哈希:按照区间进行查询
    • 所以说想要用字符串查询,用 T r i e Trie Trie 树;想要用区间 [ l , r ] [l, r] [l,r] 来查询,得用哈希表
  • 对于 T r i e Trie Trie 来说,理论上可以存储任何数据,因为所有数据终究是一堆 0 0 0 1 1 1 ,那么用 T r i e Trie Trie 存的话就变成了一棵二叉树,所以说 T r i e Trie Trie 不仅可以存字符串,也可以存数字
  • 如果我们再遇到从一堆数里面拿出两个进行操作时,我们可以像本题一样,先枚举第一个,第二个枚举到第一个数为止
    • 比如 ( 1 , 2 , 3 , 4 ) (1, 2, 3, 4) (1,2,3,4),我们i枚举到 3 3 3i, j组合就是 ( 1 , 3 ) , ( 2 , 3 ) , ( 3 , 3 ) (1, 3), (2, 3), (3, 3) (1,3),(2,3),(3,3) ,那 ( 4 , 3 ) (4, 3) (4,3) 咋办?我们枚举 4 4 4 的时候自然会枚举到 ( 4 , 3 ) (4, 3) (4,3)
    • 也就是公式 C n 2 = n × ( n − 1 ) 2 C_{n}^{2} = \frac{n \times (n - 1)}{2} Cn2=2n×(n1)

你可能感兴趣的:(算法学习记录,算法,c++,数据结构)