主席树学习笔记(poj2104,bzoj1901/zoj2112)

前言

千辛万苦才明白主席树是什么东西,同机房的jar形把我坑害得好惨......另外,网上的主席树资料都很精炼啊,像我这种蒟蒻怎么可能看的懂,不过总算是找到了一些好资料。

主席树的原理

同机房的Jar形说主席树不是树,是森林,是有一定道理的,因为主席树是很多的树。要说起主席树,肯定要说poj2104这道题。

我们首先把数据排个序后去重,比如4,4,1,2,5(记为a数组)

排序后去重:1,2,4,5(记为b数组)

然后我们要建树了,这个建树是这样的,对于a数组的每一个前缀[1...i],建立一棵树,树的每个节点表示这个前缀里在b数组[S,T]中出现的元素个数,比如对于4,4,1这个前缀,在[1,3]中出现了3个元素,而在[1,2]中只出现了1个元素。这样保存有什么好处呢?对于每个b数组中的区间[S,T],我们想知道a数组[l,r]中的k大数是在左边还是在右边,我们就可以用表示[1...l-1](a)中在树里面保存的sum值减去[1...r](a)中的sum值得到[l...r](a)在[S,T](b)中元素的个数。通过这种方式也可以得到左子树在[S,mid]中的元素个数,如果小于k,那么k大树在右边,如果大于k,k大数在左边。

可是有一个大问题,每次新建一棵树,空间会受不了的。

没关系,我们发现前缀加了一个值之后,只有和这个值相关的区间应该被修改,其他的sum值和前一棵树是一样的,那么我们每次只要新建一条边,就是和这个值有关的区间的节点的边,剩下的直接指向前一棵树就行了!

代码

 

#include<iostream>
#include<algorithm>
#include<string>
#include<vector>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<queue>
#include<map>
#include<climits>
using namespace std;
int n,m,tot,cur,kl;
int a[100005],b[100005];
struct node{
	int sum;
	int ls,rs;
}tree[3000005];
int root[100005];
void upda(int l,int r,int &x,int y,int pos){
	cur++;tree[cur]=tree[y];//首先继承上一棵树,然后这条边权值要加上1(因为当前i是这个区间里的树)
	x=cur;tree[cur].sum++;
	if(l==r)return;
	int mid=(l+r)/2;
	if(pos>mid)upda(mid+1,r,tree[cur].rs,tree[y].rs,pos);//如果当前在右子树中,那么我们更新右子树里的某条链
	else upda(l,mid,tree[cur].ls,tree[y].ls,pos);//左子树
}
int find(int l,int r,int x,int y,int k){
	if(l==r)return l;
	int mid=(l+r)/2,kl;
	kl=tree[tree[y].ls].sum-tree[tree[x].ls].sum;//区间sum值
	if(kl>=k)return find(l,mid,tree[x].ls,tree[y].ls,k);
	else return find(mid+1,r,tree[x].rs,tree[y].rs,k-kl);
}
int main()
{
    int i,j,x,y,z,zhi;
    while(scanf("%d%d",&n,&m)==2){
    	for(i=1;i<=n;i++){
    		scanf("%d",&a[i]);b[i]=a[i];}
    	sort(b+1,b+1+n);
    	tot=1;
    	for(i=1;i<=n;i++)//去重,便于离散化
    		if(b[i]!=b[tot]){tot++;b[tot]=b[i];}
    	cur=1;
    	tree[0].ls=0;tree[0].rs=0;tree[0].sum=0;
    	for(i=1;i<=n;i++){//依次插入建树
    		zhi=lower_bound(b+1,b+1+tot,a[i])-b;
    		upda(1,n,root[i],root[i-1],zhi);//有关zhi的这条边是不同的
    	}
    	for(i=1;i<=m;i++){
    		scanf("%d%d%d",&x,&y,&z);
    		zhi=find(1,n,root[x-1],root[y],z);//用y中元素个数减去x-1中的得到的就是x~y中的
    		printf("%d\n",b[zhi]);
    	}
    }
    return 0; 
}    
/*
	保存某个前缀在离散化后的s到t的值的个数。
	例如4,2,5,2,3
	对于前缀4,2
	在1,2,3里有1个,在1,2,3,4里有2个
	这样查询第k大数会比较快。
	发现对于某两个前缀,只有有关新加上的数字的一条边不同
*/

 

 

