可持久化数据结构

可持久化数据结构必须满足在操作过程中数据结构本身的拓扑结构不变,可以用来存下数据结构的所有历史版本。

核心思想:只记录每一个版本与前一个版本不一样的地方。

这里我们讨论两种数据结构的可持久化——trie和线段树。

Trie的可持久化

由于之前没有整理过trie的模板,这里我们先来整理trie的模板和用法。

trie是一个树形结构,实现很像是对单链表进行超级升级。我们用一个例题来讨论一下:

835. Trie字符串统计(活动 - AcWing)

这里我们首先要储存一下这些,单词,这里我们用trie树来储存这些单词,图解如下:

就这样建一棵树,标记最后一个字母在的位置,那么如此就存下来了所有的单词。那么具体到代码层面,这棵树该如何实现呢,我们可以借用单链表那里的用下标代替指针的方法。定义一个idx,每次新加一个节点就让这个节点的下标作为++idx,并且把这个下标与它的父节点联系起来。这里只有根节点和空节点的序号是0,由此来判断是否某个节点已经存在。

所以我们可以用一个二维数组son[N][26]来存每个节点对应的子节点信息,然后通过p=son[p][i]实现从节点p转移到节点i的过程。至于具体到每个节点的信息,我们可以用另外的数组来存。因为很显然每个节点对应的序号都是不同。这里需要知道次数,那么我们可以定义一个数组cnt[N]表示第i个节点作为单词结尾的次数,因为序号不同,所以这样可以实现。

然后在查找的时候,只要扫描待查找的字符串,并且沿着树的路径转移就可以实现查找。

#include<bits/stdc++.h>
using namespace std;
const int N=200010;
int son[N][26];
int cnt[N];
int idx;
void insert(string s)
{int p=0;for(int i=0;s[i];i++)//因为在c++的输入中字符串是以\0结尾的,所以我们可以借此来判断是否访问到结尾了{int v=s[i]-'a';if(!son[p][v]) son[p][v]=++idx;p=son[p][v];}cnt[p]++;
}
int query(string s)
{int p=0;for(int i=0;s[i];i++){int v=s[i]-'a';if(!son[p][v]) return 0;p=son[p][v];}return cnt[p];
}
int main()
{int n;scanf("%d",&n);while(n--){string op,s; cin>>op>>s;if(op[0]=='I'){insert(s);}else{cout<<query(s)<<endl;}}
}

 143. 最大异或对(活动 - AcWing)

思路:上题是储存字母,而这道题是储存数,对于数我们可以用二进制来储存。即根据二进制建一棵01树。这里要找最大异或对,那么我们就对每个数进行搜索,然后尽量往它对应位的另一个分支转移,最后得到的数就是和它异或最大的数,然后我们统计最大值。(因为异或是相异为1,所以我们要尽可能的使数相反)

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=3100010;//每个数有31位
int son[M][2],idx;
int a[N];
void insert(int x)
{int p=0;//从高位往低位存for(int i=31;i>=0;i--){int v=x>>i&1;if(!son[p][v]) son[p][v]=++idx;p=son[p][v];}
}
int query(int x)
{int p=0,res=0;for(int i=31;i>=0;i--){int v=x>>i&1;if(!son[p][v^1]) p=son[p][v],res+=(v)<<i;else p=son[p][v^1],res+=(v^1)<<i;}return res^x;
}
int main()
{int n;scanf("%d",&n);for(int i=1;i<=n;i++){int x;scanf("%d",&x);insert(x);a[i]=x;}int mx=0;for(int i=1;i<=n;i++){mx=max(mx,query(a[i]));}cout<<mx;
}

现在我们了解了什么是trie,那么就来讨论可持久化trie是什么东西。

我们假设需要插入四个新单词,每次插入的时候将上一次的信息copy到新的节点,有区别的地方就分裂一个新的点出来。

所以大概的框架如下:

p=root[i-1],q=root[i]=++idx;
if(p) tr[q]=tr[p];
tr[q][si]=++idx;
p=tr[p][si],q=tr[q][si];

 这么来看,我们给每个根节点赋一个下标,然后当需要插入时,我们定义变量p表示上个版本的根节点,变量q表示当前版本的根节点。然后这里的if(p)就是查询p的这个方向是否存在,如果存在,那么就把p的所有情况copy给q,准确的来说,是将p所有q接下来不会转移的方向copy给q,对应上图就是版本2中向左指的箭头。然后顺着q的下一步建一个新节点,这个转移方向之前已经出现过也没关系,反正我们没把它copy到当前版本来,那么就不会出现重复的问题。可以参考版本3,前两步的转移之前出现过,但是我们仍然新建了节点,并没有把之前的copy过来。然后两者同时进行转移,按照当前的方向进行转移。

