学习笔记:可持久化线段树

1、前言

线段树,众所周知,在树中的每一个元素中,保存的是线段中的一段,所维护的内容或是最大最小值,或是和等等。可持久化线段树,属于可持久化数据结构中的一种,对于可持久化数据结构这个大知识,我暂时没有去研究,今天只讲其冰山一角。

2、概念

先讲”可持久化“的含义。”可持久化“表示我们当前在处理每个状态,而之前的状态即状态的历史版本全部能够存下来。可持久化线段树,实质上是多颗线段树,最简单的可持久化线段树的题目:求区间第k大。显而易见,求区间最大值的时候我们用普通的线段树就行了,第k大总不能一个个从1数到k吧?可持久化的结构在这个时候就能够帮上大忙了。

我们设区间有n个元素,然后依次进行读入。每读入一个数字,都需要新建一颗线段树(后面会有扩展),这就是能够保存历史状态的线段树了。线段 树中每一个节点维护的是当前已经输入的数的数值位于该区间的个数。有点绕口,没错此时此刻的我也才刚刚懂了——说的直白一点,设目前是第n棵线段树中有一 个节点为[1,4],表示前n个数中数值在1至4的数的个数。Understand?

3、离散化

但是,有一个很重要的问题!题目的空间限 制肯定是有的,假设所输入的数的范围为int,你总不可能开一个大小为int的树吧?而且还要多棵线段树。此时此刻,我们就可以引入一个新知识了——离散 化。看起来很高端,其实很简单,其实你脑补一下map(属于STL)就行了,或者回忆一下高中数学必修一集合那一章,有一个叫映射的东西,和离散化意思差 不多(起码在这道题上的作用是一模一样的),所以不详细阐述,在源代码中会有小小的注释。

好了,目前有一个数列:{2,8,19,6}。假设我们已经离散化结束了,结果为2→1;6→2;8→3;19→4。那么以后我们进行数据的 处理时,1就表示2了,2就表示6了,3就表示8了。。。是不是和映射一个意思?这样的好处在于,我们不需要依赖就弄个[1,2147483647]的线 段树了,若题目规定n<=100000,则最大只需要一棵[1,10000]的线段树了。如下图(其实没有蛮多含义,真正的变化在后面):

【若是建一棵[1,19]的线段树,你想想是多么浪费空间 = =。】

4、历史版本的作用

这么多棵线段树,我们也不可能建立多个结构体来保存。我们可以把所有线段树的节点全部放在tree结构体中,设当前有m个节点,每执行一次插
入操作,新增了x个节点,则存放在tree中的第(m+1)个节点至第(m+x)个节点(当然也有别的编号方式)。同时,我们需要一个root数组,其中
root[i]表示第i棵线段树的根节点的编号。
     
 这样我们就构建完了,来想想——为什么需要历史版本?回到我们一开始的问题,求区间第k大,假设当前询问为求[x,y]的第k大,则我们所需
要用到的线段树为第x+1棵到第y棵。从根节点开始,我们将第y棵树和第x+1棵树一一对应的节点所维护的值进行相减,其所得到的数就是在所询问的
[x,y]中,当前节点表示的子区间的那几个数值在整个区间中出现的次数,用t表示,即t=root[y].[1,mid]-root[x-1].[1,mid]。先判断t是否大于k,如果t大于k,那么说明在区间[x,y]内存在[1,mid]的数的个数大于k,也就是第k大的值在[1,mid]中,否则在[mid+1,r]中。(有点绕,慢慢看 →_→)

5、缩小空间

其实必要的知识已经讲得差不多了,但是我们最后还要面临一个问题——加入一个数,就新建一棵线段树。我们假设有100000个数吧,且有
100000次询问,试想这一大片庞大的线段树森林是要占用多大的内存?一定会MLE的(当然数据小就无所谓)。我们有什么办法缩小空间需求?我们注意
到,每次我们加入一个被离散化后的数x,则从根结点开始向下更新,我们真正相对于前面一棵线段树的差异之处是很少的!设有一颗[1,4]的线段树,若当前
插入值为3,则[1,4]的左儿子[1,2]没有丝毫改动!如果又新建一个,完全是浪费。这样子,我们就有一个方法缩小冗余的空间了——将没有区别的部分
直接指回去!如图所示:

空间是不是小多了!是的。后面的线段树也以此类推。

6、例题

如前面所说,求区间第k大。【HDU 2665】

题目描述

Give you a sequence and ask
you the kth big number of a inteval.

输入格式

The first line is the number
of the test cases.

For each test case, the first
line contain two integer n and m (n, m <= 100000), indicates the
number of integers in the sequence and the number of the
quaere.

The second line contains n
integers, describe the sequence.

Each of following m lines
contains three integers s, t, k.

[s, t] indicates the interval
and k indicates the kth big number in interval [s,
t]

输出格式

For each test case, output m
lines. Each line contains the kth big
number.

输入样例

1

10
1

1 4 2 3 5 6 7 8 9
0

