您的位置:首页 > 理论基础 > 数据结构算法

数据结构录 之 主席树。

2015-07-29 20:36 525 查看
  主席树(Chair Tree),一个神奇的数据结构。

  实质上是函数式的线段树,或者可持久化的线段树。可持久化这个还比较好理解,Persistent Data Structures,看一下以上这篇文章就差不多明白为啥叫可持久化的的了。但是恕我太弱,对函数式编程实在理解不了多少,所以为啥是函数式的线段树,一时半会也不明白。 T_T

  这里从ZOJ的 2112 这道题目讲起,可以先看一下题目是啥,就是求区间第K小,但是可以单点更新某个位置(一句话题意。)

  下面我们来解一下这道题。。。

  — 先想一下,要求第K小的话,要不就是有一种数据结构,能够直接得到第K个是几(然而并不知道有没有这样的数据结构。),要不就是能够对于某个数X,得到在区间有几个比他小的,这样的话二分就能知道区间第K小是多少了。这样的数据结构的话 线段树套二叉搜索树 就好(今天下午就试了一下。),但是现在说的是主席树。。。席树。。。树。

  — 然后如果想知道比某个数X小的数有几个的话,用线段树维护所有数出现的频率就好了,这样直接就是询问1到X-1的频率和就好了,但是所有数太多,所以这里要先离散一下。

  — 但是这样有问题啊,线段树维护的是哪里的所有数的出现频率啊,题目可是要求某个区间的第K小啊!

  — 还没说完嘛,谁说就弄一颗线段树,你弄上N^2颗不就完了,把1-N的每个区间都弄一颗线段树,这样询问哪个区间找到那颗线段树,然后二分寻找第K小不就完了。

  — N^2颗线段树?楼主你TM在逗我?N可是有50000啊!

  — 好像。。。N^2确实不行哎,那么弄N颗行不,第 i 颗线段树记录的是原数组1- i 里面每个数出现的频率,反正是频率嘛,这样的话比如对于5这个数,你要求比5小的数在[3,7]这个区间里面出现的频率,直接就是 [1,7]-[1,2] 这两个区间出现的频率相减不就好了,反正是频率,满足相减性。

  — 这样好像可以询问了呢。等等,楼主你又TM在逗我,还有单点更新操作啊好不,你有N颗线段树,每一颗都是记录的前缀啊,这样如果你要更新5的话,你不得把5,6,7。。。都更新了啊,这有法玩?

  — 哦,对啊,还有更新操作啊,WTF,这怎么玩。

  (这时,突然从天而降一道闪电,光照亮了。。。某个地方(编不出来了这里),然后,看到一道题)

  “问,区间和怎么求,动态区间和怎么求?”

  — 这个还不简单?区间和的话提前处理好所有前缀和,然后两个区间左右相减不就是了;动态的话,树状数组不就轻松搞定了。

  — 等等,树状数组,动态区间和,对啊,干嘛每颗线段树记录的1- i 啊,为啥不能像树状数组一样,就想下面这个图这样记录这样一个区间啊。(为啥会有图出现?)

// ━━━━━━神兽出没━━━━━━
//      ┏┓       ┏┓
//     ┏┛┻━━━━━━━┛┻┓
//     ┃           ┃
//     ┃     ━     ┃
//     ████━████   ┃
//     ┃           ┃
//     ┃    ┻      ┃
//     ┃           ┃
//     ┗━┓       ┏━┛
//       ┃       ┃
//       ┃       ┃
//       ┃       ┗━━━┓
//       ┃           ┣┓
//       ┃           ┏┛
//       ┗┓┓┏━━━━━┳┓┏┛
//        ┃┫┫     ┃┫┫
//        ┗┻┛     ┗┻┛
//
// ━━━━━━感觉萌萌哒━━━━━━

// Author        : WhyWhy
// Created Time  : 2015年07月27日 星期一 23时43分28秒
// File Name     : 2112_2.cpp

#include <stdio.h>
#include <string.h>
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <set>
#include <map>
#include <string>
#include <math.h>
#include <stdlib.h>
#include <time.h>

using namespace std;

const int MaxN=50010+10010;
const int MaxM=10005;
const int MaxNode=2600010;

int Tcou;
int TreeRoot[MaxN];
int lson[MaxNode],rson[MaxNode],BIT[MaxNode];

int BaseTree[MaxN];                            // 对于初始序列的前缀建的树的根。

int N;
int num[MaxN];
int Hnum[MaxN],Hcou;                            // Hash相关,Hnum数组从1开始。

inline int hash(int x)
{
return lower_bound(Hnum+1,Hnum+Hcou+1,x)-Hnum;
}

inline int lowbit(int x)
{
return x&(-x);
}

int insert(int old,int val,int d)                // 在old这个根指向的那棵树上更新一个新的分支,更新的值为val这个数的频率,频率加上d。(线段树的区间是数。)
{
int newRoot=Tcou++,ret=newRoot;                // newRoot是新建的分支的根节点,并且保存下来用来做返回值。
int L=1,R=Hcou,M;

BIT[newRoot]=BIT[old]+d;                    // 从这到下就是类似线段树的更新操作了,这一步是更新根节点维护的频率。(BIT数组维护的是数的区间的出现频率总和。)

while(R>L)                                    // 就像是线段树,一步步二分找到val所在的那个确切位置,然后沿途更新(新建)节点,而对另一半子树就直接指向原来的就好了。
{
M=(L+R)>>1;

if(val<=M)                                // 进入左子树,newRoot的左儿子被新建,然后右儿子指向原来那个,反正值都是一样的。
{
lson[newRoot]=Tcou++;
rson[newRoot]=rson[old];
newRoot=lson[newRoot];
old=lson[old];
R=M;
}
else                                    // 进入右边,和上面一样。
{
lson[newRoot]=lson[old];
rson[newRoot]=Tcou++;
newRoot=rson[newRoot];
old=rson[old];
L=M+1;
}

BIT[newRoot]=BIT[old]+d;                // 新节点的值是要维护的。
}

return ret;
}

