首页 > 其他分享 >可持久化Trie

可持久化Trie

时间:2024-06-22 16:53:48浏览次数:3  
标签:持久 Trie max tr ++ int root id

更好的体验

带注释的代码


    开始理解可持久化, 这里因为是acwing打卡, 可以放图片了
    有可能会用图片, 尽量打字
    
    可持久化trie, 就是一个trie树但是可以通过不同的开头(root), 变成每个历史状态
    这里就用到上面的图片了, 每次更新trie树, 这条新加入的链一定要, 开成新的, 即使前面
    有相同的的也不行(新加入的不能共用, 不然后面新的部分将更改前面的历史版本, 具体见上面图2)
    
    p是上一版本, q是这一版本, 可持久化, 只针对于上一版本变化
    总体思路就是, 每新加入一个东西, 看看这一层里面和它不同的, 向所有不同的连边(即tr[q][v] == tr[p][v]), 
    相当于复制了一边, 从这个根节点就可以, 通过这条边, 走到这些可以共用的
    对于重复的, 比如cab和cat的c和a, 我们不向它们连边, 新开一个点, 继续进行上面的操作
    
    说实话, 上来整01trie其实并不好, 但可持久化trie的题比较少, 后面我再打一个模版, 对于存单词的
    
    这就是可持久化trie的思路
    
    而对于这题, 有y的讲解, 很易懂, 我这里就粗略讲一下
    
    我们先预处理出来异或前缀和, 然后你能发现一个神奇的性质, 我从l ^ (l + 1) ^ ...  ^ r, 就等于sum[r] ^ sum[l - 1]
    因为 5 ^ 3 ^ 5 == 3对吧, 相当于[1, r] ^ [1, l - 1] = [l, r]; // 这里的[i, j], 是 i ^ (i + 1) ^ ... ^ j得到的数
    而我们是找一个再[l, r](这里指范围)里面的位置p, 然后求[p, n] ^ x, x是题目给出的某一个数
    这玩意就等价于, sum[n] ^ sum[p - 1] ^ x, 然后这里面固定的值是, sum[n] ^ x 我们把这个数设为C
    那么答案就是C ^ sum[p - 1]的最大值, p在[l, r]里面, 那么p - 1就在[l - 1, r - 1]里面;
    也就是说在[l - 1, r - 1]里面选一个异或前缀和, 使得sum[i] ^ C最大
    如果做过01trie的话就会发现, 这个玩意max(sum[i] ^ C)可以直接用01trie求出来(注意题目是让求最大值)
    这就把这个问题转化为了, 01trie, 
    
    这时候就看看这个范围怎么整[l - 1, r - 1](下面简写为[l, r]), 
    r好弄, 我们可以发现, 可以用可持久化trie, 在第r个版本里面搜, 就可以限制住右边界(因为没有r之后的数)
    现在就看左边界, 也好搞, 假设C这个数目前我拆分出来了1, 那么我最好选trie中的0
    这样可以保留这个1, 那么就看看0这棵树里面有没有下标大于l的节点, 而有没有下标大于等于l的节点, 
    就等价于这颗子树里的最大下标是否大于等于l, 就每个数记录一个max_id, 如果这个大于等于l, 就可以选这个0, 
    就可以进到0树里面, 并保留这个1, 否则就走1树
    
    这里求最大值可以像我一样保留1那样组合出这个数, 也可以通过找到那个点max_id[p]用sum[max_id[p]]去异或C
    
    至此此题结束
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 600101, M = 25 * N;

int tr[M][2], n, m;
int max_id[M], root[N], idx;
LL sum[N];

