1.什么是静态主席树?
主席树也称函数式线段树也称可持久化线段树 ,就是基于线段树的基础上,保存线段树的每个历史版本,线段树只能查询最终区间的结果,而不能查询过程中的信息,比如我们对区间进行了100次修改,问你第50次修改完时的区间和是多少。所以我们就有了主席树来实现保存线段树的每个历史版本,注:主席树并不只是用于查询区间第k大,这只是查询历史线段树的一个用处而已!那么什么是静态主席树,就是我们这里只涉及对历史版本的查询,而不涉及对历史版本的修改(动态主席树),这里留个坑日后再填!反正我们先来学习一下静态主席树!
2.如何实现保存历史线段树
(1)总体思路:我们最容易想到的是对于每一个操作都建立一颗线段树,但是这样时间空间花费都很大!我们注意到我们每一个数据插入时影响的只是某一条路径上的节点,而对于其他大部分节点跟上一颗树都是一样的,所以为们可以直接用其他相同的节点,只添加新的节点在这棵树上!以此类推,从而节省了大量新建树的时间和空间!
(2)具体实现:我们用T[ i ],保存第 i 棵线段树的根节点rt,用L[rt]保存节点的左儿子结点编号,用R[rt] 保存节点的右儿子结点编号。用sum[rt] 保存节点rt 处的信息。每当读取一个数据新建树时,不变的部分使L[T[i] ] = L[T[i-1] ]等,改变的部分更新节点和sum。(对照上一颗线段树T[i-1])
(3)查询:我们要查询第i课线段树,直接访问T[i]这棵树即可,若要查询两棵树之间的关系,可以使两棵树做差。sum[T[r] ] - sum[T[ l - 1 ] ] ;
3.实例:最基本的保存历史线段树( 洛谷 P3919 )
(1)题意:
如题,你需要维护这样的一个长度为 N的数组,支持如下几种操作
-
在某个历史版本上修改某一个位置上的值 操作1
-
访问某个历史版本上的某一位置的值
此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)
对于操作1,格式为vi 1 loci valueiv,即为在版本vi的基础上,将loci修改为value
对于操作2,格式为vi 2 loci ,即访问版本vi中的loai的值
输出包含若干行,依次为每个操作2的结果。
(2)思路:最直接的想法是每次保存一个数组,但是数据量有1e6,所以我们就求助主席树啦!
对于操作一:我们新建主席树,只修改改变的路径点
对于操作二:我们直接复制上一个主席树即让这个树的根节点左右儿子等于上课树的左右儿子即可同时查询
(3)代码:
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1000000 + 7;
int T[maxn],L[maxn*20],R[maxn*20],num[maxn],sum[maxn*20];//树的根节点,左儿子编号,右儿子编号,sum存节点信息
int tot,n,m;
void BuildTree(int &rt,int l,int r){//建立初始树
rt = ++tot;//赋予节点编号
if(l==r){sum[rt] = num[l];return;}//到达叶子,存当前元素数据
int mid = (l+r)>>1;//区间折半
BuildTree(L[rt],l,mid);
BuildTree(R[rt],mid+1,r);
}
void InsertTree(int pre,int &rt,int l,int r,int k,int v){//修改操作,历史树对应编号,当前树编号,当前左右区间,修改位置,修改值
rt = ++tot;//更新当前树编号(到达这个点说明这个点需要修改了)
L[rt] = L[pre];//复制上课树左右儿子先
R[rt] = R[pre];
if(l==r){//到达叶子,最终修改
sum[rt] = v;
return;
}
else sum[rt] = sum[pre];//复制
int mid = (l+r)>>1;
if(k<=mid)InsertTree(L[pre],L[rt],l,mid,k,v);//判断修改位置递归,不修改的不改变
else InsertTree(R[pre],R[rt],mid+1,r,k,v);
}
int Query(int rt,int l,int r,int k){//查询
if(l==r){
return sum[rt];
}
int mid = (l+r)>>1;
if(k<=mid)return Query(L[rt],l,mid,k);
else return Query(R[rt],mid+1,r,k);
}
int main()
{
int n,m;
tot = 0;//节点编号(不用左节点 = 2n + 1,右节点 = 2n + 2了,浪费严重,改用连续记录)
memset(sum,0,sizeof(sum));
scanf("%d%d",&n,&m);
for(int i = 0;i<n;i++){
scanf("%d",&num[i]);//输入初始节点
}
int root = 0;
BuildTree(T[root++],0,n-1);//建立初始树(初始数组),T[0] = 1;
while(m--){//m个操作
int edit,op,index;
scanf("%d%d%d",&edit,&op,&index);
index--;
if(op==1){//修改操作
int val;
scanf("%d",&val);
InsertTree(T[edit],T[root++],0,n-1,index,val);//历史版本T[edit],当前树T[root]
}
else{
T[root] = ++tot;
L[T[root]] = L[T[edit]];//直接复制历史版本左右子树
R[T[root]] = R[T[edit]];
sum[T[root]] = sum[T[edit]];
int ans = Query(T[root],0,n-1,index);//查询
root++;
printf("%d\n",ans);
}
}
return 0;
}
4.可持久化线段树 + 离散化(主席树) HDU 2665
(1)题意:给你一个长度为n的数字序列,给你查询的左右区间,询问这个区间内的第k大的数字是多少
比如:5 2 1 2 3 6 9 序列中[2,5]区间内第三大的数字是2(1 2 2 3)
(2)分析:普通线段树只能求区间内最小的数字或者最大的数字是多少,这里是任给一个区间,求区间内的第k大。
<1>如果是固定一个区间求这个区间的第k大数字,那就是在这个区间内比他小的有(k - 1)个数字,那么怎么保证随便输入k都能找到比他小的(k-1)个数字呢?
这里我们就要把sum保存的信息改变为这个区间内的数字个数。
那么这个区间内保存的是什么数字的个数呢?
既然是比他小的数字,那么我们的区间[1,n]保存的就应该是第1大,第二大.....的数字的个数,那么我们查询第k大时,只需要判断节点数字的个数即可。
<2>但是现在是随便给定区间怎么办呢?
既然一个区间只能由一颗线段树保存,那么我们这里就要有多颗线段树,所以就要用到主席树了!
那么怎么应用主席树中保存的历史线段树呢?
为了遍及每一个区间点,我们需要在每一个数据时利用这个前缀建立一颗线段树,表示从[1,当前]数字里面数字大小的个数情况。若是求[l,r]区间内的数字情况,当然是只有[l,r]里面的数字,注意到这里每颗线段树的结构一样,节点一样,只是保存的数字个数不一样,所以用sum[T[r]] - sum[T[l - 1] ]表示的就是[l,r]里面的数字大小情况,此时寻找第k大依据sum[ L[ T[r] ] ] - sum[ L[ T[l - 1] ]来判断该往左子树走还是往右子树走,寻找这个差区间内的第k大!
(3)实例(表明出处:借图表明出处)
比如有4个数5 3 6 9,求区间[2,4]第2小的数。
我们先把序列离散化后是2 1 3 4。
(图丑勿喷)我们要求[2,4],直接让T[4]每个节点 - T[1]每个节点即为[2,4]区间内的所有数字大小情况
(4)代码:
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
const int maxn = 100000 + 100;
int T[maxn],L[maxn*20],R[maxn*20],sum[maxn*20];
int tot,n,q;
int num[maxn],data[maxn];
void BuildTree(int &rt,int l,int r){//建立空树
rt = ++tot;
sum[rt] = 0;
if(l==r)return;
int mid = (l + r)>>1;
BuildTree(L[rt],l,mid);
BuildTree(R[rt],mid+1,r);
}
void InsertTree(int orirt,int &rt,int l,int r,int big){//建立新树(插入)
rt = ++tot;
L[rt] = L[orirt];
R[rt] = R[orirt];
sum[rt] = sum[orirt] + 1;//更新路径节点信息
if(l==r)return;
int mid = (l + r)>>1;
if(big<=mid)InsertTree(L[orirt],L[rt],l,mid,big);//寻找插入位置
else InsertTree(R[orirt],R[rt],mid+1,r,big);
}
int Query(int lert,int rirt,int pos,int l,int r){//查询第k大
if(l==r)return l;
int lnum = sum[L[rirt]] - sum[L[lert]];//两树做差即为该区间内的节点信息
int mid = (l+r)>>1;
int res;
if(pos<=lnum)res = Query(L[lert],L[rirt],pos,l,mid);//数字个数>pos说明比他小的(k-1)个数字还在左边
else res = Query(R[lert],R[rirt],pos - lnum,mid+1,r);//否则说明左边不够(k-1)个往右找剩下的pos - lnum个
return res;
}
int main()
{
int t;
scanf("%d",&t);
while(t--){
tot = 0;
memset(sum,0,sizeof(sum));
scanf("%d%d",&n,&q);
for(int i = 0;i<n;i++){
scanf("%d",&num[i]);
data[i] = num[i];
}
sort(num,num+n);//离散化
int len = unique(num,num+n) - num;//数组去重
BuildTree(T[0],0,len-1);//先建立一颗空树T[0]
int root = 1;
for(int i = 0;i<n;i++){//插入每一个数据,建立每一棵新树
int index = lower_bound(num,num+len,data[i]) - num;//找到第几大
InsertTree(T[root-1],T[root],0,len-1,index);//插入该位序
root++;
}
while(q--){
int lif,rig,k;
scanf("%d%d%d",&lif,&rig,&k);
int ans = Query(T[lif-1],T[rig],k,0,len-1);//查询,ans是第几大
printf("%d\n",num[ans]);//输出第ans大的数字即为答案
}
}
return 0;
}