例题:http://poj.org/problem?id=2104


最近可能是念念不忘,必有回响吧,总是看到区间第k大的问题,第一次看到是在知乎上有人面试被弄懵了后来又多次在比赛中看到。以前大概是知道怎么解决但是没有实际操作过。直到昨天看到了POJ上的2104题,一个标准的区间第K大询问,然后好好总结了一下区间第K大的问题。

普通人要是没想过这个问题,突然被问到第一个反应肯定和知乎上面试的哥们儿一样,把区间里面的所有数拎出来,排序,找第K个,但是这样时间复杂度是很大的,如果m次询问,时间复杂度是O( m×(n + n×logn) )要是询问次数m非常大时间复杂度很恐怖。

要是优化就有很多种方法,第一种就是利用分治的思维,分块。将n个数分成√n × logn  块,然后对每个块进行排序。既然是区间第K大,那假设N是区间内第K大的数,那么不大于N的数至少有K个。这样对N值进行二分枚举,每枚举出一个N值,然后去区间中找不大于N的数。因为对于每个块都是排好序的,所以如果该块完全包含在区间内,就直接对块进行二分查找不大于N的数有多少个。块部分包含在区间内的就直接暴力查找(过程如图1.1所示)。根据查找的值再扩大或者缩小N值。

                   图1.1  

这个时候时间复杂度就是O( n×logn + m√nlog1.5n)

 void init() {
scanf("%d%d",&n,&m);//n个数m次询问
unit = ;//分块大小
for(ll i=;i<n;i++) {
scanf("%d",&num[i]);
OdrArr[i] = num[i];
ve[i/unit].push_back(num[i]);//分别装入块中
}
for(ll i=;i<n/unit;i++)//最后一个块不用排序
sort(ve[i].begin(),ve[i].end());//对每个块排序
sort(OdrArr, OdrArr+n);//二分枚举N值
}

初始化分块

 int query(int L, int R, int k) {//询问区间L,R内的第k大
int l = -, r = n-;
while(r - l > ) {//结束状态为l + 1 = r, r取闭
int cnt = ;
int mid = (l + r) >> ;
int temp_l = L, temp_r = R+;//设定区间为左闭右开
int va = OdrArr[mid];//二分枚举N值 //不完全包含在区间的部分
while(temp_l < temp_r && temp_l%unit)
if(num[temp_l++] <= va)
cnt++;
while(temp_l < temp_r && temp_r%unit)
if(num[--temp_r] <= va)
cnt++; //完全包含在区间中的块
while(temp_l < temp_r) {
int b = temp_l/unit;
cnt += upper_bound(ve[b].begin(), ve[b].end(), va) - ve[b].begin();
temp_l += unit;
}
if(cnt >= k)
r = mid;
else
l = mid;
}
return OdrArr[r];
}

二分枚举值查询块

但是就分块我花了各种姿势都过不了例题,直接TLE。哭唧唧。


第二种方法就是参考归并排序的姿势,用一棵线段树来维护归并排序的过程。线段树的每一个叶子结点为一个数,然后父结点就是两个儿子结点归并排序。这个时候线段树的每一个结点代表的就是一个区域,这种线段树也叫区域树(Range Tree)。

在查询区间第k大的时候也需要和分块一样二分枚举N值,但是在找不大于N值个数的时候可以直接在线段树的中相应的结点上二分查找。这种算法的主要优势都是用线段树来模拟归并排序的过程,那么线段树结点上代表的那一段区域内的数一定是有序的,二分查找直接上。

具体过程是:

  • 如果区间和当前结点完全没有交集,直接返回0;
  • 如果当前结点完全包含在区间内直接二分查找不小于N值的个数;
  • 如果当前结点部分包含在区间中那就递归到子结点中去。
 vector <int> Tree[maxn];
void build_tree(int root,int l,int r) {
if(l == r) {
Tree[root].push_back(num[l]);
return ;
}
int chl = root<<;
int chr = root<<|;
int mid = l + r >> ;
build_tree(chl, l, mid);
build_tree(chr, mid+, r);
Tree[root].resize(r-l+);//开线段树这个结点上区域的大小
merge(Tree[chl].begin(), Tree[chl].end(), Tree[chr].begin(), Tree[chr].end(), Tree[root].begin());//用自带的merge函数
}

