整体二分初探 两类区间第K大问题 poj2104 & hdu5412

看到好多讲解都把整体二分和$CDQ$分治放到一起讲 不过自己目前还没学会$CDQ$分治 就单独谈谈整体二分好了

先推荐一下$XHR$的 <浅谈数据结构题的几个非经典解法> 整体二分在当中有较为详细的讲解

先来说一下静态第$K$小的整体二分解法 $(POJ2104)$

题目链接:http://poj.org/problem?id=2104

所谓整体二分 就是利用所有的询问相互独立而把它们$($此题没有修改操作$)$通过二分把它们分治进行处理

不妨先来考虑下一个简单易懂的$O(NlogS)$的排序算法$(S$为数值范围$)$

这个方法是自己在思考整体二分的时候$yy$的 虽然在实际应用上没什么意义 但是有助于理解整体二分的分治过程

我们假设当前处理的区间里最小值不小于$L$ 最大值不大于$R$ 令$MID = (L + R) / 2$

然后把当前区间扫描一遍 如果一个数不大于MID就放到左子区间 否则放到右子区间

如此下去 直到区间内只剩一个数或者$L$ 与 $R$相等 排序就完成了

现在回到静态区间第$K$小问题 和刚才那个排序算法类似 我们先二分一个答案$MID$

如果区间内小于等于$MID$的数的个数$($记为$CNT)$不超过$K$ 那么最终答案显然也是不超过$MID$的

这类询问我们把它们划分到左子区间

而对于$CNT$大于$K$的 我们则把它们划分到右子区间 并且把$K$减去$CNT$

换句话说就是把小于等于$MID$的数的贡献全部算上后之后就不用考虑了

可以发现这样划分的层数是$logS$ 而每一层的询问个数是$Q$个 再加上算贡献时用到的$BIT$ 所以复杂度是O(QlogNlogS)

以下是$poj2104$参考代码

 1 //#include <bits/stdc++.h>
 2 #include <cstdio>
 3 #include <cstring>
 4 #include <cmath>
 5 #include <algorithm>
 6 using namespace std;
 7 const int N = 1e5 + 10, Q = 5e3 + 10, lim = 1e9;
 8 struct point
 9 {
10     int x, num;
11 }a[N];
12 struct query
13 {
14     int x, y, k, cnt, num;
15 }q[Q], b[Q];
16 int sum[N], ans[Q];
17 int n, m;
18 bool cmp(const point &aa, const point &bb)
19 {
20     return aa.x < bb.x;
21 }
22 void calc(int ll, int rr, int rawL, int mid)
23 {
24     int L = 1, R = n + 1, MID;
25     while(L < R)
26     {
27         MID = (L + R) >> 1;
28         if(a[MID].x >= rawL)
29             R = MID;
30         else
31             L = MID + 1;
32     }
33     for(int i = R; i <= n && a[i].x <= mid; ++i)
34         for(int j = a[i].num; j <= n; j += (j & -j))
35             ++sum[j];
36     for(int i = ll; i <= rr; ++i)
37     {
38         q[i].cnt = 0;
39         for(int j = q[i].y; j; j -= (j & -j))
40             q[i].cnt += sum[j];
41         for(int j = q[i].x - 1; j; j -= (j & -j))
42             q[i].cnt -= sum[j];
43     }
44     for(int i = R; i <= n && a[i].x <= mid; ++i)
45         for(int j = a[i].num; j <= n; j += (j & -j))
46             --sum[j];
47 }
48 void divide(int ll, int rr, int rawL, int rawR)
49 {
50     if(rawL == rawR)
51     {
52         for(int i = ll; i <= rr; ++i)
53             ans[q[i].num] = rawR;
54         return;
55     }
56     int mid = rawL + ((rawR - rawL) >> 1);
57     calc(ll, rr, rawL, mid);
58     int now1 = ll, now2 = rr;
59     for(int i = ll; i <= rr; ++i)
60     {
61         if(q[i].cnt >= q[i].k)
62             b[now1++] = q[i];
63         else
64         {
65             q[i].k -= q[i].cnt;
66             b[now2--] = q[i];
67         }
68     }
69     for(int i = ll; i <= rr; ++i)
70         q[i] = b[i];
71     if(now1 != ll)
72         divide(ll, now1 - 1, rawL, mid);
73     if(now2 != rr)
74         divide(now2 + 1, rr, mid + 1, rawR);
75 }
76 int main()
77 {
78     scanf("%d%d", &n, &m);
79     for(int i = 1; i <= n; ++i)
80     {
81         scanf("%d", &a[i].x);
82         a[i].num = i;
83     }
84     a[n + 1].x = 2e9;
85     sort(a + 1, a + 1 + n, cmp);
86     for(int i = 1; i <= m; ++i)
87     {
88         scanf("%d%d%d", &q[i].x, &q[i].y, &q[i].k);
89         q[i].num = i;
90     }
91     divide(1, m, -lim, lim);
92     for(int i = 1; i <= m; ++i)
93         printf("%d\n", ans[i]);
94     return 0;
95 }

