zoukankan      html  css  js  c++  java
  • 可持久化数据结构初步

    可持久化数据结构

    可持久化的前提条件

    一个数据结构的拓补结构在使用的过程中保持不变。如线段树的结构在使用的过程中是不会变的,故其可持久化(就是主席树)。

    而平衡树就不行,平衡树的旋转操作会改变节点之间的拓补序,所以带旋的平衡树都不可持久化。

    可持久化解决的问题

    可持久化就是通过每次修改都创建新版本,来保留数据结构回滚与访问历史版本的能力

    可持久化的核心思想

    对于这个问题,其实我们有个暴力做法,即每次都创建一个新的数据结构备份。

    这样做当然可以,但如果可持久化对象是如线段树一类开销本身就很大的数据结构,就会MLE甚至因为空间不足而RE。

    并且每次创建一个完整的备份,时间上也不优秀。

    那还有其他办法吗?

    想想,当我们的游戏以及其他应用更新时,有多少是全部重新下载的?

    几乎都是下载更新的部分,也就是增量更新

    说的具体点,只记录每一个版本上一版本不同的地方。这样的话我们的时空复杂度是有希望降低到一个较低的水平的。

    又拿线段树举例子:我们知道线段树的空间要开到 (4n) 的级别,但每次修改只涉及到了 (4log n) 的节点,所以创建新版本期望时空复杂度能达到 (O(log n))


    可持久化的实例

    Trie树的可持久化

    先来一个最原始的Trie树:

    依次维护catratcabfry四个单词。

    则原汁原味的Trie长成下面这个样子

    (画渣用鼠标画的,别喷QAQ)

    现在我们希望用可持久化的Trie来维护这几个单词。

    第一个单词是cat,直接插入进去:

    第二个单词是rat,首先我们要开一个新的根节点,它的节点信息有变化所以要裂开(?),它是我们新版本的入口。

    我们将涉及到更新的所有节点都插入进去,注意这里的a节点与路径cata节点在Trie中的意义不同,是两个互异的节点。

    插入完成后,这棵树就长成了这样:

    绿框中的就是版本2。

    该更新版本3了,先复制上一个版本的信息......

    但是c点在要修改的路径上,所以c要新建一个点。

    所以复制裂开一个c点,a点同理。但是这个新的a点又要指回原来的t点。

    为什么?

    因为t不在要修改的路径上啊。

    完成插入后结构如图:

    绿框中的即是版本3。

    注意我们每次都只更新了一条路径,每次更新至于上一个版本比较,复制信息也从上一个版本复制。

    按照上面的方法,更新fry

    绿框中的即是版本4。

    这就是可持久化Trie的过程了。

    总之,只修改有关路径上的点


    可持久化线段树(主席树)

    上面已经提到,线段树也是可持久化的。

    每一次修改,都存下来当前的一个最新版本。

    修改的时候与Trie也是一个道理,当某一次修改,一个节点的信息发生了改变,我们就 让这个点裂开 克隆一个全新的节点出来承载修改并继承原节点的信息。

    每一次修改涉及 (O(4log n)) 个区间,设有 (m) 次操作,则额外的时空复杂度为 (O(mlog n))

    还需提及的是,这种线段树存储方式显然是不满足堆式存储的要求的,所以只能使用结构体式存储。

    另外,可持久化线段树是很难以进行区间修改操作的。

    具体原因出在懒标记下传需要更新的节点过多。当然,其实也可以用标记永久化来弄,但是标记永久化局限性也大。

    言归正传,主席树的节点结构体是有一点变化的:

    struct node
    {
        int lson,rson;//左右儿子的下标
        int cnt;//当前区间有多少个数
    }
    

    不再存储区间了,直接存储了下标。

    现在我们来看一下可持久化线段树如何单点修改

    假设线段树的一次修改路径是这样的:

    首先我们要做的还是把原来的节点复制出来,然后在复制出来的节点上做修改。

    再把改变之前的边连上,得到下面这个东西:

    想体现左右儿子的关系,画得很丑。

    思路基本和trie一样,仍然是只修改路径上的点。


    例题&代码实现

    可持久化Trie:最大异或和

    >luoguP4735 最大异或和<

    题目描述

    给定一个非负整数序列 ({a_n}),初始长度为 (N)

    (M) 个操作,有以下两种操作类型:

    1. A x:添加操作,表示在序列末尾添加一个数 (x),序列的长度 (N) 增大 (1)

    2. Q l r x:询问操作,你需要找到一个位置 (p),满足 (l≤p≤r) ,使得:(a_p xor a_{p+1} xor … xor a_N xor x) 最大,输出这个最大值。

    输入格式

    第一行包括两个整数 (N,M),含义如题所述。

    第二行包括 (N) 个非负整数,表示初始序列 ({a_n})

    接下来 (M) 行,每行描述一个操作,格式如题面所述。

    输出格式

    对于每一个询问,都输出一行一个整数表示答案。

    输入样例

    5 5
    2 6 4 3 6
    A 1
    Q 3 5 4
    A 4
    Q 5 7 0
    Q 3 6 6
    

    输出样例

    4
    5
    6
    

    数据范围

    (N,Mleq 3 imes 10^5,a_iin [0,10^7]∩Z)


    解析

    由于异或有着类似减法的自反性,我们考虑前缀和。

    (S_n=a_1 oplus a_2 oplus a_3 oplus dots oplus a_n),

    则:(a_p oplus a_{p+1} oplus dots oplus a_n oplus x=S_n oplus S_{p-1} oplus x)

    题目的要求变为:在区间 ([L,R]) 内寻找一个最大的 (p) 使得 (S_{p-1} oplus C) 最大, (C=S_n oplus x)

    现在先忽视区间的限制,想想 ([1,N]) 怎么做。

    考虑构建一棵trie树,树上每个节点为一个二进制位。也就是把各个数字拆成二进制处理。

    这就是0-1trie。

    然后根据异或的原理,每次尽量选取相反的值即可。

    再考虑把区间右限制加上,就相当于查询这个trie的历史版本,所以我们需要可持久化。

    可持久化的细节见代码。

    处理左限制,我们可以再加一个信息max_id:当前子树中下标的最大值。在寻找的时候我们判断一下max_id是否大于 (L) ,是则继续递归,不是则返回。
    code:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=6e5+10,M=N*25;
    
    int n,m;
    int s[N];
    int tree[M][2]/*0-1trie,只有两种儿子*/,max_id[M];
    int root[N],cnt=0;
    /*极限空间大小:180M-190M*/
    
    void insert(int i,int k,int p,int q)//i:当前前缀和下标,k:当前处理到了哪一个二进制位,p:上一版本,q:当前版本。
    {
    	if(k<0)
    	{
    		max_id[q]=i;//叶节点保存下标信息
    		return ;
    	}
    	int nw= s[i]>>k&1;//当前位
    	if(p) tree[q][nw^1]=tree[p][nw^1];//复制同级另外一个节点的信息。
    	tree[q][nw]=++cnt;//插入当前点
    	insert(i,k-1,tree[p][nw],tree[q][nw]);
    	max_id[q]=max(max_id[tree[q][0]],max_id[tree[q][1]]);
    
    }
    
    int query(int root,int C,int L)//迭代查询
    {
    	int p=root;
    	for(int i=23; i>=0; i--)
    	{
    		int nw=C>>i&1;// C的当前位
    		if(max_id[tree[p][nw^1]]>=L) p=tree[p][nw^1];//关于L的处理
    		else p=tree[p][nw];//贪
    	}
    
    	return C^s[max_id[p]];
    }
    
    int main()
    {
    	scanf("%d%d",&n,&m);
    
    	max_id[0]=-1;//由于S0也是合法前缀和,根节点的max_id初始化成一个小于0的值
    	root[0]=++cnt;//创建初始版本
    	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]=++cnt;
    		insert(i,23,root[i-1],root[i]);
    	}
    	char op[2];
    	int l,r,x;
    	for(int i=1; i<=m; i++)
    	{
    		scanf("%s ",op);
    		if(*op=='A')
    		{
    			scanf("%d",&x);
    			n++;
    			s[n]=s[n-1]^x;
    			root[n]=++cnt;
    			insert(n,23,root[n-1],root[n]);
    		}
    		if(*op=='Q')
    		{
    			scanf("%d%d%d",&l,&r,&x);
    			printf("%d
    ",query(root[r-1],s[n]^x,l-1));
    		}
    	}
    }
    

    可持久化线段树:区间第k小

    >luoguP3834 【模板】可持久化线段树 2(主席树) <

    题目描述

    给定长度为 (N) 的整数序列 (A),下标为 (1∼N)

    现在要执行 (M) 次操作,其中第 (i) 次操作为给出三个整数 (l_i,r_i,k_i) ,求 (A[l_i] , A[l_i+1] … A[r_i]) (即 (A) 的下标区间 ([l_i,r_i]) )中第 (k_i) 小的数是多少。

    输入格式

    第一行包含两个整数 (N) 和M。

    第二行包含 (N) 个整数,表示整数序列 (A)

    接下来 (M) 行,每行包含三个整数(l_i,r_i,k_i),用以描述第 (i) 次操作。

    输出格式

    对于每次操作,都输出一行一个整数,表示该次操作中,第 (k_i) 小的数是多少。

    输入样例

    7 3
    1 5 2 6 3 7 4
    2 5 3
    4 4 1
    1 7 3
    

    输出样例

    5
    6
    3
    

    数据范围

    (Nleq 10^5, Mleq 10^4, |A[i]]|leq 10^9)


    解析

    这是一个静态序列的区间求k值的问题。

    首先我们要明白,主席树本身在这个问题里面是不能支持动态序列的。

    要动态区间,就要再套平衡树或者树状数组。这也不是本文的重点。

    说回思路,本题求区间最值,首先我们能想到的是权值线段树。不知道的->线段树进阶——权值线段树与动态开点<

    既然都离线了,先将所有的数都离散化一下。

    然后在离散化后的值域上建立可持久化权值线段树。

    一开始这棵树是空的,我们把序列的数一个一个插进去,那么每个版本的线段树就是只加前 (R) 个节点的权值线段树。

    但是左范围不能像上一个题那样做,上个题由于是存在性问题,具有一定的特殊性。

    不过,线段树有个很特殊的地方:它的结构几乎没什么变化,不像trie那样会多很多节点。可持久化的节点都能与之前版本中的节点一一对应起来的。所以,区间之间是能够做减法的。

    也就是说我们可以通过减法,计算出 ([L,R]) 中位于值域区间 ([l,r]) 中的数的个数的。然后套入权值线段树的求k小中即可。

    code:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=2e5+10;
    
    struct node
    {
    	int lson,rson;
    	int sum;
    } tree[(N<<2)+N*17];
    
    int n,m;
    int arr[N];
    vector<int> disc;
    int root[N],tot=0;
    
    int find(int x)
    {
    	return lower_bound(disc.begin(),disc.end(),x)-disc.begin();
    }
    
    #define lnode tree[node].lson
    #define rnode tree[node].rson
    
    int build(int start,int end)//返回新建的节点编号
    {
    	int node=++tot;
    	if(start==end) return node;
    	int mid=start+end>>1;
    	lnode=build(start,mid);
    	rnode=build(mid+1,end);
    
    	return node;
    }
    
    #define lnode1 tree[node1].lson
    #define rnode1 tree[node1].rson
    
    int update(int node,int start,int end,int val)//返回值同build
    {
    	int node1=++tot;
    	tree[node1]=tree[node];//新建节点并复制信息
    	if(start==end)
    	{
    		tree[node1].sum++;
    		return node1;
    	}
    	int mid=start+end>>1;
    	if(val<=mid) lnode1=update(lnode,start,mid,val);
    	else rnode1=update(rnode,mid+1,end,val);
    
    	tree[node1].sum=tree[lnode1].sum+tree[rnode1].sum;
    	return node1;
    }
    
    int query(int node1,int node,int start,int end,int k)//查找区间k小值,涉及两个版本作差
    {
    	if(start==end) return start;
    	int tmp=tree[lnode1].sum-tree[lnode].sum;
    	int mid=start+end>>1;
    	if(k<=tmp) return query(lnode1,lnode,start,mid,k);
    	else return query(rnode1,rnode,mid+1,end,k-tmp);//这里的处理参考权值线段树
    }
    
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(int i=1; i<=n; i++)
    	{
    		scanf("%d",arr+i);
    		disc.push_back(arr[i]);
    	}
    
    	sort(disc.begin(),disc.end());
    	disc.erase(unique(disc.begin(),disc.end()),disc.end());//暴力离散化
    
    	int MAX=disc.size()-1;
    	root[0]=build(0,MAX);//建树,记录初始版本
    	for(int i=1; i<=n; i++)
    	{
    		root[i]=update(root[i-1],0,MAX,find(arr[i]));//一个一个插入
    	}
    	for(int i=1; i<=m; i++)
    	{
    		int l,r,k;
    		scanf("%d%d%d",&l,&r,&k);
    		printf("%d
    ",disc[query(root[r],root[l-1],0,MAX,k)]);//查询[l,r]第k小,l-1与r版本作差
    	}
    	return 0;
    }
    
  • 相关阅读:
    2018牛客网暑期ACM多校训练营(第九场)A -Circulant Matrix(FWT)
    ZOJ
    BZOJ 4318 OSU!(概率DP)
    POJ
    POJ
    Linux安装及管理程序
    Linux目录及文件管理
    linux账号管理操作
    linux系统命令总结
    linux目录及文件管理操作
  • 原文地址:https://www.cnblogs.com/IzayoiMiku/p/14008252.html
Copyright © 2011-2022 走看看