建立线段树

 int Sum(int root, int ql, int qr, int l, int r, int va) {//查询值为va的数有几个
if(ql == l && qr == r) {//结点完全包含在区域内
return upper_bound(Tree[root].begin(), Tree[root].end(), va) - Tree[root].begin();
}
int chl = root<<;
int chr = root<<|;
int mid = l + r >> ;
if(qr <= mid) {
return Sum(chl, ql, qr, l, mid, va);
} else if(ql > mid) {
return Sum(chr, ql, qr, mid+, r, va);
} else {//部分包含递归下去
return Sum(chl, ql, mid, l, mid, va) + Sum(chr, mid+, qr, mid+, r, va);
}
} int query(int ql, int qr, int va) {
int l = , r = n;
while(r - l > ) {
int mid = l + r >> ;
int x = OdrArr[mid];//枚举N值
int cnt = Sum(, ql, qr, , n, x);
if(cnt >= va)//r端为闭
r = mid;
else
l = mid;
}
return OdrArr[r];
}

查询树中寻找

用这种方法就可以过例题了,只不过跑的比较慢,6266ms。


上面说的两种方法其实思想都是很简单的,就是想办法排序,然后二分查找。因为只有二分查找的时候能够减少一个量级(从O(n) 到 O(logn))的复杂度。下面说的第三种方法就和前两种有一些不一样。第三种方法就是可持久化线段树,先不说可持久化线段树,就说线段树。如果线段树需要修改,但是要保留每一次修改之后线段树的模样,不能覆盖掉,咋办?创立多棵线段树,每一棵线段树表示一个时刻该线段树的状态。这样我们在解决区间第K大问题的时候就可以这样。我们把每一个数按时刻插入到线段树中,例如第一个数就在第一刻插入线段树中,第二个数就先将第一棵线段树复制下来,然后再插入,第三个数就将第二棵线段树复制下来,然后插入第三个数。

这样如果查找l到r区间内的某个数,那么第r棵线段树比第l-1棵线段树多出来的数就是区间l到r内的数。我们在将数字插入线段树的时候就可以按照大小顺序插入,并且维护线段树每个结点上数的个数。在找第K大的时候就是r这棵线段树从左往右开始数比l-1这个线段树多出的第K个数,不懂得看后面的例子。例如第l-1棵线段树的叶子节点(-1代表这个数不存在)是1,2,-1,-1,4,5,-1,-1。第r棵线段树的叶子节点是1,2,3,4,4,5,6,-1,那么多出来的数就是3,4,6,第2大的数就是4。维护每个节点数的个数就可以在查找的时候logn复杂度内解决。

但是算一算空间复杂度,n个数就是n棵线段树,空间爆炸啊。这个时候就是可持久化线段树的实现方式了,我们每一次修改线段树一个叶子节点上的值对于整棵树来说需要修改多少个节点,logn个,这么一算其实需要修改的节点并不多,那就可以在建立下一个线段树的时候需要修改的节点我们开辟新的空间来储存,没有改变的节点共用就行了,反正都没变还分什么你的我的。

为了能够更好的理解可持久化线段树为什么能够解决区间第k大的问题,以上的描述是用n棵不同的线段树去记录,但是可持久化线段树表示的是同一个线段树在不同的时间节点的不同形态,所以不能看成线段树的加减。r树比l-1树多出的数为啥是l到r区间的数,因为r节点比l-1节点多出的数是在l到r时刻变化的数,变化的肯定是新插入的数。线段树的加减也没法简单的用代码实现啊。

 void build_tree(int &root, int l, int r) {//开始是一棵空树
root = ++cnt;
node[root].sum = ;
if(l == r) {
return ;
}
int mid = l + r >> ;
build_tree(node[root].l, l, mid);
build_tree(node[root].r, mid+, r);
}

可持久化线段树建树

 void insert(int &root, int pre, int pos, int l, int r) {
root = ++cnt;
node[root] = node[pre];
if(l == r) {
node[root].sum++;
return ;
}
int mid = (l + r) >> ;
if(pos <= mid)
insert(node[root].l, node[pre].l, pos, l, mid);
else
insert(node[root].r, node[pre].r, pos, mid+, r);
updata(root);
} void insert() {
for(int i=;i<=n;i++) {
int pos = (int)(lower_bound(ve.begin(), ve.end(), num[i]) - ve.begin()) + ;//离散化
insert(rt[Time], rt[Time-], pos, , n);//rt存储每一个根结点的编号
Time++;
}
}