所以总的来说,就是定义两个变量表示指针,一个从上一个版本的根节点开始,一个从当前版本的根节点开始,进行同步转移,一旦某一步在上个版本中有分叉,那么就将分叉全部copy到当前版本的这个节点来,因为我们相当于链表,所以只要将下标处理得当即可。

我们用一个例题来详细介绍一下这个该如何实现。

256. 最大异或和(256. 最大异或和 - AcWing题库)

这里的核心问题在于我们需要从[l,r]中选一个p,然后计算出a[p]^a[p+1]^a[p+2]^...^a[n]^x,这个该如何实现呢?我们先假设这个p是任意的而非从[l,r]中选出来的,这个值该怎么算呢?显然我们可以借用前缀和的思想s[n]^s[p-1]=a[p]^a[p+1]^a[p+2]^...^a[n]。所以我们实际上可以让每个版本新插入的都是s[i],然后在访问的时候我们只要访问第r-1个版本即可访问到之前所有的s[i],为什么是r-1给版本呢,由上面的推导s[p-1],p最大到r,所以应该是从r-1版本开始找。

#include<bits/stdc++.h>
using namespace std;
const int N=600010,M=25*N;
int n,m;
int root[N],tr[M][2],s[N],maxi[M],idx;
void insert(int i,int k,int p,int q)
{if(k<0){maxi[q]=i;return;}int v=s[i]>>k&1;if(p) tr[q][v^1]=tr[p][v^1];tr[q][v]=++idx;insert(i,k-1,tr[p][v],tr[q][v]);maxi[q]=max( maxi[tr[q][0]] , maxi[tr[q][1]] );
}
int query(int root,int c,int l)
{int p=root;for(int i=23;i>=0;i--){int v=c>>i&1;if(maxi[ tr[p][v^1] ]>=l) p=tr[p][v^1];else p=tr[p][v];}return c^s[maxi[p]];
}
int main()
{scanf("%d%d",&n,&m);maxi[0]=-1;//root[0]=++idx;insert(0,23,0,root[0]);for(int i=1;i<=n;i++){int x;scanf("%d",&x);s[i]=s[i-1]^x;root[i]=++idx;insert(i,23,root[i-1],root[i]);}while(m--){char op[2];scanf("%s",op);if(op[0]=='A'){int x;scanf("%d",&x);//s[++n]=s[n-1]^x;先执行右边然后执行自增,然后执行赋值n++;s[n]=s[n-1]^x;root[n]=++idx;insert(n,23,root[n-1],root[n]);}else{int l,r,x;scanf("%d%d%d",&l,&r,&x);cout<<query(root[r-1],s[n]^x,l-1)<<endl;}}
}

可持久化线段树

可持久化线段树的图解及实现:

按照上图,实际上就是将被修改路径上的点的左右指向进行替换即可,剩下的点实际上是不变的。这里我们的l,r存的是左右子节点的下标,不再是区间范围。 

可持久化线段树可以这么来理解,每个版本的线段树结构实际是一样的,不同的地方就在于每个节点储存信息是不同的,每个节点储存的信息包括它的左右子节点的下标,这里就给了我们修改区别不同版本的思路,每个版本有一个单独的根节点,每个根节点的序号是不同的,那么在数组中对应的位置也不同,而我们可以用一个二维数组来储存每个节点的左右子节点,每次对某个点修改的时候,我们就新建一个点,它的父节点是一个新定义的点,父节点原本的左右指向是从上一个版本的相同位置的节点copy的,这里我们更新子节点后,就可以进一步更改父节点的左右指向,然后我们从不同版本的root开始访问的时候访问到指向就是不同的,也就意味着访问的路径是不同的。也就实现了版本间的区别。

不过这个数据结构难以进行区间修改操作,因为它很难处理懒标记。

我们结合一个例题具体来看看。

255. 第K小数(255. 第K小数 - AcWing题库)

这是一个静态问题,在查询的过程中,原数组是不变的。

有几种经典做法:划分树(O(mlogn)),树套树(线段树套平衡树,线段树的每个节点存一个平衡树,平衡树用来把[l,r]中的数维护成一个有序序列,每次查询(logn)^2,它是支持修改的,空间复杂度是O(nlogn)),可持久化线段树(时间/空间:O(nlogn),又叫主席树)

