【学习笔记】可持久化权值线段树--主席树 (静态)

主席树——可持久化权值线段树(静态)

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 总结

以上就是静态主席树的基本内容了,都看到这了,各位看官点个赞再走吧!

posted @ 2022-09-16 11:50  NinT_W  阅读(78)  评论(3编辑  收藏  举报