可持久化数据结构之主席树

来源:互联网 发布:米思米2016选型软件 编辑:程序博客网 时间:2024/05/22 05:23

前序

怀着激动地心情学习了可持久化数据结构主席树,其实很简单,但是自己因为眼残导致一直没有理解。后来发现这个问题,然后手动模拟了一下这个过程,然后大彻大悟。。表示自己就看了无修改的区间第K大,学习自百度巨巨。。本篇博文转自百度巨巨,链接chairman tree

引言

首先引入CLJ论文中的定义:
所谓的“持久化数据结构”,就是保存这个数据结构的所有历史版本,同时利用它们之间的共用数据减少时间和空间的消耗。
本文主要讨论两种可持久化线段树的算法思想、具体实现以及编码技巧。

核心思想

可持久化线段树是利用函数式编程的思想,对记录的数据只赋值不修改,每次插入一个数据后保存一个历史版本,然后利用线段树的结构完全相同,可以直接相减的特性进行区间询问。
我们以经典的区间第K大问题为例:输入n个数字组成的序列,询问序列中区间[l, r]上面的第K大的元素为何。其中第K大定义为:将区间[l, r]按升序排列后的第K个元素。

思想

我们先考虑简化的问题:我们要询问整个区间内的第K大。这样我们对值域建线段树,每个节点记录这个区间所包含的元素个数,建树和查询时的区间范围用递归参数传递,然后用二叉查找树的询问方式即可:即如果左边元素个数sum>=K,递归查找左子树第K大,否则递归查找右子树第K - sum大,直到返回叶子的值。
现在我们要回答对于区间[l, r]的第K大询问。如果我们能够得到一个插入原序列中[1, l - 1]元素的线段树,和一颗插入了[1, r]元素的线段树,由于线段树是开在值域上,区间长度是一定的,所以结构也必然是完全相同的,我们可以直接对这两颗线段树进行相减,得到的是相当于插入了区间[l ,r]元素的线段树。注意这里利用到的区间相减性质,实际上是用两颗不同历史版本的线段树进行相减:一颗是插入到第l-1个元素的旧树,一颗是插入到第r元素的新树。
这样相减之后得到的是相当于只插入了原序列中[l, r]元素的一颗记录了区间数字个数的线段树。直接对这颗线段树按照BST的方式询问,即可得到区间第k大。
这种做法是可行的,但是我们显然不能每次插入一个元素,就从头建立一颗全新的线段树,否则内存开销无法承受。事实上,每次插入一个新的元素时,我们不需要新建所有的节点,而是只新建增加的节点。也就是从根节点出发,先新建节点并复制原节点的值,然后进行修改即可。
这样我们我们每到一个节点,只需要修改左儿子或者右儿子其一的信息,一直递归到叶子后结束,修改的节点数量就是树高,也就是新建了不超过树高个节点,内存开销就可以承受了。
注意我们对root[0]也就是插入了零个元素的那颗树,记录的左右儿子指针都是0,这样我们就可以用这一个节点表示一个任意结构的空树而不需要显式建树。这是因为对于这个节点,不管你再怎么递归,都是指向这个节点本身,里面记录的元素个数就是零。

POJ2104/HDU2665

#include <cstdio>#include <cstring>#include <iostream>#include <algorithm>#define MAX 100010#define CLR(arr,val) memset(arr,val,sizeof(arr))using namespace std;const int INF = 0x3f3f3f3f;//记录原数组、排序后的数组、每个元素对应的根节点int nums[MAX], sorted[MAX], root[MAX];int cnt;struct TMD{    int sum, L_son, R_son;} Tree[MAX<<5];inline int CreateNode( int _sum, int _L_son, int _R_son ){    int idx = ++cnt;    Tree[idx].sum = _sum;    Tree[idx].L_son = _L_son;    Tree[idx].R_son = _R_son;    return idx;}void Insert( int & root, int pre_rt, int pos, int L, int R ){    //从根节点往下更新到叶子,新建立出一路更新的节点,这样就是一颗新树了。    root = CreateNode( Tree[pre_rt].sum + 1, Tree[pre_rt].L_son, Tree[pre_rt].R_son );    if ( L == R ) return;    int M = ( L + R ) >> 1;    if ( pos <= M )        Insert( Tree[root].L_son, Tree[pre_rt].L_son, pos, L, M );    else        Insert( Tree[root].R_son, Tree[pre_rt].R_son, pos, M + 1, R );}int Query( int S, int E, int L, int R, int K ){    if ( L == R ) return L;    int M = ( L + R ) >> 1;    //下面计算的sum就是当前询问的区间中,左儿子中的元素个数。    int sum = Tree[Tree[E].L_son].sum - Tree[Tree[S].L_son].sum;    if ( K <= sum )        return Query( Tree[S].L_son, Tree[E].L_son, L, M, K );    else        return Query( Tree[S].R_son, Tree[E].R_son, M + 1, R, K - sum );}int main(){    int n, m, num, pos, T;    while ( scanf("%d %d", &n, &m) != EOF )    {        cnt = 0; root[0] = 0;        for ( int i = 1; i <= n; ++i )        {            scanf("%d", &nums[i]);            sorted[i] = nums[i];        }        sort( sorted + 1, sorted + 1 + n );        num = unique( sorted + 1, sorted + n + 1 ) - ( sorted + 1 );        for ( int i = 1; i <= n; ++i )        {            //实际上是对每个元素建立了一颗线段树,保存其根节点            pos = lower_bound( sorted + 1, sorted + num + 1, nums[i] ) - sorted;            Insert( root[i], root[i - 1], pos, 1, num );        }        int l, r, k;        while ( m-- )        {            scanf("%d %d %d", &l, &r, &k);            pos = Query( root[l - 1], root[r], 1, num, k );            printf("%d\n", sorted[pos]);        }    }}
0 0
原创粉丝点击