POJ 2104 K-th Number 静态区间第k大 主席树

题意:给你一个长度为N数列(N<=100000),有Q(Q<=5000)个询问,每个询问包含l和r.对于每个询问,要求输出[l,r]区间第k大的数.

下面我讲一下主席树怎么做这题,并穿插着介绍了函数式线段树:主席树.

首先,如果询问的区间只有一个,那么我们可以用线段树做.我们先将原数组离散化,然后建一棵线段树,让线段树的每个节点保存"对应的区间内的数出现的次数的总和".比如,2号节点对应的区间为[4,7],2号节点保存的值sum[2]=4,表示[4,7]区间内的数总共出现了4次.这样,求第k大的数时,我们在线段树上二分地找就能找到所询问的整个区间的第k大的数了.

然后,对于任意一个数列a[1..i](1 <= i <= N),我们可以建出我上面所说的一棵线段树,一共有N棵.不难发现,这N棵线段树相当于维护了一个"前缀和".这样这些线段树就有一个很好的性质:满足区间减法.所以,我们只要将第r棵线段树和第i-1棵线段树对应节点的值相减即得到了[l,r]区间的信息,然后我们照着上一段的做法,二分地找就行了.

这样,时间复杂度是令人愉悦的O(N*logN),但是,因为要建N棵线段树,空间复杂度就是令人沮丧的O(N*N)了.然而,进一步思考我们很容易发现,第i+1棵线段树只是在第i棵线段树的基础上修改了一条链上的logN个节点,所以我们大可不必开N棵独立的线段树.后一个版本的线段树可以利用前一个版本的线段树的绝大多数的节点,只要新开logN个不同的节点就可以了.于是,空间复杂度也变成了可喜的O(N*logN).

以上是函数式编程的思想在这里的应用:主席树.

#include <cstdio>
#include <algorithm>
using namespace std;

const int MAX_N = 100005;
const int MAX_M = MAX_N * 30;

int N, M, Q, a[MAX_N], t[MAX_N];
int tot, T[MAX_N], lson[MAX_M], rson[MAX_M], c[MAX_M];

void init_hash()
{
	for (int i = 1; i <= N; ++i) {
		t[i] = a[i];
	}
	sort(t + 1, t + 1 + N);
	M = unique(t + 1, t + 1 + N) - (t + 1);
}

int Hash(int x)
{
	return lower_bound(t + 1, t + 1 + M, x) - t;
<span style="white-space:pre">	</span>// 注意:上面应该是M而不是N.我这个错误的代码竟然还过了POJ,(⊙﹏⊙)b.
<span style="white-space:pre">	</span>// 早就听闻POJ数据水,原来真的有这么水呀(⊙﹏⊙)b
}

int build(int l, int r)
{
	int root = tot++;
	c[root] = 0;
	if (l < r) {
		int mid = (l + r) / 2;
		lson[root] = build(l, mid);
		rson[root] = build(mid + 1, r);
	}
	return root;
}

int update(int root, int pos, int val)
{
	int newroot = tot++, tmp = newroot;
	c[newroot] = c[root] + val;
	int l = 1, r = M;
	while (l < r) {
		int mid = (l + r) / 2;
		if (pos <= mid) {
			r = mid;
			rson[newroot] = rson[root]; lson[newroot] = tot++;
			newroot = lson[newroot]; root = lson[root];
		}
		else {
			l = mid + 1;
			lson[newroot] = lson[root]; rson[newroot] = tot++;
			newroot = rson[newroot]; root = rson[root];
		}
		c[newroot] = c[root] + val;
	}
	return tmp;
}
int query(int letf_root, int right_root, int k)
{
	int l = 1, r = M;
	while (l < r) {
		int mid = (l + r) / 2;
		if (c[lson[right_root]] - c[lson[letf_root]] >= k) {
			r = mid;
			letf_root = lson[letf_root]; right_root = lson[right_root];
		}
		else {
			l = mid + 1;
			k -= c[lson[right_root]] - c[lson[letf_root]];
			letf_root = rson[letf_root]; right_root = rson[right_root];
		}
	}
	return l;
}

int main()
{
	while (scanf("%d%d", &N, &Q) != EOF) {
		tot = 0;
		for (int i = 1; i <= N; ++i) {
			scanf("%d", &a[i]);
		}
		init_hash();
		T[0] = build(1, M);
		for (int i = 1; i <= N; ++i) {
			int pos = Hash(a[i]);
			T[i] = update(T[i - 1], pos, 1);
		}
		while (Q--) {
			int l, r, k; scanf("%d%d%d", &l, &r, &k);
			printf("%d\n", t[query(T[l - 1], T[r], k)]);
		}
	}
	return 0;
}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值