按顺序插入

int query(int i, int j, int k, int l, int r) {//在区间i,j中找第k大
if(l == r) {
return ve[l-];
}
int mid = l + r >> ;
int Sum = node[node[j].l].sum - node[node[i].l].sum;
if(Sum >= k)
return query(node[i].l, node[j].l, k, l, mid);
else
return query(node[i].r, node[j].r, k-Sum, mid+, r);//进入右节点的时候要将左边已经找到的删除
}

查找

可持久化线段树过2104题还是过得很快的,大概跑了1688ms。


第四种方法是利用划分树,说实话我也是第一次使用划分树,划分树较为复杂,写的时候也不容易扯清楚。自己搞了半天搞过了,然而我也有点好奇为啥专门有一个划分树用来解决第K大的问题,除此之外没发现什么地方还需要使用划分树,后来在网上发现有别人写文章把划分树说得十分清楚,这就不多赘述了。需要了解划分树的传送:https://www.cnblogs.com/hchlqlz-oj-mrj/p/5744308.html以及https://blog.csdn.net/luomingjun12315/article/details/51253205

AC代码:

 #include <stdio.h>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn = 1e5+; int num[][maxn], va[][maxn], odr[maxn];
int n, m; void build_tree(int row, int l, int r) {
if(l == r)
return ;
int mid = l + r >> ;
int cnt_l = l, cnt_r = mid + ;
int sameMid = ;
for(int i=l;i<=r;i++) {
if (odr[i] == odr[mid])
sameMid++;
else if (odr[i] > odr[mid])
break;
}
for(int i=l;i<=r;i++) {
if(i == l)
num[row][i] = ;
else
num[row][i] = num[row][i-];
if(va[row][i] < odr[mid] || (sameMid > && va[row][i] == odr[mid])) {
va[row+][cnt_l++] = va[row][i];
num[row][i]++;
if(va[row][i] == odr[mid])
sameMid--;
} else {
va[row+][cnt_r++] = va[row][i];
}
}
build_tree(row+, l, mid);
build_tree(row+, mid+, r);
} void init() {
scanf("%d%d",&n,&m);
for(int i=;i<=n;i++) {
scanf("%d", &odr[i]);
va[][i] = odr[i];
}
sort(odr+, odr++n);
build_tree(, , n);
} int query(int row, int ql, int qr, int l, int r, int k) {
int mid = l + r >> ;
if(l == r) {
return va[row][l];
}
int fromLeft = ;//来自ql之前的数进入左孩子有几个
if(l != ql)
fromLeft = num[row][ql-];
int cnt = num[row][qr]-fromLeft;//qr之前的进入左孩子个数减去(ql-1)之前的个数就是ql-qr的个数
if(cnt >= k) {//如果ql到qr进入左孩子的个数大于等于k,那么k就在左孩子里面
return query(row+, l+fromLeft, l+num[row][qr]-, l, mid, k);//进入左孩子中,从大于ql之前进入左孩子的开始找第k个
} else {//否则进入右儿子寻找
int pos_r = mid++(ql - l - fromLeft);//ql-l-formLeft是ql之前进入右儿子的个数
return query(row+, pos_r, pos_r+qr-ql+-cnt-, mid+, r, k-cnt);//从pos_r找qr-ql+1个但是要减去在左儿子找到的cnt个
}
} int main() {
init();
while(m--) {
int ql, qr , k;
scanf("%d%d%d",&ql, &qr, &k);
printf("%d\n", query(, ql, qr, , n, k));
}
return ;
}

划分树确实溜啊,速度比前面的几种方法跑起来都快,空间占用也要小。