void insert(int i, int k, int p, int q) // 这里可以写成迭代的形式, 但是为了求max_id方便, 所以用递归的形式
{
    if (k < 0) // 说明到最后一个点
    {
        max_id[q] = i; // 那么最大下标就是它自己
        return ;
    }
    
    int v = sum[i] >> k & 1;
    if (p) tr[q][v ^ 1] = tr[p][v ^ 1]; // 连向另一边, 注意是连非当前点(v)的点的边, 并且这个点上一个版本必须有(没有的点我怎么共用, 凭空产生?)
    
    tr[q][v] = ++ idx; // 新加点
    insert(i, k - 1, tr[p][v], tr[q][v]); // 进入下一个点
    max_id[q] = max(max_id[tr[q][0]], max_id[tr[q][1]]); // 在儿子里面求最大下标
}

int query(int root, int C, int L)
{
    int p = root, res = 0; // res是组合出的这个数
    for (int i = 23; i >= 0; i -- )
    {
        int v = C >> i & 1;
        if (max_id[tr[p][!v]] >= L) p = tr[p][!v], res += 1 << i; 
        else p = tr[p][v];
    }

    return res; //  也可以写成 return C ^ sum[max_id[p]]; 因为最后p一定是异或最大的一条线里面的最后的一个点, 那么max_id[p]一定是异或C最大的
}

int main()
{
    cin >> n >> m;
    
    sum[0] = 0; // 开始的0, 因为可能会用到sum[0]; 
    max_id[0] = -1; // 必须小于0, 因为为了防止l - 1 == 0时这个0号历史方案(实际上什么都没有)被选上了, max_id[tr[p][!v]] >= L, 如果小于0的话就可以防止这种情况
    root[0] = ++ idx; // 这就是开始的0点
    insert(0, 23, 0, root[0]); 
    
    for (int i = 1; i <= n; i ++ )
    {
        int w;
        cin >> w;
        sum[i] = sum[i - 1] ^ w;
        root[i] = ++ idx;
        insert(i, 23, root[i - 1], root[i]);
    }
    
    while (m -- )
    {
        char op[2];
        int l, r, x;
        scanf("%s", op);
        if (*op == 'A') 
        {
            scanf("%d", &x);
            root[ ++ n] = ++ idx; // 注意这里 ++ n了长度已经变化了
            sum[n] = sum[n - 1] ^ x;
            insert(n, 23, root[n - 1], root[n]); 
        }
        else
        {
            scanf("%d%d%d", &l, &r, &x);
            printf("%d\n", query(root[r - 1], sum[n] ^ x, l - 1)); // 注意是l - 1, 和 r - 1
        }
    }
    
    return 0;
}

通用模版

/*
    通用的可持久化trie

    具体干什么
    给出n个字符串, 给出m个询问, 每个询问给出一个字符串s, 和一个限制r
    问在前r个字符串里面是否有字符串s
    如果有输出yes, 否则输出no

测试样例
3 4
a ab abc
ab 2
ab 1
abc 3
abc 2

yes
no
yes
no

*/
#include <iostream>
#include <cstring>
#include <algorithm>
#include <ctime>

using namespace std;

const int N = 10010;

int tr[N][27], idx;
int n, m, cnt[N];
int root[N];

void insert(string s, int p, int q) // 这是迭代版本的写法, 下面的扩展模版里面有递归写法
{
    for (int i = 0; s[i]; i ++ )
    {
        int v = s[i] - 'a';
        for (int j = 0; j < 26; j ++ ) // 复制可用边, 这里的复杂度可以以优化
            if (p && j != v) tr[q][j] = tr[p][j];
            
        // memcpy(tr[q], tr[p], sizeof tr[p]); // 相对高效的写法, 当然也快不了太多, 
        q = tr[q][v] = ++ idx; // 走向下一个/建立新点
        p = tr[p][v]; // 走向下一个
    }
    cnt[q] ++ ;
}

bool query(string s, int p)
{
    for (int i = 0; s[i]; i ++ )
    {
        int v = s[i] - 'a';
        if (!tr[p][v]) return false;
        p = tr[p][v];
    }

    return cnt[p];
}

