[SP3946]MKTHNUM - K-th Number
Posted -wallace-
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[SP3946]MKTHNUM - K-th Number相关的知识,希望对你有一定的参考价值。
闲话
题解区一路翻下来居然没有归并树的题解?!那我来补一发吧。
对于像我这样的的juruo归并树当然是最好理解的。
分块在后面。
题解
在这道题之前,我们先来考虑这一个问题:
实现一种数据结构,支持静态查询区间([l,r])中有几个数(le x)。
假设我们已经可以高效地处理这个问题了,那么我们就可以使用二分查找法在(O( ext{查询一次的时间} imeslog_2 n))的复杂度内得到答案。
对于上述问题,看到区间,我们考虑线段树。不过,这个并不是像以往的一样,一个结点存放一个值或几个值,而是 一个数组——代表这个结点的维护区域排好序的结果。
For example:{1,4,3,6,9,0,3,2}
的维护结果:
注:结点编号为:
可以看出,这棵树相当于还原了归并排序
的完整过程,因而得名归并树
。
这样,对于一颗建好的归并树,我们如何在区间([l,r])内找出(le x)的数的个数呢?
大概分几个步骤:
若当前结点维护区域 被([l,r]) 所包含,那么显然我们可以用一次二分查找找到该维护区域内的第一个大于(x)的数(p),返回(p)前面的数的个数即可。
若没有完全包含,那么分别查看左右子结点的维护区域 是否有交集 ,有的递归累计答案,没有的不管,最后返回累计后的答案。
For example:以上述归并树为例,(l=2,r=6,x=3)时:
走到根节点,发现并没有被([l,r])完全包含,且([l,r])与之左右子结点的维护区域均有交集,于是分别递归左右子结点,返回
步骤2的答案+步骤3的答案
=2;走到结点2,发现并没有被([l,r])完全包含,且([l,r])与之左右子结点的维护区域均有交集,于是分别递归左右子结点,返回
步骤4的答案+步骤5的答案
=1;走到结点3,发现并没有被([l,r])完全包含,且([l,r])仅与之左子结点的维护区域有交集,于是递归左子结点,返回
步骤6的答案
=1;走到结点4,发现并没有被([l,r])完全包含,且([l,r])仅与之右子结点的维护区域有交集,于是递归右子结点,返回
步骤7的答案
=0;走到结点5,发现被([l,r])完全包含,直接使用二分法,发现1个数(le x),返回1;
走到结点6,发现被([l,r])完全包含,二分发现有1个数(le x),返回1;
走到结点9,发现被([l,r])完全包含,二分发现有没有数(le x),返回0;
- 最后答案为
2
。
递归结果可能难以理解,请仔细斟酌。
代码分步实现
重要部分有二:建树&查询
变量定义 variables
int L[N<<2],R[N<<2];//结点维护的区间范围
vector<int> dat[N<<2];//结点维护区域上数列排序后的结果
建树 build
void build(int l,int r,int rt=1)
{
L[rt]=l,R[rt]=r;//记录维护区间
if(l==r)//结束递归的条件
{
dat[rt].resize(1);
dat[rt][0]=Array[l];//叶子结点只存一个数
return;
}
int mid=(l+r)>>1;
build(l,mid,rt<<1),build(mid+1,r,rt<<1|1);//递归建树
dat[rt].resize(r-l+1);
merge(dat[rt<<1].begin(),dat[rt<<1].end(),
dat[rt<<1|1].begin(),dat[rt<<1|1].end(),
dat[rt].begin());//仿照归并排序有序合并两个子结点的排序后的结果
}
查询 query
int query(int l,int r,int x,int rt=1)//查询[l,r]中<=x的数的个数。
{
if(l<=L[rt]&&R[rt]<=r)//被完全包含
return upper_bound(dat[rt].begin(),dat[rt].end(),x)
-dat[rt].begin();//二分法查找第一个>x的数的下标,由于是下标从0开始的vector,将这个下标直接返回即可。
int mid=(L[rt]+R[rt])>>1,ret=0;
if(l<=mid) ret+=query(l,r,x,rt<<1);//如果答案与左边有关,递归左边
if(r>mid) ret+=query(l,r,x,rt<<1|1);//如果答案与右边有关,递归右边
return ret;//返回答案
}
时间复杂度
以下说法是对于原题(查询k-th)的。
建树的复杂度与归并排序相同,为(O(nlog_2 n));
查询(le x)的数的个数的复杂度为普通线段树的复杂度( imes)二分的复杂度,约为(O(log_2^2 n))
真正的查询还要加一个二分,因此 总复杂度 为(O(nlog n + mlog^3 n)),应该是比较优秀了,可以通过此题。
完整代码
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
const int N=1e5+7;
int Array[N];
namespace SGTree{
int L[N<<2],R[N<<2];
vector<int> dat[N<<2];
void build(int l,int r,int rt=1)
{
L[rt]=l,R[rt]=r;
if(l==r)
{
dat[rt].resize(1);
dat[rt][0]=Array[l];
return;
}
int mid=(l+r)>>1;
build(l,mid,rt<<1),build(mid+1,r,rt<<1|1);
dat[rt].resize(r-l+1);
merge(dat[rt<<1].begin(),dat[rt<<1].end(),
dat[rt<<1|1].begin(),dat[rt<<1|1].end(),
dat[rt].begin());
}
int query(int l,int r,int x,int rt=1)//count numbers which <=x
{
if(l<=L[rt]&&R[rt]<=r)
return upper_bound(dat[rt].begin(),dat[rt].end(),x)
-dat[rt].begin();
int mid=(L[rt]+R[rt])>>1,ret=0;
if(l<=mid) ret+=query(l,r,x,rt<<1);
if(r>mid) ret+=query(l,r,x,rt<<1|1);
return ret;
}
}
int n,m;
int Solve(int i,int j,int k)
{
int lb=0,ub=n;
while(ub-lb>1)
{
int mid=(lb+ub)>>1;
int cnt=SGTree::query(i,j,Array[mid]);
if(cnt>=k) ub=mid;
else lb=mid;
}
return Array[ub];
}
signed main()
{
while(scanf("%d%d",&n,&m)!=EOF)
{
for(register int i=1;i<=n;i++)
scanf("%d",Array+i);
SGTree::build(1,n);
sort(Array+1,Array+1+n);
int i,j,k;
while(m--)
{
scanf("%d%d%d",&i,&j,&k);
printf("%d
",Solve(i,j,k));
}
}
return 0;
}
总结
线段树在处理区间问题时是一个利器,熟练了可以受益匪浅,常做线段树的题还可以锻炼思维。有些题线段树虽然复杂度不一定是最优的,但是它代码简单,易于实现,所以掌握它还是很必要的。
AC record:https://www.luogu.com.cn/record/29929630
拓展:分块
众所周知,线段树能做的大部分分块都可以做,那么这题也是显然。这里我将分块做法也 提一下(?)。
我们将数列分为(t)块,每个块放(dfrac{n}{t})个排好序的数。维护([l,r])的块存的是(A[l...r])的排序后的结果。这里块推荐使用vector。
那么对于每次查询,我们同样将它分成二分和找(le x)的数的个数两个部分。
二分不讲了,要查询([l,r])中(le x)的数的个数:
整块:二分法,原理同线段树;
散块:在 原数组上 暴力查找。
这样一下(包括二分)是(O(tlog (dfrac{n}{t})+dfrac{n}{t}))
预处理复杂度(O(nlog (dfrac{n}{t})))
总复杂度(O(nlog (dfrac{n}{t}) +mtlog(dfrac{n}{t})+dfrac{nm}{t}))
注:由于查询在整块与散块的复杂度不同,所以(t=sqrt{n})不一定是最佳选择,一种参考值是(t=dfrac{sqrt{n}}{log ^{0.5}n})。
参考
- 《挑战程序设计竞赛(第2版)》3.3
完结撒花。。。。有错误请在评论请指正。
求赞(QwQ)
以上是关于[SP3946]MKTHNUM - K-th Number的主要内容,如果未能解决你的问题,请参考以下文章