poj-2104 K-th Number[主席树/函数式线段树/可持久化线段树]

膜拜大神:点击打开链接 点击打开链接

【题目描述】有n个数字排成一列,有m个询问,格式为:left right k .即问在区间[left,right]第k大的数据为多少?

纯属个人理解,有不正确的地方欢迎留言指正:


先来设想下如何解决这个问题。

把数字在数组中的位置i作为定义域,数字的值v[i]作为值域。

假如对于 [left,right]的数我们能知道它们的值域在不同区间出现的个数,就可以根据出现个数来二分查找来找的第k大的值,而区间出现的个数可以用线段树来存储。

为了避免浪费空间,将值域离散化存进线段树。对于某段值域[l,r],[left,right]中数字出现的个数 =[0,right]中数字出现的个数-[0,left-1]中数字出现的个数

那么就可以建立 n+1颗线段树,第i颗线段树记录[0,i]数字的值域的区间出现次数。

举个栗子:

4
10 31 42 15  
这个数据按照上面思路可以建立5个线段树


很明显,时间效率和空间效率都很高。

从图上也能很直观的看出每个线段树之间有很多可以共用的节点。

可持久化数据结构

在算法执行的过程中,会发现在更新一个动态集合时,需要维护其过去的版本。这样的集合称为是可持久的。

实现持久集合的一种方法时每当该集合被修改时,就将其整个的复制下来,但是这种方法会降低执行速度并占用过多的空间。

可持久化数据结构(Persistent data structure)就是利用函数式编程的思想使其支持询问历史版本、同时充分利用它们之间的共同数据来减少时间和空间消耗。

单点更新

第i个树建立的时候,是在第i-1个树的基础上把某条从树根到叶子节点的路径上的节点+1。这条路径上的节点是必须新建的,非此路径的节点可以与上颗树共用的,与它共用。

这明显只能用链式结构,所以不能对线段树用标号实现了。


结构

struct node{
	node *ls,*rs;
	int cnt;
};

第0颗树就只能单独建立了

node *build(int l,int r)
{//构建第0个线段树
	node *rt=get_nd();
	if(l==r){
		rt->cnt=0,rt->ls=rt->rs=NULL;
		return rt;
	}
	int mid=(l+r)>>1;
	rt->ls=build(l,mid);
	rt->rs=build(mid+1,r);/
	push_up(rt);
	return rt;
}

构建第1颗到第n颗线段树

//pos为第i个数的离散化后的值域,val=1表示增加的个数
//pre是第i-1颗线段树的指针
node *update(node *pre,int l,int r,int pos,int val)
{//在前一个线段树的基础上构建此次线段树
	node *rt=get_nd();
	*rt=*pre;
	if(l==pos&&r==pos){
		rt->cnt+=val;
		return rt;
	}
	int mid=(l+r)>>1;
	if(pos<=mid) rt->ls=update(pre->ls,l,mid,pos,val);
	else rt->rs=update(pre->rs,mid+1,r,pos,val);
	push_up(rt);
	return rt;
}

查询第k小个值

int query(node *cur,node *pre,int l,int r,int kth)
{//查询
	if(l==r) return l;
	int mid=(l+r)>>1;
	int lim=cur->ls->cnt-pre->ls->cnt;
	if(kth<=lim) return query(cur->ls,pre->ls,l,mid,kth);
	else return query(cur->rs,pre->rs,mid+1,r,kth-lim); 
}

poj2104 代码:

/*【题目描述】有n个数字排成一列,有m个询问,格式为:left right k
 即问在区间[left,right]第k大的数据为多少?*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>
#define find_max(a,b) a>b?a:b
using namespace std;
const int maxn=100008;
struct node{
	node *ls,*rs;
	int cnt;
}rsta[maxn*30];//内存池
int scnt,n,m;
node *rot[maxn];//第i个线段树的根节点
int v[maxn];//每个数字的原值
int dv[maxn];//每个数字的离散值
int sub_v[maxn];//v排序后的备份
int maxv;//最大的离散值
inline node *get_nd()
{//从预开的内存池中得到一个节点
	return &rsta[scnt++];
}
inline void push_up(node *rt)
{//上压
	rt->cnt=rt->ls->cnt+rt->rs->cnt;
}
node *build(int l,int r)
{//构建第0个线段树
	node *rt=get_nd();
	if(l==r){
		rt->cnt=0,rt->ls=rt->rs=NULL;
		return rt;
	}
	int mid=(l+r)>>1;
	rt->ls=build(l,mid);
	rt->rs=build(mid+1,r);/
	push_up(rt);
	return rt;
}
node *update(node *pre,int l,int r,int pos,int val)
{//在前一个线段树的基础上构建此次线段树
	node *rt=get_nd();
	*rt=*pre;
	if(l==pos&&r==pos){
		rt->cnt+=val;
		return rt;
	}
	int mid=(l+r)>>1;
	if(pos<=mid) rt->ls=update(pre->ls,l,mid,pos,val);
	else rt->rs=update(pre->rs,mid+1,r,pos,val);
	push_up(rt);
	return rt;
}
int query(node *cur,node *pre,int l,int r,int kth)
{//查询
	if(l==r) return l;
	int mid=(l+r)>>1;
	int lim=cur->ls->cnt-pre->ls->cnt;
	if(kth<=lim) return query(cur->ls,pre->ls,l,mid,kth);
	else return query(cur->rs,pre->rs,mid+1,r,kth-lim); 
}
void discretize()
{//离散化
	for(int i=1;i<=n;++i)
		sub_v[i]=v[i];
	sort(sub_v+1,sub_v+n+1);
	int size=unique(sub_v+1,sub_v+n+1)-sub_v-1;//size为离散化后元素个数
	for(int i=1;i<=n;++i)//k为b[i]经离散化后对应的值
	{
		dv[i]=lower_bound(sub_v+1,sub_v+size+1,v[i])-sub_v;
		maxv=find_max(dv[i],maxv);
	}
}
void solve()
{
	scnt=0,maxv=0;
	discretize();
	int t=maxv,r=1;
	while(t) t/=2,r*=2;//求值域范围
	rot[0]=build(1,r);
	for(int i=1;i<=n;++i)
		rot[i]=update(rot[i-1],1,r,dv[i],1);
	int x,y,k;
	while(m--)
	{
		scanf("%d%d%d",&x,&y,&k);
		k=query(rot[y],rot[x-1],1,r,k);//得到的是离散值
		printf("%d\n",sub_v[k]);
	}
}
int main()
{
	while(~scanf("%d%d",&n,&m))
	{
		for(int i=1;i<=n;++i)
			scanf("%d",v+i);
		solve();
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值