int main()
{
    cin >> n >> m;

    root[0] =  ++ idx;

    for (int i = 1; i <= n; i ++ )
    {
        string s;
        cin >> s;
        // cout << s << endl;
        root[i] = ++ idx;  
        insert(s, root[i - 1], root[i]);
    }

    while (m -- )
    {
        string s;
        int r;
        cin >> s >> r;
        if (query(s, root[r])) puts("yes");
        else puts("no");
    }
    cout << clock() << endl;
    return 0;
}

扩展的通用模版

/*
    通用的可持久化trie

    具体干什么
    给出n个字符串, 给出m个询问, 每个询问给出一个字符串s, 和限制[l, r]
    问在[l, r]内的字符串里面是否有字符串s
    如果有输出yes, 否则输出no
    
    trie的每层的数量越多, 这玩意的消耗越大, 每次都得乘上以大常数, 所以一般都是01可持久化trie
    这样就常数小
    
测试样例
3 4
a cbc ab
ab 2 3
cb 1 3
aaaa 1 3
ab 1 2

输出样例
yes
no
no
no

*/
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 10010;

int tr[N][27], idx;
int n, m, cnt[N];
int root[N], max_id[N];

// void insert(string s, int p, int q)
// {
//     for (int i = 0; s[i]; i ++ )
//     {
//         int v = s[i] - 'a';
//         for (int j = 0; j < 26; j ++ ) // 复制可用边, 这里的复杂度可以以优化
//             if (p && j != v) tr[q][j] = tr[p][j];
//         q = tr[q][v] = ++ idx; // 走向下一个/建立新点
//         p = tr[p][v]; // 走向下一个
//     }
//     cnt[q] ++ ;
// }

void insert(int i, string s, int k, int p, int q) // 递归版本, 加上求 max_id; 当前的下标, 加入的串, 当前第几个字母, 上一个版本的这层, 这个版本的这层
{
    if (!s[k])
    {
        max_id[q] = i;
        cnt[q] ++ ;
        return ;
    }
    
    int v = s[k] - 'a';
    for (int j = 0; j < 26; j ++ )
        if (p && j != v) tr[q][j] = tr[p][j];  
    tr[q][v] = ++ idx;
    
    insert(i, s, k + 1, tr[p][v], tr[q][v]);
    int maxv = -1;
    for (int j = 0; j < 26; j ++ )
        maxv = max(maxv, max_id[tr[q][j]]);
    max_id[q] = maxv;
}

bool query(int l, string s, int p) // 递归的加上求max_id, 求区间的
{
    for (int i = 0; s[i]; i ++ )
    {
        int v = s[i] - 'a';
        if (!tr[p][v] || max_id[tr[p][v]] < l) return false;
        p = tr[p][v];
    }
    
    return cnt[p];
}

// bool query(string s, int p) // 普通, 
// {
//     for (int i = 0; s[i]; i ++ )
//     {
//         int v = s[i] - 'a';
//         if (!tr[p][v]) return false;
//         p = tr[p][v];
//     }

//     return cnt[p];
// }

int main()
{
    cin >> n >> m;

    root[0] =  ++ idx;
    
    for (int i = 1; i <= n; i ++ )
    {
        string s;
        cin >> s;
        root[i] = ++ idx;  
        insert(i, s, 0, root[i - 1], root[i]);
    }

    while (m -- )
    {
        string s;
        int l, r;
        cin >> s >> l >> r;
        if (query(l, s, root[r])) puts("yes");
        else puts("no");
    }

    return 0;
}

图片

图一
屏幕截图 2023-10-29 205807.png

图二
WIN_20231030_19_38_32_Pr0.jpg

图三
屏幕截图 2023-10-30 143556.png

图四
屏幕截图 2023-10-30 214644.png

标签:持久,Trie,max,tr,++,int,root,id
From: https://www.cnblogs.com/blind5883/p/18262493