1 3
2

输出样例

2

——————————————————————我是分割线——————————————————————————

 1 #include <cstdio>
 2 #include <algorithm>
 3 #define MAXN 100005
 4 using namespace std;
 5
 6 int a[MAXN],b[MAXN],n,tot,root[MAXN],q,l,r,k,link[MAXN],t;
 7
 8 struct Node
 9 {
10       int ls,rs,size;
11 };
12 Node tree[MAXN*20];
13
14 struct cmp
15 {
16       bool operator () (int i,int j)
17       {
18             return (a[i]《a[j]);
19       }
20 };
21 cmp x;
22
23 void discretize()
24 {
25       sort(link+1,link+n+1,x); // 以link作为中转站,对a进行排序
26       for (int i=1;i<=n;i++) b[link[i]]=i;
27 }
28
29 void insert(int &now,int l,int r,int x)
30 {
31       tree[++tot]=tree[now]; now=tot;
32       tree[now].size++;
33       if (l==r) return;
34       int mid=(l+r)>>1;
35       if (x<=mid) insert(tree[now].ls,l,mid,x);
36       else insert(tree[now].rs,mid+1,r,x);
37 }
38
39 int query(int nl,int nr,int l,int r,int k)
40 {
41       if (l==r) return l;
42       int size=tree[tree[nr].ls].size-tree[tree[nl].ls].size,mid=(l+r)>>1;
43       if (size>=k) return query(tree[nl].ls,tree[nr].ls,l,mid,k);
44       else return query(tree[nl].rs,tree[nr].rs,mid+1,r,k-size);
45 }
46
47 int main()
48 {
49       freopen("HDU2665.in","r",stdin);
50       freopen("HDU2665.out","w",stdout);
51       scanf("%d",&t);
52       for (int j=1;j<=t;j++)
53       {
54             root[0]=0; tot=0;
55             scanf("%d %d",&n,&q);
56             for (int i=1;i<=n;i++) { scanf("%d",&a[i]); link[i]=i; }
57             discretize(); // 离散化
58             for (int i=1;i<=n;i++)
59             {
60                   root[i]=root[i-1];
61                   insert(root[i],1,n,b[i]);
62             }
63             for (int i=1;i<=q;i++)
64             {
65                   scanf("%d %d %d",&l,&r,&k);
66                   printf("%d\n",a[link[query(root[l-1],root[r],1,n,k)]]);
67             }
68       }
69       return 0;
70 }

++++++++++++++++++++++++++++我是华丽丽的分割线++++++++++++++++++++++++++++

【总结】

单纯的可持久化线段树主要适用于一类只包含查询而不包含修改的(广义)区间查询问题,这类问题至少满足下面的 第二项条件:1、整体查询可利用(离散化后的)权值线段树解决,部分区间却无法解决;2、每一个元素的状态可表示为某个前趋元素的修改版本,修改通常是极 少的——具体来说,在线段树中只会修改常数条链。

如果条件1、2都具备,我们通常可以建立具有前缀和性质的可持久化线段树解决。所谓前缀和性质,指的是对每个元素i建立线段树T(i)后,T(i)包含了1-i的信息,而这个信息是可减的。我 们可以利用树的减法将部分区间变为整个区间。具体来说,对于线性表上的查询[l,r],常用的模式是Query(T(l)-T(r-1));对于树上路径 的查询,由于每个节点都有惟一的前趋节点,我们可以把路径视作一种广义的区间,查询[l,r]时,设p=lca(l,r),常用的查询模式是 Query(T(l)+T(r)-T(p)-T(par[p]))(对点)或Query(T(l)+T(r)-2T(p))(对边)。

如果只具备条件2,我们常常不利用前缀和性质(也常常无法利用),而是将区间问题转化为点问题,单独查询某个点上的线段树,并在特定的一棵线段树上获取区间信息。这时,可能需要二分答案。这一类问题的典型例子是BZOJ2653。

上述一类问题的解决过程中,在建树时通常需要利用上一节点的已有版本。

如果题目还要求修改操作,单纯的可持久化线段树通常不能完成。这时,对于满足上述1、2条件的问题,我们会发 现,修改一个版本的线段树会对后续版本造成影响——这与修改前缀和数组中某个值会对后继值造成影响是类似的。因此,我们可以考虑使用树状数组维护前缀和。 如果是树上的问题,我们可以建立DFS序的树状数组来维护前缀和。这样,节点不存在明确的前趋,建树时也就不再需要利用上一节点的已有版本了。

只满足上述条件2且需要修改的题目我还在思考中,不知道有没有这一类的题目(如带修改的bzoj2653)。此外,我还在考虑可持久化线段树更复杂的应用(如区间运算?),因此这个总结未来也许会更新。

参考:http://blog.sina.com.cn/s/blog_6022c4720102w03t.html

时间: 2024-08-18 23:42:50

学习笔记:可持久化线段树的相关文章

HDU 5820 (可持久化线段树)

