【学习笔记】可持久化权值线段树--主席树 (静态)
主席树——可持久化权值线段树(静态)
001 前置芝士
(1)动态开点线段树
(2)普通の权值线段树
(3)动态开点维护权值线段树
(4)可持久化数组(可持久化线段树)
002 动态开点线段树
我们知道,开一棵线段树数组所需空间为4*MAXN,当MAXN过大时,显然会MLE,那有没有什么优化空间的方法呢?当然有啦QWQ(废话,要是没有我还写什么)
动态开点
顾名思义,就是动态地新开节点。平时使用线段树时,建树过程中直接分成两部分向下递归建树,明显的,这么做会有冗余产生(因为递归时建了某一子节点但可能根本没有用到该节点)。对于这类冗余,我们完全没有必要开这一节点。解决办法很简单,现用现开,没有用的我就不开,用到时在新建节点,可以很大程度上的优化空间复杂度,代码很简单啦qwq
OPEN NODE
void open_node(int &p,int l,int r) //p一定要传址调用
{
p=++cnt;
t[p].val=sum[r]-sum[l-1]; //sum类似于前缀和,这里是区间加操作
}
query and update 操作就是在开头加上开点的操作,没什么好说的
放代码——动态开点线段树(区间加 区间查询)
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<cstdlib>
#include<climits>
#include<algorithm>
#define int long long
using namespace std;
const int maxn=1e6+5;
inline int read()
{
int w=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9')
{
if(ch=='-')
{
f=-1;
}
ch=getchar();
}
while(ch>='0' && ch<='9')
{
w=(w<<3)+(w<<1)+(ch^48);
ch=getchar();
}
return w*f;
}
struct s_t
{
int ls;
int rs;
int val;
int tag;
}t[maxn*4];
int root;
int sum[maxn];
int n,q,cnt;
int a[maxn];
void open_node(int &p,int l,int r)
{
p=++cnt;
t[p].val=sum[r]-sum[l-1];
}
void push_up(int p,int l,int r)
{
if(l==r)
{
return ;
}
int mid=(l+r)>>1;
if(t[p].ls==0)
{
open_node(t[p].ls,l,mid);
}
if(t[p].rs==0)
{
open_node(t[p].rs,mid+1,r);
}
t[p].val=t[t[p].ls].val+t[t[p].rs].val;
}
void push_down(int p,int l,int r)
{
if(t[p].tag)
{
if(l==r)
{
return ;
}
int mid=(l+r)>>1;
if(t[p].ls==0)
{
open_node(t[p].ls,l,mid);
}
if(t[p].rs==0)
{
open_node(t[p].rs,mid+1,r);
}
t[t[p].rs].tag+=t[p].tag;
t[t[p].ls].tag+=t[p].tag;
t[t[p].ls].val+=(mid-l+1)*t[p].tag;
t[t[p].rs].val+=(r-mid)*t[p].tag;
t[p].tag=0;
}
}
void update(int &p,int l,int r,int l_que,int r_que,int k)
{
if(p==0)
{
open_node(p,l,r);
}
if(l_que<=l && r<=r_que)
{
t[p].val+=(r-l+1)*k;
t[p].tag+=k;
return ;
}
push_down(p,l,r);
int mid=(l+r)>>1;
if(l_que<=mid)
{
update(t[p].ls,l,mid,l_que,r_que,k);
}
if(r_que>mid)
{
update(t[p].rs,mid+1,r,l_que,r_que,k);
}
push_up(p,l,r);
}
int query(int &p,int l,int r,int l_que,int r_que)
{
if(p==0)
{
open_node(p,l,r);
}
if(l_que<=l && r<=r_que)
{
return t[p].val;
}
push_down(p,l,r);
int mid=(l+r)>>1;
int ans=0;
if(l_que<=mid)
{
ans+=query(t[p].ls,l,mid,l_que,r_que);
}
if(r_que>mid)
{
ans+=query(t[p].rs,mid+1,r,l_que,r_que);
}
return ans;
}
signed main()
{
n=read();
q=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
sum[i]=sum[i-1]+a[i];
}
for(int i=1;i<=q;i++)
{
int opt=read();
if(opt==1)
{
int l=read();
int r=read();
int k=read();
update(root,1,n,l,r,k);
}
if(opt==2)
{
int l=read();
int r=read();
cout<<query(root,1,n,l,r)<<endl;
}
}
return 0;
}
根据代码也可知道,动态开点和普通线段树的差别并不大,唯一的差别就是省去了建树操作,记录的是子节点而不是父节点(认儿子不认爹qwq)
003 权值线段树
权值,指的是一个数出现的次数,比如我们的桶思想,开一个cnt数组存放各个元素出现的次数。顾名思义,权值线段树就是维护这个桶数组的线段树啦
这东西有啥用嘞?我们可以用它求某个元素出现的次数和全局(整个序列)第K大/小的元素。什么?这还没用?用sort水过的橙题?转化一下,如果你不会归并&树状数组,那它还可以帮你求逆序对!
建树和update还是普通线段树啦,这里就不说了,着重说一下两种查询操作
查询k出现的次数
因为我们维护的就是权值,所以t[p].val就是其出现的次数,线段树query板子即可
int query(int p,int k)
{
if(t[p].l==t[p].r)
{
return t[p].val;
}
int mid=(t[p].l+t[p].r)>>1;
if(k<=mid)
{
return query(p*2,k);
}
if(k>mid)
{
return query(p*2+1,k);
}
}
查询第k大/小的值
第k大/小当我们查询到一叶子节点时,我们知道桶的下标就是该元素,所以当前节点的l=r=桶的下标=该元素的值
int query_kth(int p,int k)
{
if(t[p].l==t[p].r)
{
return t[p].l;
}
if(k<=t[p*2].val) //如果k小于左子树的长度 说明第k小の数在左子树
{
return query_kth(p*2,k);
}
else
{
return query_kth(p*2+1,k-t[p*2].val); //这里要注意,它在右子树里的排名变成了k-左子树的权值(左子树里的元素个数)
}
}
求逆序对
就是在每次update之前查询一下后面已经插了多少个元素(因为我是按照从前到后的顺序插入的,所以已经出现了比当前元素大的元素,就产生了一对逆序对)
AC 代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#define int long long
using namespace std;
const int maxn=5e5+5;
inline int read()
{
int w=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9')
{
if(ch=='-')
{
f=-1;
}
ch=getchar();
}
while(ch>='0' && ch<='9')
{
w=(w<<3)+(w<<1)+(ch^48);
ch=getchar();
}
return w*f;
}
int n,k,ans;
int a[maxn];
int b[maxn];
int bucket[maxn];
struct s_t
{
int l;
int r;
int val;
}t[maxn*4];
void build(int p,int l,int r)
{
t[p].l=l;
t[p].r=r;
if(l==r)
{
return ;
}
int mid=(l+r)>>1;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
}
void update(int p,int k)
{
if(t[p].l==t[p].r)
{
t[p].val++;
return ;
}
int mid=(t[p].l+t[p].r)>>1;
if(k<=mid)
{
update(p*2,k);
}
else
{
update(p*2+1,k);
}
t[p].val=t[p*2].val+t[p*2+1].val;
}
int query(int p,int l,int r)
{
if(l<=t[p].l && t[p].r<=r)
{
return t[p].val;
}
int mid=(t[p].l+t[p].r)>>1;
int ans=0;
if(l<=mid)
{
ans+=query(p*2,l,r);
}
if(r>mid)
{
ans+=query(p*2+1,l,r);
}
return ans;
}
void disappeard()
{
for(int i=1;i<=n;i++)
{
b[i]=a[i];
}
sort(b+1,b+n+1);
for(int i=1;i<=n;i++)
{
a[i]=lower_bound(b+1,b+1+n,a[i])-b;
}
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
}
build(1,1,n);
disappeard(); //离散化
for(int i=1;i<=n;i++)
{
ans+=query(1,a[i]+1,n);
update(1,a[i]);
}
cout<<ans;
return 0;
}
由于权值线段树是建立在值域上的,为防止MLE,所以经常与离散化或动态开点搭配使用
004 动态开点权值线段树
就是动态开点维护权值线段树啦,学习了上面两项后自己都能搞出来の东西,直接放代码吧
查询k出现的次数
int query(int p,int l,int r,int l_que,int r_que)
{
if(p==0)
{
return 0;
}
if(l_que<=l && r<=r_que)
{
return t[p].val;
}
int mid=(l+r)>>1;
long long ans=0;
if(l_que<=mid)
{
ans+=query(t[p].ls,l,mid,l_que,r_que);
}
if(r_que>mid)
{
ans+=query(t[p].rs,mid+1,r,l_que,r_que);
}
return ans;
}
查询第k大/小的值
int query_kth(int p,int l,int r,int k)
{
if(p==0)
{
return 0;
}
if(l==r)
{
return l;
}
int mid=(l+r)>>1;
if(k<=t[t[p].ls].val)
{
return query_kth(t[p].ls,l,mid,k);
}
else
{
return query_kth(t[p].rs,mid+1,r,k-t[t[p].ls].val);
}
}
005 可持久化数组
可持久化数据结构的基础
给你一个序列,让你进行如下操作
1.在某个历史版本上修改某一个位置上的值
2.访问某个历史版本上的某一位置的值
看到题目之后的感受只能用三个字母来形容——WOC
dalao应该一眼就想到正解了,没错,每次操作之后都建一棵线段树,然后你就得到了好看的整页MLE
那该怎么办呢?当然是放弃啦(逃
可以想出,每次操作后影响的只有更改的节点到根节点这一条链上的值,所以我们只需要在原来的线段树上新加一条链,用来存放更改后的值就好啦
新加一条链?
动态开点(链)の思想!!!
常规的建树(根)
void build(int &p,int l,int r)
{
p=++cnt;
if(l==r)
{
t[p].val=a[l];
return ;
}
int mid=(l+r)>>1;
build(t[p].ls,l,mid);
build(t[p].rs,mid+1,r);
}
update时只需要把新节点更改,其他的把老版的复制过来即可
void update(int &p,int las,int l,int r,int pos,int k)
{
p=++cnt;
t[p]=t[las];//复制过来
if(l==r)
{
t[p].val=k;
return ;
}
int mid=(l+r)>>1;
if(pos<=mid)
{
update(t[p].ls,t[las].ls,l,mid,pos,k);
}
else
{
update(t[p].rs,t[las].rs,mid+1,r,pos,k);
}
}
查询还是一样的,全部代码如下
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
using namespace std;
const int maxn=1e6+5;
inline int read()
{
int w=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9')
{
if(ch=='-')
{
f=-1;
}
ch=getchar();
}
while(ch>='0' && ch<='9')
{
w=(w<<3)+(w<<1)+(ch^48);
ch=getchar();
}
return w*f;
}
int cnt,m;
int n,root[maxn];
int a[maxn];
struct s_t
{
int ls;
int rs;
int val;
}t[maxn<<5];
void build(int &p,int l,int r)
{
p=++cnt;
if(l==r)
{
t[p].val=a[l];
return ;
}
int mid=(l+r)>>1;
build(t[p].ls,l,mid);
build(t[p].rs,mid+1,r);
}
void update(int &p,int las,int l,int r,int pos,int k)
{
p=++cnt;
t[p]=t[las];
if(l==r)
{
t[p].val=k;
return ;
}
int mid=(l+r)>>1;
if(pos<=mid)
{
update(t[p].ls,t[las].ls,l,mid,pos,k);
}
else
{
update(t[p].rs,t[las].rs,mid+1,r,pos,k);
}
}
int query(int p,int l,int r,int k)
{
if(l==r)
{
return t[p].val;
}
int mid=(l+r)>>1;
if(k<=mid)
{
return query(t[p].ls,l,mid,k);
}
else
{
return query(t[p].rs,mid+1,r,k);
}
}
int main()
{
n=read();
m=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
}
build(root[0],1,n);
for(int i=1;i<=m;i++)
{
int vi=read();
int opt=read();
if(opt==1)
{
int loc=read();
int val=read();
update(root[i],root[vi],1,n,loc,val);
}
if(opt==2)
{
int loc=read();
root[i]=root[vi];
cout<<query(root[i],1,n,loc)<<endl;
}
}
return 0;
}
006 主席树(可持久化权值线段树)
水了TM这么多字终于到了正题力
给出序列a和m次查询,每次查询区间[l,r]第k大/小の值
看到第k大/小——权值线段树!!!
然而这里让求的是区间[l,r]的第k大/小的值
怎么找区间呢?
可持久化思想查询区间
由于可持久化是不断地新开链,那么可知其具有单调性&可加性,我们用前缀和的思想,在预处理时就把每个区间[1,i]update进去,那么区间[1,r]-[1,l-1]==区间[l,r](就是前缀和啦),找到对应的子树之后就成了查询全局第k小
Code
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#define int long long
using namespace std;
const int maxn=2e5+5;
inline int read()
{
int w=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9')
{
if(ch=='-')
{
f=-1;
}
ch=getchar();
}
while(ch>='0' && ch<='9')
{
w=(w<<3)+(w<<1)+(ch^48);
ch=getchar();
}
return w*f;
}
int n,m,cnt;
int a[maxn];
int b[maxn];
int root[maxn];
struct s_t
{
int ls;
int rs;
int sum;
}t[maxn<<5];
void update(int &p,int las,int l,int r,int k)
{
p=++cnt;
t[p]=t[las];
if(l==r)
{
t[p].sum++;
return ;
}
int mid=(l+r)>>1;
if(k<=mid)
{
update(t[p].ls,t[las].ls,l,mid,k);
}
else
{
update(t[p].rs,t[las].rs,mid+1,r,k);
}
t[p].sum=t[t[p].ls].sum+t[t[p].rs].sum;
}
int query(int p,int las,int l,int r,int k)
{
if(l==r)
{
return l;
}
int mid=(l+r)>>1;
int sum=t[t[p].ls].sum-t[t[las].ls].sum;
if(k<=sum)
{
return query(t[p].ls,t[las].ls,l,mid,k);
}
else
{
return query(t[p].rs,t[las].rs,mid+1,r,k-sum);
}
}
signed main()
{
n=read();
m=read();
for(int i=1;i<=n;i++)
{
b[i]=a[i]=read();
}
sort(b+1,b+n+1);
int num=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;i++)
{
a[i]=lower_bound(b+1,b+num+1,a[i])-b;
}
for(int i=1;i<=n;i++)
{
update(root[i],root[i-1],1,num,a[i]);
}
for(int i=1;i<=m;i++)
{
int l=read();
int r=read();
int k=read();
cout<<b[query(root[r],root[l-1],1,num,k)]<<endl;//这里一定要记得,查询出来的是下标
}
return 0;
}
还有就是记得要离散化!!!(毕竟也是权值线段树QWQ)
007 总结
以上就是静态主席树的基本内容了,都看到这了,各位看官点个赞再走吧!