然后就是整体二分真正突出存在意义的问题 动态区间第K大了$(hdu5412)$

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5412

动态区间第$K$大就是额外多了修改操作 修改不仅对询问有影响 对修改也有影响 因此直观上看起来要麻烦很多

我们知道 区间询问之所以能排序后分治 主要是所有的询问相互独立

对于修改操作 如果我们希望它们相互独立该怎么做呢 那就算贡献好了

实际上  每次把一个位置的数修改为另一个数相当于使这个位置删除一个原来的数 并插入一个新的数 而初始的数则相当于仅有插入操作

这样只要保证同一区间内的查询和删除或插入操作的相对顺序不变 即相对时间顺序不变 最后算贡献结果也是一样的

并且由于对大于$MID$的数的删除或插入操作 即便时间顺序是在划分到左子区间的询问之前 也不会造成影响 因此它们可以划分到右子区间

而对小于$MID$的数的删除或插入操作 对划分到右子区间的询问的影响可以直接在划分前计入贡献之后不再考虑 因此它们可以划分到左子区间

这样按照类似静态区间第$K$大问题计算复杂度会发现是$O((Q+N)logNlogS)$

以下是$hdu5412$的参考代码

  1 #include <bits/stdc++.h>
  2 using namespace std;
  3 const int N = 1e5 + 10, Q = 1e5 + 10, A = N + Q * 2, lim = 1e9;
  4 struct operate
  5 {
  6     int x, y, k, cnt, num;
  7     operate(){}
  8     operate(int _x, int _y, int _k, int _cnt, int _num)
  9     {
 10         x = _x;
 11         y = _y;
 12         k = _k;
 13         cnt = _cnt;
 14         num = _num;
 15     }
 16 }a[A], q1[A], q2[A];
 17 int raw[N], ans[Q], sum[N];
 18 int n, m, len, l1, l2;
 19 void update(int x, int y)
 20 {
 21     for(int i = x; i <= n; i += (i & -i))
 22         sum[i] += y;
 23 }
 24 int query(int x)
 25 {
 26     int re = 0;
 27     for(int i = x; i; i -= (i & -i))
 28         re += sum[i];
 29     return re;
 30 }
 31 void calc(int ll, int rr, int rawl, int mid)
 32 {
 33     for(int i = ll; i <= rr; ++i)
 34     {
 35         if(a[i].k)
 36             a[i].cnt = query(a[i].y) - query(a[i].x - 1);
 37         else if(a[i].y <= mid)
 38             update(a[i].x, a[i].cnt);
 39     }
 40     for(int i = ll; i <= rr; ++i)
 41         if(!a[i].k && a[i].y <= mid)
 42             update(a[i].x, -a[i].cnt);
 43     l1 = l2 = 0;
 44     for(int i = ll; i <= rr; ++i)
 45         if(a[i].k)
 46         {
 47             if(a[i].k <= a[i].cnt)
 48                 q1[++l1] = a[i];
 49             else
 50             {
 51                 a[i].k -= a[i].cnt;
 52                 q2[++l2] = a[i];
 53             }
 54         }
 55         else
 56         {
 57             if(a[i].y <= mid)
 58                 q1[++l1] = a[i];
 59             else
 60                 q2[++l2] = a[i];
 61         }
 62     int now = ll;
 63     for(int i = 1; i <= l1; ++i)
 64         a[now++] = q1[i];
 65     for(int i = 1; i <= l2; ++i)
 66         a[now++] = q2[i];
 67 }
 68 void divide(int ll, int rr, int rawl, int rawr)
 69 {
 70     if(rawl == rawr)
 71     {
 72         for(int i = ll; i <= rr; ++i)
 73             if(a[i].k)
 74                 ans[a[i].num] = rawl;
 75         return;
 76     }
 77     int mid = (rawl + rawr) >> 1;
 78     calc(ll, rr, rawl, mid);
 79     int tmp = l1;
 80     if(tmp)
 81         divide(ll, ll + tmp - 1, rawl, mid);
 82     if(ll + tmp <= rr)
 83         divide(ll + tmp, rr, mid + 1, rawr);
 84 }
 85 int main()
 86 {
 87     while(scanf("%d", &n) != EOF)
 88     {
 89         len = 0;
 90         for(int i = 1; i <= n; ++i)
 91         {
 92             scanf("%d", &raw[i]);
 93             a[++len] = operate(i, raw[i], 0, 1, 0);
 94          }
 95          scanf("%d", &m);
 96          int op, x, y, z;
 97          for(int i = 1; i <= m; ++i)
 98          {
 99             scanf("%d", &op);
100             if(op & 1)
101             {
102                 scanf("%d%d", &x, &y);
103                 a[++len] = operate(x, raw[x], 0, -1, 0);
104                 a[++len] = operate(x, y, 0, 1, 0);
105                 raw[x] = y;
106                 ans[i] = 0;
107             }
108             else
109             {
110                 scanf("%d%d%d", &x, &y, &z);
111                 a[++len] = operate(x, y, z, 0, i);
112             }
113         }
114         divide(1, len, 1, lim);
115         for(int i = 1; i <= m; ++i)
116             if(ans[i])
117                 printf("%d\n", ans[i]);
118     }
119     return 0;
120 }
时间: 2024-08-08 05:17:35

