第一篇Blog……


嗯……主要为了帮助自己整理做题的思路,于是决定写写Blog尝试一下……

今天先总结一道去年数算实习机考的题--第K大数(POJ2104) 吧……


题目概要:

先给定一个n个数的序列,然后做m次询问,每次询问形式为(i, j, k),表示询问第a个数到第b个数之内第k小的数是几。

(1<= n <= 100000,1 <= m<= 5000, 序列中的数绝对值 <= 10^9)

(题目名说的是第k大……算了不要在意这些细节,反正题意肯定是求第k小)


该题(经室友指导)可以用可持久化线段树。这是个什么东西呢?基本上就是一棵线段树,但每次“修改”一个点的时候,并不修改原节点的值,而是把原节点复制一份,然后修改新的节点。比如说要在某个父节点下修改某个子节点,则先把父节点复制一份(因为父节点也修改了,只要有修改就复制),然后把对应子节点(不妨设为左节点)复制一份,改变其中的相关信息,然后让新父节点的左指针指向新的左子节点,右指针不变,与旧父节点相同,是指向旧的右子节点。

然后这个东西跟这个问题有啥关系呢?可以这样搞:初始的时候创建一棵线段树,每个节点维护一个size信息,表示这个节点代表的区间里面已经插入了多少个数(初始为0)。从前往后遍历整个序列,每次把这个数插入到这棵可持久化线段树当中,然后插入路径上的节点都创建了新的,并且比旧的size值+1。其中必然有新的根,把新根的id记录下来。(我们用顺序方法存储这棵树)

插入完成之后,得到一颗错综复杂的树……这怎么用来处理询问呢?比如询问是从i到j。我们可以注意到,从第j个根往下遍历所有子节点,得到各节点的size值是插入完第j个数之后的size值;对于第i-1个根,得到的是插入完第i-1个数之后的size值。于是把这两棵树对应位置(代表同一区间的)节点相减,得到的正是从插入第i个数到插入第j个数之间各节点得到的size值,也就是只有[i, j]区间的信息。这样我们只需要进行一次搜索,从根节点往下,每次把k与左节点的size值比较,如果小于则往左找,如果大于则把k减掉左节点size,并往右找。这样找到底就是我们要的区间第k小数了。


这样就可以O(logn)解决每个询问了……预处理时间是O(nlogn),于是就可以了

噢对了,还有一个麻烦事,就是虽然数只有n个,但大小可以很大,如果对-10^9到10^9维护一个线段树就炸飞天了,于是需要先离散化,具体来说就是先对序列排序(间接排序),求出序列名次 -> 位置的映射,然后反过来求一个位置 -> 名次的映射,用这个名次值去维护线段树,最后输出的时候,是用树里找出来的名次值先换回位置,在去原序列里找到原数值。

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <algorithm>

using namespace std;

struct Node
{
	int size;
	int lc;
	int rc;
} node[2000007]={};

int root[100007]={};
int nID = 0;
int N,M;
int arr[100007]={};
int sa[100007]={};
int rank[100007]={};

void Insert(int o, int l, int r, int x)		//预处理的插入操作 
{
	if(l == r)
	{
		return;
	}
	int m = (l+r)/2;
	if(x <= m)
	{
		node[++nID] = node[node[o].lc];		//新建左子节点 
		node[o].lc = nID;			//更新当前节点的左子节点索引 
		node[nID].size++;			//更新左子节点信息 
		Insert(nID, l, m, x);		<span style="white-space:pre">	</span>//向左子节点插入 
	}
	else
	{
		node[++nID] = node[node[o].rc];		//新建右子节点... 
		node[o].rc = nID;
		node[nID].size++;
		Insert(nID, m+1, r, x);
	}
}

int Query(int a, int b, int k)			//查询 
{
	int o1 = root[a-1], o2 = root[b];	//o1, o2表示两棵树中的对应位置的节点, o1与o2的size作差即为待查区间的size信息 
	int l = 1;				// 区间范围: [l, r] 
	int r = N;
	
	while(l < r)
	{
		int lSize = node[node[o2].lc].size - node[node[o1].lc].size;	//计算左子节点size 
		if(k <= lSize)			//确定插入方向 
		{
			o1 = node[o1].lc;
			o2 = node[o2].lc;
			r = (l+r)/2;		//更新区间范围 
		}
		else
		{
			k -= lSize;
			o1 = node[o1].rc;
			o2 = node[o2].rc;
			l = (l+r)/2 + 1;
		}
	}
	
	return l;
}

bool cmp(int i, int j)			//位置之间比大小的函数 
{
	return arr[i] < arr[j];
}

int main()
{
	scanf("%d%d", &N, &M);
	/* 离散化 */ 
	for(int i = 1; i <= N; i++)
	{
		scanf("%d", &arr[i]);
		sa[i] = i;		//待排序的数组, 准备得到名次 -> 位置的映射 
	}
	sort(sa+1, sa+N+1, cmp);	//排序. 现在sa数组存放的位置1~N,按照在原序列中大小关系的顺序 
	for(int i = 1; i <= N; i++)
	{
		rank[sa[i]] = i;	//求反函数, 即位置 -> 名次的映射 
	}
	
	for(int i = 1; i <= N; i++)	//依次插入序列中的值 
	{
		root[i] = ++nID;		<span style="white-space:pre">	</span>//保存新根ID 
		node[root[i]] = node[root[i-1]];	//维护新根节点信息... 
		node[root[i]].size++;
		Insert(root[i], 1, N, rank[i]);		//向新根插入需要插入的数 
	}
	
	for(int i = 0; i < M; i++)
	{
		int a,b,k;
		scanf("%d%d%d", &a, &b, &k);
		int ans = Query(a, b, k);
		printf("%d\n", arr[sa[ans]]);		//输出时,用名次值换回原值输出 
	}
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值