标签:img 线段树 列排序 amp 分块 number return 左右 http
题解区一路翻下来居然没有归并树的题解?!那我来补一发吧。
对于像我这样的的juruo归并树当然是最好理解的。
分块在后面。
在这道题之前,我们先来考虑这一个问题:
实现一种数据结构,支持静态查询区间\([l,r]\)中有几个数\(\le x\)。
假设我们已经可以高效地处理这个问题了,那么我们就可以使用二分查找法在\(O(\text{查询一次的时间}\times\log_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
。递归结果可能难以理解,请仔细斟酌。
重要部分有二:建树&查询
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)//查询[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(n\log_2 n)\);
查询\(\le x\)的数的个数的复杂度为普通线段树的复杂度\(\times\)二分的复杂度,约为\(O(\log_2^2 n)\)
真正的查询还要加一个二分,因此 总复杂度 为\(O(n\log n + m\log^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\n",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(t\log (\dfrac{n}{t})+\dfrac{n}{t})\)
预处理复杂度\(O(n\log (\dfrac{n}{t}))\)
总复杂度\(O(n\log (\dfrac{n}{t}) +mt\log(\dfrac{n}{t})+\dfrac{nm}{t})\)
注:由于查询在整块与散块的复杂度不同,所以\(t=\sqrt{n}\)不一定是最佳选择,一种参考值是\(t=\dfrac{\sqrt{n}}{\log ^{0.5}n}\)。
完结撒花。。。。有错误请在评论请指正。
求赞\(QwQ\)
标签:img 线段树 列排序 amp 分块 number return 左右 http
原文地址:https://www.cnblogs.com/-Wallace-/p/12267934.html