带修改的区间第k小数

 

 

时隔近一年的再次更新......

我们知道,主席树的每一棵树维护的是一段前缀,而前缀修改,用树状数组是很吼滴,所以我们要试一试主席树套树状数组。

于是我们看到例题bzoj1901(也就是zoj2112)

离线操作,所有原数列与修改后的数中出现的数都要进行离散。

每次修改,利用树状数组类似原理进行修改,每次修改就要新建立若干条链。查询操作则是将树状数组原理要查询的在左子树中的节点统统提取出来,仿照主席树的方法进行查询。

总之......代码比言语更清楚。

 

代码

#include<bits/stdc++.h>
using namespace std;
const int N=10005,M=10005;
int n,m,top,tot=1,sz,lsz,rsz;
int a[N+M],b[N+M],xx[M],yy[M],kk[M],flag[M];
struct node{int sum,ls,rs;}tree[N*250];
int rot[N],L[N],R[N];
int lowbit(int x) {return x&(-x);}
void upda(int s,int t,int &x,int las,int pos,int num) {//建立新链+修改操作
	x=++sz,tree[x]=tree[las],tree[x].sum+=num;
	int mid=(s+t)>>1;
	if(s==t) return;
	if(pos>mid) upda(mid+1,t,tree[x].rs,tree[las].rs,pos,num);
	else upda(s,mid,tree[x].ls,tree[las].ls,pos,num);
}
int query(int l,int r,int kth) {
	if(l==r) return l;
	int suml=0,sumr=0,mid=(l+r)>>1;
	for(int i=1;i<=lsz;++i) suml+=tree[tree[L[i]].ls].sum;
	for(int i=1;i<=rsz;++i) sumr+=tree[tree[R[i]].ls].sum;//查询左子树代表的该区间里的数的个数
	if(sumr-suml>=kth) {
		for(int i=1;i<=lsz;++i) L[i]=tree[L[i]].ls;
		for(int i=1;i<=rsz;++i) R[i]=tree[R[i]].ls;
		return query(l,mid,kth);
	}
	else {
		for(int i=1;i<=lsz;++i) L[i]=tree[L[i]].rs;
		for(int i=1;i<=rsz;++i) R[i]=tree[R[i]].rs;
		return query(mid+1,r,kth-(sumr-suml));
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]),b[++top]=a[i];
	for(int i=1;i<=m;++i) {
		scanf("%s",ch);scanf("%d%d",&xx[i],&yy[i]);
		if(ch[0]=='Q') scanf("%d",&kk[i]),flag[i]=1;
		else b[++top]=yy[i];//b:用于离散的数组
	}
	sort(b+1,b+1+top);
	for(int i=2;i<=top;++i) if(b[i]!=b[tot]) b[++tot]=b[i];
	for(int i=1;i<=n;++i) {//建树
		int tt=lower_bound(b+1,b+1+tot,a[i])-b;
		for(int j=i;j<=n;j+=lowbit(j)) upda(1,tot,rot[j],rot[j],tt,1);
	}
	for(int i=1;i<=m;++i)
		if(flag[i]) {
			lsz=rsz=0,--xx[i];
			for(int j=xx[i];j;j-=lowbit(j)) L[++lsz]=rot[j];//提取为了查左右端点前缀需要查的节点
			for(int j=yy[i];j;j-=lowbit(j)) R[++rsz]=rot[j];
			printf("%d\n",b[query(1,tot,kk[i])]);
		}
		else {
			int tt=lower_bound(b+1,b+1+tot,a[xx[i]])-b;
			for(int j=xx[i];j<=n;j+=lowbit(j))//删除原值影响
				upda(1,tot,rot[j],rot[j],tt,-1);
			a[xx[i]]=yy[i];tt=lower_bound(b+1,b+1+tot,yy[i])-b;
			for(int j=xx[i];j<=n;j+=lowbit(j))//更新
				upda(1,tot,rot[j],rot[j],tt,1);
		}
    return 0;
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值