快乐找到区间第K大——划分树

划分树,顾名思义就是将一个序列,划分成很多小部分。其作用就是可以快乐地找到给定区间第K大的数。其实使用归并树和快排也都可以找到区间内第K大的数,但其效率都不如划分树要好,快排过的时间复杂度O(n x m),而划分树是O(n x logn)。

划分树原理:
1. 建树:根结点就是原序列,左孩子保存父结点所有元素排序后的一半,右孩子也存一半,也就是说排名1 -> mid的存在左边,排名(mid+1) -> r 的存在右边,同一结点上每个元素保持原序列中相对的顺序,蓝色数字表示其是在左子树中的,最后通过递归左右子树建立整棵树,同时在建树的过程中记录一个辅助数组num[i],其意义是从 l 到 i 其中小于中间值的元素个数。
(来自hchlqlz博客)
图示:
在这里插入图片描述2. 查询:对于在区间[l,r]中的第k大的询问,先比较num[r]- num[l-1] (在区间 [l,r] 中在左子树中的元素个数) 和k的值。
  如果num[r]- num[l-1] >= k (左子树中的个数大于k[l,r] 中的第k大的数字在左子树,到左子树中去找。
  否则第k大的元素就在右子树。此处要注意递归到左右子树时, lrk 值的下一状态。
举个栗子:
  比如要查找2 到6 之间第3 大的数,那么先判断2 到6 之间有多少元素进入左子树,(在此忽略细节)num[6]-num[2-1]=2,就说明2 到6 有两个数进入左子树,又因为我们要找的是第3 大的数,所以一定在右子树中

划分树步骤:
1.建树
  可以发现,划分树的每一层结点都是n个,所以采用一个tree[max][max]的二维数组来存储整棵树,同样num数组也采用同样的方式存储。
  a.将原数组sort后,存在一个数组sorted中,在每个区间中找到区间中间值mid(偏左)
  b.扫描原数组在区间[l,r]的部分,将l到r中的小于等于mid的数组个数记录下来在num数组中
  c.将小于等于mid的树放到左子树,将大于mid的放到右子树
  d.向左、右子树开始递归建树  递归基:r == l 时,返回
2.查询区间[l,r]中第k大的元素
  if(k > num[r]-num[l-1]) 进入右子树

  if(k <= num[r]-num[l-1]) 进入左子树

下面以POJ 2014为例 给出代码 http://poj.org/problem?id=2104

题意:给出n个数,进行m询问,每次询问给出l,r,k 找出区间[l,r]的第k大的数。
代码:

/*
1.建树
	a.将原数组sort后,存在一个数组sorted中,在每个区间中找到区间中间值mid
	b.扫描原数组在区间[l,r]的部分,将l到r中的小于等于mid的数组个数记录下来在num数组中
	c.将小于等于mid的树放到左子树,将大于mid的放到右子树
	d.向左、右子树开始递归建树
	  递归基:r == l 时,返回
2.查询区间[l,r]中第k大的元素
	判断num[r]-num[l-1]与k值的大小
		a.if(k > num[r]-num[l-1]) 进入左子树
		  if(k <= num[r]-num[l-1]) 进入右子树
Tip:1.注意查询递归转移的s,e,l,r的参数更新变化
	2.注意每次区间中num数组的要初始化
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int Max = 100010;
int n,m,mid_sort,last,ans= 0 ;
int a[Max],sorted[Max];
int tree[20][Max],num[20][Max] = {0};
void creat(int l,int r,int layer){
	//递归基
	if(l == r) {
		last = layer;
		return ;
	}
	//找到中间点的位置
	int mid = (l+r)>>1;
	mid_sort = sorted[mid];
	//左右子树的起始位置
	int cnt_l = l-1,cnt_r = mid; 
	//cnt_same:当前结点可以容纳多少个和mid_sort一样的元素的上限
	//cnt_same值为l到mid的元素个数减去l到r中小于mid_sort的元素个数
	int cnt_same = mid-l+1;
	for(int i = l; i <= r; i++) if(tree[layer][i] < mid_sort) cnt_same--;
	for(int i = l; i <= r; i++){
		//注意每次区间中num数组的要初始化
		if(i == l) num[layer][i] = 0;
		else num[layer][i] = num[layer][i-1];
		//元素小于中间值mid_sort 或者 等于mid_sort且没到达上限  放到左子树
		if(tree[layer][i] < mid_sort || (tree[layer][i] == mid_sort && cnt_same > 0)) {
			num[layer][i]++;
			tree[layer+1][++cnt_l] = tree[layer][i];
			if(tree[layer][i] == mid_sort)
			cnt_same--;
		}
		else {
			tree[layer+1][++cnt_r] = tree[layer][i];
		}
			
	}
	creat(l,mid,layer+1);
	creat(mid+1,r,layer+1);
}
//s代表当前区间的左界,e代表当前区间的右界,layer为树的层数 ,l是查询的左界,r是查询右界,k是题中定义
int query(int s,int e,int layer,int l,int r,int k){
	//递归基
	if(s == e) {return tree[layer][s];}
	//ll表示 l 前面有多少元素进入左孩子
	int ll ;
	if(l != s) ll= num[layer][l-1];
	else ll = 0;
	//toleft表示左孩子里有多少元素
	int toleft;
	int mid = (s+e)>>1;
	toleft = num[layer][r] - ll;
	//cout<<"num[layer][r] = "<<num[layer][r]<<" num[layer][l-1] = "<<num[layer][l-1]<<" s  = "<<s<<" e = "<<e<<" layer = "<<layer<<" l = "<<l <<" r = "<<r<<" k = "<<k<<" toleft = "<<toleft<<"\n";
	
	if(toleft >= k)
		//s+ll是掠过s之前(不包含s)的被分到左孩子里的元素,s+num[layer][r]-1也是相似意思
		return query(s,mid,layer+1,s+ll,s+num[layer][r]-1,k);
	else{
		// l-s 表示l前面有多少数,再减ll 表示这些数中去右子树的有多少个
		int lr = mid+1+(l-s-ll);
		// r-l+1 表示l到r有多少数,减去去左边的,剩下是去右边的,去右边1个,下标就是lr,所以减1
		//因为k 大于 去左孩子的元素个数,所以就是找左孩子中第k-toleft大的数
		return query(mid+1,e,layer+1,lr,lr+r-l+1-toleft-1,k-toleft);
	}
}
int main(){
	scanf("%d %d",&n,&m);
	int l,r,k;
	for(int i = 1; i <= n; i++) scanf("%d",a+i),tree[1][i] = sorted[i] = a[i];
	sort(sorted+1,sorted+n+1);
	creat(1,n,1);
	for(int i = 0; i < m; i++){
		scanf("%d %d %d",&l,&r,&k);
		printf("%d\n",query(1,n,1,l,r,k));
	}
	return 0;
}

参考内容:https://www.cnblogs.com/hchlqlz-oj-mrj/p/5744308.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值