解决区间第K大的问题的各种方法的更多相关文章

  1. POJ 2104 HDU 2665 主席树 解决区间第K大

    两道题都是区间第K大询问,数据规模基本相同. 解决这种问题, 可以采用平方划分(块状表)复杂度也可以接受,但是实际表现比主席树差得多. 这里大致讲一下我对主席树的理解. 首先,如果对于某个区间[L,R ...

  2. 线段树专题2-(加强版线段树-可持续化线段树)主席树 orz! ------用于解决区间第k大的问题----xdoj-1216

    poj-2104(区间第K大问题) #include <iostream> #include <algorithm> #include <cstdio> #incl ...

  3. 可持久化线段树(主席树)(图文并茂详解)【poj2104】【区间第k大】

    [pixiv] https://www.pixiv.net/member_illust.php?mode=medium&illust_id=63740442 向大(hei)佬(e)实力学(di ...

  4. POJ 2104 静态找区间第k大

    静态区间第k大的问题,往往可以利用主席树来解决 这是主席树的第一道题 主席树大概可以理解为在n个节点上都建立一棵线段树,但是想想会超出内存 每一个节点保存的线段树都记录当前整段前缀区间的信息 但是因为 ...

  5. POJ2761---Feed the dogs (Treap求区间第k大)

    题意 就是求区间第k大,区间 不互相包含. 尝试用treap解决一下 第k大的问题. #include <set> #include <map> #include <cm ...

  6. poj2104&&poj2761 (主席树&&划分树)主席树静态区间第k大模板

    K-th Number Time Limit: 20000MS   Memory Limit: 65536K Total Submissions: 43315   Accepted: 14296 Ca ...

  7. 51nod 1175 区间第k大 整体二分

    题意: 一个长度为N的整数序列,编号0 - N - 1.进行Q次查询,查询编号i至j的所有数中,第K大的数是多少. 分析: 仅仅就是一道整体二分的入门题而已,没听说过整体二分? 其实就是一个分治的函数 ...

  8. 【POJ】【2104】区间第K大

    可持久化线段树 可持久化线段树是一种神奇的数据结构,它跟我们原来常用的线段树不同,它每次更新是不更改原来数据的,而是新开节点,维护它的历史版本,实现“可持久化”.(当然视情况也会有需要修改的时候) 可 ...

  9. 【ZOJ2112】【整体二分+树状数组】带修改区间第k大

    The Company Dynamic Rankings has developed a new kind of computer that is no longer satisfied with t ...

随机推荐

  1. Google Colab 免费的谷歌GPU for deep learning

    Who wants to use a free GPU for deep learning?Google Colab is a free cloud service and now it suppor ...

  2. msvcr100.dll问题描述及修复方式

    出现问题的大部分原因是因该文件被木马病毒破坏导致系统找不到此文件,出现错误提示框,想要解决此问题只需找到专业的DLL文件下载网站,下载该文件,复制到相应目录.即可解决.msvcr100.dll为Vis ...

  3. January 31 2017 Week 5 Tuesday

    Better to get hurt by the truth than comforted with a lie. 被真相伤害好过被谎言安慰. For ourselves, we need to b ...

  4. 用jquery实现的简单数据双向绑定

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. SOA架构

    基于服务的SOA架构_后续篇   今天是元宵节,首先祝各位广大博友在接下来的光阴中技术更上一层,事事如意!(没能在元宵节发布,今天就补上吧) 昨天简单介绍了一下本人在近期开发过的一个电商购物平台的架构 ...

  6. Django admin页面 显示中文问题

    http://127.0.0.1:8000/admin/ 该页中实现中文显示. 1.  页面显示的数据表表名 实现中文显示. models.Model类的内部类Meta,有两个特殊的选项:verbos ...

  7. MySQL错误问题

    启动Tomcat的时候报错:no suitable driver,MySql更新使用com.mysql.cj.jdbc.Driver,废弃老的com.mysql.jdbc.Driver驱动,需要将D: ...

  8. js获取给定时间的下一天

    //获取给定时间的下一天 yyyy-MM-dd格式 function getNextDay(d){ d = new Date(d); d = +d + 1000*60*60*24; d = new D ...

  9. Paxos一致性算法(三)

    一.概述: Google Chubby的作者说过这个世界只有一种一致性算法,那就Paxos算法,其他的都是残次品. 二.Paxos算法: 一种基于消息传递的高度容错性的一致性算法. Paxos:少数服 ...

  10. 463. Island Perimeter (5月29日)

    解答 class Solution { public: int islandPerimeter(vector<vector<int>>& grid) { int num ...