Problem Lights (HDU 5820) 题目大意 在一个大小为50000*50000的矩形中,有n个路灯.(n<=500000) 询问是否每一对路灯之间存在一条道路,使得长度为|x1 – x2| + |y1 – y2|且每个拐弯点都是路灯. 解题分析 官方题解: 除了从左往右扫描一遍外,个人认为还需从右往左扫描一遍,记录右上方的点的信息. 实际实现时,只需将整个图左右对称翻转一下即可. 学习了一下可持久化线段树的正确姿势. 可持久化线段树的空间一定要开大,开大,开大. 下图为模拟的小

【BZOJ-4408】神秘数 可持久化线段树

4408: [Fjoi 2016]神秘数 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 475  Solved: 287[Submit][Status][Discuss] Description 一个可重复数字集合S的神秘数定义为最小的不能被S的子集的和表示的正整数.例如S={1,1,1,4,13}, 1 = 1 2 = 1+1 3 = 1+1+1 4 = 4 5 = 4+1 6 = 4+1+1 7 = 4+1+1+1 8无法表示为集合S的子集的

《Hibernate学习笔记十一》:树状结构设计

<Hibernate学习笔记十一>:树状结构设计 这是马士兵老师讲解Hibernate的一个作业题,树状结构设计,这是一个比较典型的例子,因此有必要写篇博文记录下. 树状结构的设计,它是在同一个类中使用了多对一(ManyToOne)和一对多(OneToMany). 在完成这个题目我们应该按照如下的步骤进行: 1.先思考数据库的模型应该是什么样的?? 数据库中的模型应该如下:即存在id p_id 2.思考面向对象的模型,及如何来进行映射??? 根据数据库中表的特点,对象应该有id name;由于

【BZOJ 3674】可持久化并查集加强版&amp;【BZOJ 3673】可持久化并查集 by zky 用可持久化线段树破之

最后还是去掉异或顺手A了3673,,, 并查集其实就是fa数组,我们只需要维护这个fa数组,用可持久化线段树就行啦 1:判断是否属于同一集合,我加了路径压缩. 2:直接把跟的值指向root[k]的值破之. 3:输出判断即可. 难者不会,会者不难,1h前我还在膜这道题,现在吗hhh就当支持下zky学长出的题了. 3673: #include<cstdio> #include<cstring> #include<algorithm> #define read(x) x=ge

poj2104 求区间第k大 可持久化线段树

poj2104 求区间第k大  可持久化线段树 #include<iostream> #include<cstdio> #include<cstring> #include<cstdlib> #include<algorithm> #define REP(i,a,b) for(int i=a;i<=b;i++) #define MS0(a) memset(a,0,sizeof(a)) using namespace std; typedef

Codeforces 484E. Sign on Fence 可持久化线段树

大概题意: 给一数组a,问在某一区间L~R中,问对于连续的长为W的一段中最小的数字的最大值是多少. 显然可以转化成二分高度然后判断可行性的问题. 考虑到高度肯定为数组中的某一个值,将数组从大到小排序. 建n棵线段树,对于第 i 棵线段树,将 大于等于a[i] 的叶子的值设置为1,其他的叶子设置为0,问题就转化成了用线段树求某一区间中最长的连续的1的个数,这是一个线段树的经典问题,可以通过维护每个节点的 左边连续和,右边连续和,连续和的最大值 得到. 由于空间问题,不可能建立10^5棵线段树,考虑

【bzoj3956】Count 单调栈+可持久化线段树

题目描述 输入 输出 样例输入 3 2 0 2 1 2 1 1 1 3 样例输出 0 3 题解 单调栈+可持久化线段树 本题是 bzoj4826 的弱化版(我为什么做题总喜欢先挑难的做QAQ) $k$对点对$(i,j)$有贡献,当且仅当$a_k=max(a_{i+1},a_{i+2},...,a_{r-1})$,且$a_k<a_i\&\&a_k<a_j$. 那么我们可以使用单调栈求出i左面第一个比它大的位置$lp[i]$,和右面第一个比它大的位置$rp[i]$,那么点对$(lp

POJ-2104-K-th Number(可持久化线段树)

K-th Number Time Limit: 20000MS   Memory Limit: 65536K Total Submissions: 55456   Accepted: 19068 Case Time Limit: 2000MS Description You are working for Macrohard company in data structures department. After failing your previous task about key inse

COGS 2554. [福利]可持久化线段树

2554. [福利]可持久化线段树 ★★☆   输入文件:longterm_segtree.in   输出文件:longterm_segtree.out   简单对比时间限制:3 s   内存限制:256 MB [题目描述] 为什么说本题是福利呢?因为这是一道非常直白的可持久化线段树的练习题,目的并不是虐人,而是指导你入门可持久化数据结构. 线段树有个非常经典的应用是处理RMQ问题,即区间最大/最小值询问问题.现在我们把这个问题可持久化一下: Q k l r 查询数列在第k个版本时,区间[l,