void add(int x,int val,int d)                    // 给树状数组上面的关于x的所有线段树进行insert操作。
{
for(;x<=N;x+=lowbit(x))
TreeRoot[x]=insert(TreeRoot[x],val,d);    // 在这颗线段树的基础上新建一个线段树(只是一些分支),这个线段树的根节点指向这个新线段树。
}

void update(int up,int ut)                        // 更新up位置上数为ut。
{
add(up,hash(num[up]),-1);                    // 把这个位置上面原来的数的频率减去1。
add(up,hash(ut),1);                            // 把ut这个数的频率加上1。
num[up]=ut;                                    // num数组就是记录这个位置的数是多少的。
}

int use[MaxN];                                    // 省事的一个数组,记录当前要询问的区间。

int sum(int x)                                    // 求1-x的线段树的某个区间的连续和。
{
int ret=0;

for(;x;x-=lowbit(x))
ret+=BIT[lson[use[x]]];

return ret;
}

int query(int ql,int qr,int K)                    // ql到qr这个区间的第K小。
{
int L=1,R=Hcou,M;
int temp;

int BaseL=BaseTree[ql-1],BaseR=BaseTree[qr];// 记录对初始序列的线段树当前所要查询的位置。

for(int i=ql-1;i;i-=lowbit(i))                // 记录当前要找的区间。
use[i]=TreeRoot[i];
for(int i=qr;i;i-=lowbit(i))
use[i]=TreeRoot[i];

while(R>L)                                    // 二分查找。
{
M=(L+R)>>1;
temp=sum(qr)-sum(ql-1);                    // 获得在ql到qr这个区间里面的1-M这些数的出现了多少次。
temp+=BIT[lson[BaseR]]-BIT[lson[BaseL]];// 再加上初始序列线段树的。

if(K<=temp)                                // 说明第K个在1-M里面找。
{
for(int i=ql-1;i;i-=lowbit(i))
use[i]=lson[use[i]];
for(int i=qr;i;i-=lowbit(i))
use[i]=lson[use[i]];
BaseL=lson[BaseL];
BaseR=lson[BaseR];
R=M;
}
else
{
for(int i=ql-1;i;i-=lowbit(i))
use[i]=rson[use[i]];
for(int i=qr;i;i-=lowbit(i))
use[i]=rson[use[i]];
BaseL=rson[BaseL];
BaseR=rson[BaseR];
K-=temp;
L=M+1;
}
}

return L;                                    // 这就是第K个。
}

int build_BIT(int L,int R)                        // 建一颗空的线段树。
{
int root=Tcou++;

BIT[root]=0;

if(L!=R)
{
int M=(L+R)>>1;

lson[root]=build_BIT(L,M);                // 指向左儿子。
rson[root]=build_BIT(M+1,R);            // 指向右儿子。
}

return root;
}

void ChairTree_init(int num[])                    // 初始化主席树。
{
TreeRoot[0]=build_BIT(1,Hcou);                // 建空树。

for(int i=1;i<=N;++i)                        // 把所有线段树指向空树。
{
TreeRoot[i]=TreeRoot[0];
BaseTree[i]=insert(BaseTree[i-1],hash(num[i]),1);
}
}

void Hash_init()
{
sort(Hnum+1,Hnum+Hcou+1);
Hcou=unique(Hnum+1,Hnum+Hcou+1)-Hnum-1;
}

void init()
{
Tcou=Hcou=0;
}

struct Query
{
bool type;
int a,b,c;

}q[MaxM];

int main()
{
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);

int T;
char ts[10];
int M;

scanf("%d",&T);

while(T--)
{
scanf("%d %d",&N,&M);
init();

for(int i=1;i<=N;++i)
{
scanf("%d",&num[i]);
Hnum[++Hcou]=num[i];
}

for(int i=1;i<=M;++i)
{
scanf("%s",ts);

if(ts[0]=='Q')
{
q[i].type=0;
scanf("%d %d %d",&q[i].a,&q[i].b,&q[i].c);
}
else
{
q[i].type=1;
scanf("%d %d",&q[i].a,&q[i].b);
Hnum[++Hcou]=q[i].b;
}
}

Hash_init();
ChairTree_init(num);

for(int i=1;i<=M;++i)
{
if(q[i].type)
update(q[i].a,q[i].b);
else
printf("%d\n",Hnum[query(q[i].a,q[i].b,q[i].c)]);
}
}

return 0;
}


View Code
  然后终于(TM)过了。。。真是醉了。。。

  通过这个题差不多也就说了说主席树了,感觉主席树就是一群线段树而已,然后每次充分利用之前的线段树,只新建一个分支。

  这个题的话是维护了每个数出现的次数,感觉可以维护的东西很多,只要能够借用之前的线段树就好。

  突然觉得用处好大,嗯,确实好大。

  作为一个菜鸟,主席树到目前也只是学了一些皮毛,然后像个逗比(就是逗比)一样写了这个,来加深一下印象。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: