可持久化数据结构初步

可持久化数据结构

可持久化的前提条件

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

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

可持久化解决的问题

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

可持久化的核心思想

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

这样做当然可以,但如果可持久化对象是如线段树一类开销本身就很大的数据结构,就会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,M\leq 3 \times 10^5,a_i\in [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\n",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

数据范围

\(N\leq 10^5,\ M\leq 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\n",disc[query(root[r],root[l-1],0,MAX,k)]);//查询[l,r]第k小,l-1与r版本作差
	}
	return 0;
}
posted @ 2020-11-19 22:08  RemilaScarlet  阅读(232)  评论(0编辑  收藏  举报