整体二分初探 两类区间第K大问题 poj2104 & hdu5412
Posted sagitta
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了整体二分初探 两类区间第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 }
以上是关于整体二分初探 两类区间第K大问题 poj2104 & hdu5412的主要内容,如果未能解决你的问题,请参考以下文章
POJ 2104 K-th Number(区间第k大数)(平方切割,归并树,划分树)