相关文章

  • [字符串专题] KMP、Hash、Trie
    KMP核心思想:在每次失配时,不是把p串往后移一位,而是把p串往后移动至下一次可以和前面部分匹配的位置,这样就可以跳过大多数的失配步骤。而每次p串移动的步数就是通过查找next数组确定的。KMP主要分两步:求next数组、匹配字符串,其难点在于如何求next数组for(inti=1,......
  • vue3 详细配置 pinia,以及持久化处理
    安装piniapnpminstallpiniapnpminstallpinia-plugin-persistedstate使用pinia根目录下创建store文件夹,新家moudules文件夹和index.ts文件,如图:index.ts文件//store/index.ts//仓库大仓库import{createPinia}from"pinia";importpiniaPluginPersisteds......
  • Redis的持久化机制和缓存预热
    Redis的持久化机制Redis是一个内存数据库,它的数据存放在内存中,但是如果关闭服务、机器关机或者断电的话,内存中的所有数据都会慢慢消失消失。内存数据消失的原因:因为内存中的数据是半导体晶体管开关,这种开关高度依赖电源,当电源断电后,无法再控制晶体管的开关状态。这时候电容发挥......
  • 12.1.k8s中的pod数据持久化-pv与pvc资源及动态存储StorageClass
    目录一、pc与pvc的概念二、pvc与pv初体验1,准别nfs环境1.1.所有节点安装nfs工具1.2.harbor节点编辑nfs配置文件 2,创建3个pv资源3,harbor节点创建pv对应的nfs存储路径 4,创建pvc关联pv5,创建pod引入pvc6,编辑index访问文件到harbor存储目录下7,浏览器访问测试三、Storag......
  • 论文阅读:Corrective Retrieval Augmented Generation
    CorrectiveRetrievalAugmentedGeneration(https://arxiv.org/pdf/2401.15884.pdf)https://github.com/jiangnanboy/paper_read_note一.序言RAG即检索增强生成(retrievalaugmentedgeneration),当检索到不准确的数据时,会产生对模型的生成干扰。CorrectiveRetrievalAugme......
  • redis——P3:持久化
    虽然缓存功能已经实现,但是作为对外提供服务的软件开发者,不能只关注是否提供了正确的服务,稳定和快速恢复等等指标是同样非常非常重要的。考虑这样一个问题,redis确实因为不可抗力宕机了(假设我喜欢黑框框打开,然后手贱按了^C),于是瞬间redis里所有缓存全没了?这对于一个追求高性能、高可......
  • 详解Redis 的持久化和主从复制
    在这篇文章,一起了解一下其中一个非常重要的内容:Redis的持久化机制。什么是Redis持久化?Redis作为一个键值对内存数据库(NoSQL),数据都存储在内存当中,在处理客户端请求时,所有操作都在内存当中进行,如下所示:  这样做有什么问题呢?其实,只要稍微有点计算机基础知识的人都知道,存......
  • 鸿蒙——数据持久化存储(AppStorage、PersitentStoreage、数据库、首选项)
    Localstorage-内存化存储-局部可用AppStorage-内存化存储-全局可用PersitentStoreage-写入磁盘(沙箱)全局可用首选项-写入磁盘-全局可用关系型数据库-写入磁盘1.用户首选项:获取Preferences实例、保存/更新数据、获取数据用户首选项为应用提供Key-Value键值型的数据处......
  • 手把手教你改造 Sentinel Dashboard 实现配置持久化
    一.概述Sentinel客户端默认情况下接收到Dashboard推送的规则配置后,可以实时生效。但是有一个致命缺陷,Dashboard和业务服务并没有持久化这些配置,当业务服务重启后,这些规则配置将全部丢失。Sentinel提供两种方式修改规则:通过API直接修改(loadRules)通过DataSource适配......
  • 游戏缓存与异步持久化的完美邂逅
    1、问题提出游戏服务器,需要频繁的读取玩家数据,同时也需求频发修改玩家数据,并持久化到数据库。为了提高游戏服务器的性能,我们应该怎么处理呢?2、应用程序缓存缓存,是指应用程序从数据库读取完数据之后,就将数据缓存在进程内存或第三方内存(例如redis)。游戏服务器对于玩家数据的读......