这里我们就按照线段树来思考一下我们可以怎么处理,我们可以在数值上建立线段树,也即线段树维护的是某个区间中的数有多少个,我们可以每插入一个新的数,就生成一个新版本,那么如果从root[r]开始访问就可以得到前r个数生成的线段树。我们另外储存了一个cnt,而且由于我们是在数值上建立的线段树,所以我们可以得到[1,r]这些数的一些性质。

这里我们先把如何建立线段树的过程再理一下,首先我们需要维护n个数,那么我们就先建立一棵有n个子节点的线段树,怎么建呢?先将所有需要插入的数进行离散化,因为它们的范围太大了,然后我们开始建树,每个节点有一个下标,然后在递归的时候,我们从中间分开,tr[u].l=build(l,mid),tr[u].r=build(mid+1,r);因为我们在建点的时候是会给每个点赋一个下标的,或者可以理解成指针,每个点的l,r就指向它左右节点的下标。

然后考虑如何插入值,显然我们先将待插入的值进行离散化,,找到它在我们维护的区间中的位置,然后因为插入一个新的值需要新建一个版本,所以我们实际上就需要新开一个根节点,然后将之前的根节点的值赋值过来,实际上就是把原来根节点的左右指针赋过来,然后查找的时候,因为我们维护的是数值上的线段树,所以虽然我们没有将每个节点表示的区间确切的存下来,但我们可以通过对区间的计算划分然后和待插入的值进行比较,进而找到当前这个值该插在什么位置,然后再往上一层一层更新。

然后就是查找如何实现,这个是这道题最麻烦的一点,我们如果想获得[1,r]之间第k大的数,那么实际上我们只要看第r个版本,然后因为维护的数值区间是默认的,所以我们假设第一步查询的时候取了mid(数值上的),那么我们去看左子区间的cnt的值,如果这个值是大于等于k的,就说明小于等于mid的值不少于k个,所以第k大的数一定在左半边,那么我们就该往左子区间进行递归,否则就应该往右子区间进行递归。

但是我们实际上需要找的并不是[1,r],而是[l,r]之间的第k大的数。这个该怎么来找呢?我们对第r个版本和第l-1个版本进行查询,令a=tr[r].cnt,b=tr[l-1].cnt,那么a-b就可以得到在[l,r]区间中小于等于mid的数的个数,如果这个个数是大于等于k的,很显然就应该往左子区间递归,否则显然应该往右子区间递归。这个还是有点抽象,最核心的就是要明白这里cnt的含义,cnt表示的是区间中数的个数,而区间我们则是隐形的对最大的区间进行不断地二分,这也是抽象的地方,需要多思考一下。那么我们通过不断地递归,就可以落脚在某个叶子节点上,这个叶子节点即我们需要找的数,因为离散过,所以我们要处理一下再输出。

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m,idx;
struct node{int l,r;int cnt;
}tr[4*N+17*N];
int root[N],a[N];
vector<int>num;
int find(int x)
{return lower_bound(num.begin(),num.end(),x)-num.begin();
}
int build(int l,int r)
{int p=++idx;if(l==r) return p;int mid=l+r>>1;tr[p].l= build(l,mid),tr[p].r=build(mid+1,r);return p;
}
int insert(int p,int l,int r,int x)
{int q=++idx;tr[q]=tr[p];if(l==r) {tr[q].cnt++;return q;}int mid=l+r>>1;if(x<=mid) tr[q].l=insert(tr[q].l,l,mid,x);else tr[q].r=insert(tr[q].r,mid+1,r,x);tr[q].cnt=tr[tr[q].l].cnt+tr[tr[q].r].cnt;return q;
}
int query(int q,int p,int l,int r,int k)
{if(l==r) return r;int cnt=tr[tr[q].l].cnt-tr[tr[p].l].cnt;int mid=l+r>>1;if(cnt>=k) return query(tr[q].l,tr[p].l,l,mid,k);else return query(tr[q].r,tr[p].r,mid+1,r,k-cnt);
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++) {scanf("%d",&a[i]);num.push_back(a[i]);}sort(num.begin(),num.end());num.erase(unique(num.begin(),num.end()),num.end());root[0]=build(0,num.size()-1);//离散化后数从0到num.size()-1for(int i=1;i<=n;i++){root[i]=insert(root[i-1],0,num.size()-1,find(a[i]));}while(m--){int l,r,k;scanf("%d%d%d",&l,&r,&k);cout<<num[query(root[r],root[l-1],0,num.size()-1,k)]<<endl;}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/512750.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