整体二分初探 两类区间第K大问题 poj2104 & hdu5412的相关文章

【BZOJ3110】【整体二分+树状数组区间修改/线段树】K大数查询

Description 有N个位置,M个操作.操作有两种,每次操作如果是1 a b c的形式表示在第a个位置到第b个位置,每个位置加入一个数c 如果是2 a b c形式,表示询问从第a个位置到第b个位置,第C大的数是多少. Input 第一行N,M 接下来M行,每行形如1 a b c或2 a b c Output 输出每个询问的结果 Sample Input 2 5 1 1 2 1 1 1 2 2 2 1 1 2 2 1 1 1 2 1 2 3 Sample Output 1 2 1 HINT

【bzoj3110】[Zjoi2013]K大数查询 整体二分+树状数组区间修改

题目描述 有N个位置,M个操作.操作有两种,每次操作如果是1 a b c的形式表示在第a个位置到第b个位置,每个位置加入一个数c.如果是2 a b c形式,表示询问从第a个位置到第b个位置,第C大的数是多少. 输入 第一行N,M接下来M行,每行形如1 a b c或2 a b c 输出 输出每个询问的结果 样例输入 2 5 1 1 2 1 1 1 2 2 2 1 1 2 2 1 1 1 2 1 2 3 样例输出 1 2 1 题解 整体二分+树状数组区间修改 当年naive的树套树题解 前两天由于要

[NBUT 1458 Teemo]区间第k大问题,划分树

裸的区间第k大问题,划分树搞起. #pragma comment(linker, "/STACK:10240000") #include <map> #include <set> #include <cmath> #include <ctime> #include <deque> #include <queue> #include <stack> #include <vector> #inc

区间第k大问题 权值线段树

先说下权值线段树的概念吧 待整理 附上吴迎学长离散化的标准代码 #include<iostream> #include<algorithm> #include<cstdio> using namespace std; int a[100],b[100],c[100],n; int solve()//离散化 { for(int i=0;i<n;i++) b[i]=a[i]; sort(b,b+n); int m=unique(b,b+n)-b;//去重 for(in

BZOJ_3110_[Zjoi2013]K大数查询_整体二分+树状数组

Description 有N个位置,M个操作.操作有两种,每次操作如果是1 a b c的形式表示在第a个位置到第b个位置,每个位置加入一个数c 如果是2 a b c形式,表示询问从第a个位置到第b个位置,第C大的数是多少. Input 第一行N,M 接下来M行,每行形如1 a b c或2 a b c Output 输出每个询问的结果 Sample Input 2 5 1 1 2 1 1 1 2 2 2 1 1 2 2 1 1 1 2 1 2 3 Sample Output 1 2 1 solve

CQH分治与整体二分

CDH分治,核心思想就是对操作进行二分.感觉和我以前对操作分块的思想很像啊,fhb分块 ……(⊙o⊙)… 日常懒得写模板的题解,转载一篇(本家) -----------------------------------------------------------分割线---------------------------------------------------------------------- 在线/离线:首要考虑 在线算法: 可以以序列化的方式一个一个的处理输入,不必事先知道所有

关于求解区间第k大的在线和离线做法

最近做了一道关于整体二分的题. 很开心地涉足了关于求区间第k大问题. 问题:给定序列,若干询问,求区间第k小. 第k大类推. 离线算法: 整体二分. 将所有询问离线下来,挂在区间右端点先. 对所有询问二分答案mid. 那么序列上的数就可以划分为两类了,一类小于等于mid,一类大于mid. 用树状数组维护一下. 接着处理询问,对于每个当前的询问判断是否合法,然后将询问划分为两边,一边答案小于等于mid的,一边是大于mid的. 看一下复杂度,每次答案的范围除以2,每次二分分别扫描一次序列和询问,总复

CDQ分治与整体二分总结

Cdq分治和整体二分是两个很奇妙的东西.他们都是通过离线的思想来进行优化,从而更快的求出解. 整体二分通俗的讲就是二分答案,但是它了不起的地方是一下子把所有的答案都二分出来了,从而可以一下子得出所有查询. CDQ分治通俗的讲就是二分查询.通常的做法是把所有的查询分成两半,然后通过递归先计算出左边一半的所有的查询,然后通过这些已知的左半边的值来更新右半边的值.这里,最最重要的思想是通过左半边来更新右半边.更具体一点,就是用左半边的修改来更新右半边的查询. 重要的事情说话三遍: CDQ分治就是通过左

ZOJ 1112 Dynamic Rankings【动态区间第K大,整体二分】

题目链接: http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=1112 题意: 求动态区间第K大. 分析: 把修改操作看成删除与增加,对所有操作进行整体二分. 代码: #include<cstdio> #include<iostream> #include<cstring> using namespace std; #define pr(x) cout << #x << &quo