02. Nginx入门-Nginx安装

Nginx安装 yum安装 编辑yum环境 cat > /etc/yum.repos.d/nginx.repo << EOF [nginx-stable] namenginx stable repo baseurlhttp://nginx.org/packages/centos/$releasever/$basearch/ gpgcheck1 enabled1 gpgkeyhttps://nginx.org/keys/nginx_signing.key module_…

PTA L2-009 抢红包

题目&#xff1a; 没有人没抢过红包吧…… 这里给出N个人之间互相发红包、抢红包的记录&#xff0c;请你统计一下他们抢红包的收获。 输入格式&#xff1a; 输入第一行给出一个正整数N&#xff08;≤104&#xff09;&#xff0c;即参与发红包和抢红包的总人数&#xff0c;则…

L2-001 紧急救援(Java)

作为一个城市的应急救援队伍的负责人&#xff0c;你有一张特殊的全国地图。在地图上显示有多个分散的城市和一些连接城市的快速道路。每个城市的救援队数量和每一条连接两个城市的快速道路长度都标在地图上。当其他城市有紧急求助电话给你的时候&#xff0c;你的任务是带领你的…

干货!带你快速了解Python元组

1.元组 元组一般用来存储多个数据&#xff0c;使用() 2.创建元组 创建空元组 tup1 () print(tup1) # () print(type(tup1)) # <class tuple> 创建非空元组&#xff08;元组中只有一个元素&#xff0c;一般要在元素的后面加 , 若不加 , 该数据类型不一定是元组…

2024 年广西职业院校技能大赛高职组《云计算应用》赛项赛题第 3 套

#需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; #需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; #需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; 某企业根据自身业务需求&…

AI时代的数据分析软件DeepBI与传统BI数据分析的差距

#BI智能数据分析工具# 目前AI在各行业的渗透正在改变我们的工作方式和生活方式&#xff0c;尤其是在智能数据分析领域。今天我们就来看一下新时代数据分析的产品DeepBI与传统BI之间的差距有多大。 BI在过去的二十年被错误定义数据可视化&#xff0c;根本原因在于&#xff0c;…

低代码工具APEX的入门使用(未包含安装)

第一次使用APEX是2019年&#xff0c;这个技术成名已久只是我了解的比较晚。请看Oracle ACE的网站&#xff0c;这就是用APEX做的。实际上有一次我看O记的人操作他们的办公流程&#xff0c;都是用APEX做的。 那一年&#xff0c;我用APEX做了一个CMDB的管理系统。那时候还没有流行…

网络工程师笔记7

路由器需要知道下一跳和出接口才能把数据转发出去 各个协议的优先级 直连&#xff1a;0 OSPF&#xff1a;10 ISIS&#xff1a;15 静态&#xff1a;60 RIP :100 静态路由 ip route-static <目的ip地址> 掩码 下一跳地址 例…

关于大数据技术的学习

关于大数据技术的学习 《Java编程》、《python程序开发》&#xff0c;《Linux操作系统》、《Hadoop大数据平台构建与应用》、《网络爬虫技术与应用》、《大数据平台运维》、《Docker容器技术与应用》、《数据库技术》、《数据挖掘》、《可视化设计与开发》、《大数据分析实战》…

鸿蒙开发,对于前端开发来说,究竟是福是祸呢?

提前声明&#xff1a; 先说好哈&#xff0c;不要一上来就开喷&#xff0c;好吧&#xff0c;不感兴趣的话你可以不用看下文直接划走&#xff0c;直接喷俺有点承受不住&#xff0c;心脏不好。如果你感兴趣&#xff0c;你可以先把这篇文章看完&#xff0c;看完后感觉俺讲的还挺有道…

华容道问题求解第一部分_详细设计(二)之棋子和游戏类_棋盘和棋子渲染

&#xff08;续上篇&#xff09; HrdGame 类中的图像输出部分的函数&#xff0c;包括两部分&#xff0c;布局定义和绘制。 布局定义 广义上讲&#xff0c;布局只是棋子众多排列组合中一个快照&#xff0c;或者说是一个状态&#xff0c;因此引入了一个GameState 类&#xff0c…

07 |「存储小工具——SharedPreferences」

前言 实践是最好的学习方式&#xff0c;技术也如此。 文章目录 前言一、简介二、操作1. 存储数据2. 取数据3. 查看存储的位置和文件 一、简介 解决什么问题&#xff1a; Acticivty 中的数据还是临时的&#xff0c;当我们退出之后再进来&#xff0c;这些数据是不会被保存的&…