【基础操作】整体二分概述

整体二分是一个常数小的离线做法。

这篇讲 $CDQ$ 的文章里提到了其一个分支——整体二分。

整体二分的适用性

有一些问题,在有多组操作(一开始赋初值也算操作)但只有一组询问的情况下(当然这组询问正常情况下就放在最后的,不然它后面的操作是摆着玩的),可以二分这个询问的答案。

二分的时间复杂度是 $O(log(n))$,验证一个答案是大了还是小了的时间复杂度是 $O(n)$(或者是这个级别的,差不多不到 $O(n^2)$ 就行),总时间复杂度是 $O(n\times log(n))$(或者是这个级别的)。

对于很多组询问的情况,设询问组数为 $Q$,如果对每组都二分,时间复杂度是 $O(Q\times n\times log(n))$ 级别的,对于祖传数据(全 $100000$)直接升天。

然后我们发现,有一些问题的修改对询问的影响非常有特性,支持高效维护和查询。这时候就可以把所有操作(包括修改和查询)放到一块二分。

总之,整体二分只能在满足以下条件的情况下使用:

  - 单组询问可以二分

  - 存在高效的数据结构维护修改对询问的影响(了解整体二分的应该知道,像区间修改这种的就不存在)

  - 题目可以离线做(废话)

  ……

主席树(模板)

题意

询问静态区间第 $k$ 小。$n,Q\le 200000$。

题解

其实也是个整体二分模板题。

如果只有一组询问,我们可以二分答案,判断的话就扫一遍询问区间,看有几个数比这个答案小,就可以确定这个答案是区间第几小了。

对于多组询问的话,我们可以把所有操作扔到一起二分。

具体是什么意思呢?

举个例子,给出一个数 $x$,在二分答案 $mid$ 时,它只会对所有大于 $mid$ 的答案造成影响。也就是说它跟询问一样,可以被二分。

下面说说具体操作:

我们按输入的顺序,依次模拟当前操作区间的操作。(在模拟当前区间的所有操作之前,把之前二分时记录的信息都清空,即假装之前啥操作也没做过)

如果这个操作是赋值操作(在这题就是一开始给数组赋值),

  如果赋的数小于等于 $mid$,设这个数为 $x$,把它在树状数组/线段树的第 $x$ 位 $+1$,并把这个操作扔到左递归区间(即 $[l,mid]$ 答案区间);

  如果赋的数大于 $mid$,直接把这个操作扔到右递归区间(即 $(mid,r]$ 区间)。

如果这个操作是询问操作,

  先用树状数组/线段树对其询问区间求和(这就是之前为什么说在问题比较简单时 才适合整体二分,你得确保能再用个数据结构维护),也就是查其询问区间当前有多少个小于等于 $mid$ 的数,这里设有 $x$ 个。

  如果 $x$ 小于该查询的 $k$(即这个查询问的是区间第 $k$ 小),就把该询问的 $k$ 减去 $x$(左递归区间有足够的数小于 $mid$,这个询问的答案显然在右递归区间,于是减去左区间的数的影响),并把这个操作扔到右递归区间;

  如果 $x$ 大于等于该查询的 $k$,直接把这个操作扔到左递归区间。

如果二分答案的区间 $[l,r]$ 递归到了 $l=r$,就把扔到这个递归区间的所有询问操作的答案都赋为 $l$ 或 $r$,回溯。

$Q1:$ 为什么对于赋值操作,赋的数小于等于 $mid$ 时,要把操作扔到左递归区间?

$A1:$ 因为我们在询问操作中,把要递归到右子区间的询问 减掉了小于等于 $mid$ 的数对询问的影响,也就是说这个赋值操作以后不用管递归到右子区间的询问操作了,于是扔到左子区间即可。

$Q2:$ 那赋的数大于 $mid$ 时,要把操作扔到右递归区间?

$A2:$ 因为这个数不会对询问答案在左子区间的询问造成影响,它不会参与这些询问的排名。

其它证明都比较显然了吧(其实是我没时间写)

对于这题,如果不离散化的话,只能用权值线段树;如果离散化的话,树状数组和权值线段树就都可以用了。

注意一个事情,一开始就要把所有操作的数改为离散化后的数,不要在整体二分里再对每个数套 $map$ 查询,因为整体二分的时间复杂度顶多做到 $O(n\times log^2(n))$,再套个 $map$ 的 $log$ 可能会爆炸。

 1 #include<bits/stdc++.h>
 2 #define rep(i,x,y) for(int i=x;i<=y;++i)
 3 #define dwn(i,x,y) for(int i=x;i>=y;--i)
 4 #define ll long long
 5 #define N 400001
 6 using namespace std;
 7 inline int read(){
 8     int x=0; bool f=1; char c=getchar();
 9     for(;!isdigit(c);c=getchar()) if(c==‘-‘) f=0;
10     for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^‘0‘);
11     if(f) return x;
12     return 0-x;
13 }
14 int n,m,a[N],b[N],ans[N],cnt;
15 map<int,int>mp; int lsh[N];
16 struct query{int x,l,r,k;}q[N],L[N],R[N];
17 namespace Fwk{
18     int fwk[N];
19     inline int lowbit(int x){return x&(-x);}
20     inline void add(int x,int v){
21         for(;x<=n;x+=lowbit(x)) fwk[x]+=v;
22     }
23     inline int query(int x){
24         int ret=0;
25         for(;x>0;x-=lowbit(x)) ret+=fwk[x];
26         return ret;
27     }
28 };
29 void solve(int ql,int qr,int l,int r){
30     //printf("solve:%d %d %d %d\n",ql,qr,l,r);
31     if(l==r){
32         rep(i,ql,qr) if(q[i].x) ans[q[i].x]=l;
33         return;
34     }
35     int mid=(l+r)>>1,cntL=0,cntR=0,tmp;
36     rep(i,ql,qr){
37         //cout<<"faq:"<<Fwk::query(2)<<‘ ‘<<Fwk::query(0)<<endl;
38         if(!q[i].x){
39             if(q[i].k<=mid) L[++cntL]=q[i], Fwk::add(q[i].l,1);
40             else R[++cntR]=q[i];
41             //printf("1:%d %d %d\n",q[i].l,q[i].k,mid);
42         }
43         else{
44             tmp=Fwk::query(q[i].r)-Fwk::query(q[i].l-1);
45             if(tmp<q[i].k) q[i].k-=tmp, R[++cntR]=q[i];
46             else L[++cntL]=q[i];
47             //printf("2:%d %d %d %d %d\n",q[i].x,q[i].l,q[i].r,q[i].k,tmp);
48         }
49     }
50     rep(i,1,cntL){
51         q[ql-1+i]=L[i];
52         if(!L[i].x) Fwk::add(L[i].l,-1);
53     }
54     rep(i,1,cntR) q[ql-1+cntL+i]=R[i];
55     solve(ql,ql-1+cntL,l,mid), solve(ql-1+cntL+1,qr,mid+1,r);
56 }
57 int main(){
58     //freopen("luogu3834.in","r",stdin);
59     //freopen("luogu3834.out","w",stdout);
60     n=read(),m=read();
61     rep(i,1,n) b[i]=read(), q[i]=(query){0,i,i,b[i]};
62     sort(b+1,b+n+1);
63     mp[b[1]]=++cnt, lsh[cnt]=b[1];
64     rep(i,2,n) if(b[i]!=b[i-1]) mp[b[i]]=++cnt, lsh[cnt]=b[i];
65     int l,r,k;
66     rep(i,1,m) l=read(),r=read(),k=read(), q[n+i]=(query){i,l,r,k};
67     rep(i,1,n) q[i].k=mp[q[i].k];
68     solve(1,n+m,1,cnt);
69     rep(i,1,m) printf("%d\n",lsh[ans[i]]);
70     return 0;
71 }

Dynamic Rankings(zoj2112)

题意

询问动态区间第 $k$ 小。

题解

我们发现,既然可以把所有操作都混到一块,为什么不能在询问中间插入一些修改操作呢?

修改操作可以看成减一个数,再加一个数。因此把每个修改操作拆成减去原来的数、加上新的数即可。

注意,对于减去的数,要按它的绝对值进行整体二分的递归判断。因为之前肯定加过这个数,之前那个数对 $mid$ 有影响(给线段树对应数位加 $1$ 什么的),这个数就对 $mid$ 有影响(比如给线段树对应数位 $-1$,去掉之前那个数的影响),也就是说这个数实际上就是消去之前加上的那个数的影响,之前那个数在哪,这个数就得在哪。

后记

其实大部分整体二分的题目都完全可以用纯数据结构(比如主席树)代替,写整体二分只是为了减小常数。

了解一下,会写就好了。

原文地址:https://www.cnblogs.com/scx2015noip-as-php/p/whole_dichotomy.html

时间: 2024-08-30 15:54:00

【基础操作】整体二分概述的相关文章

网络基础篇----计算机网络基本概述(1)

享受生活  热爱挑战                                                                刘明远分享    一   计算机网络基本概述(1) 每章一段话: 不要让自己闲下来,给自己找些事情做.哪怕是看看书. 正文   (提示:本章内容比较无聊,最好当看故事一样来看,不必记下只需了解,内容基础) 1什么是计算机网络 号称新的"电力火花"是以计算机.通信.信息技术为支撑的计算机网络技术. 计算机网络将两台或多台计算机通过电缆或网络设

CDQ分治与整体二分总结

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

【学时总结】◆学时&#183;IX◆ 整体二分

◆学时·IX◆ 整体二分 至于我怎么了解到这个算法的……只是因为发现一道题,明显的二分查找,但是时间会爆炸,被逼无奈搜题解……然后就发现了一些东西QwQ ◇ 算法概述 整体二分大概是把BFS与二分查找完美结合了. 它针对一种可以用二分查找(直接查找答案),但是询问很多,对于每一个询问都做二分查找会超时的问题. 由于多次询问,那么问题的答案很可能在二分的区间中比较分散,那么我们可以用BFS枚举出二分将会产生的所有区间的情况……类似于线段树: 最初区间为 [1,m] 然后从该区间再拓展出 [1,mi

整体二分学习

这大概是我目前学过的最难理解的知识点了吧( 概述 整体二分,意味着同时二分一切 这个算法适用于静态和动态区间第\(k\)大,以及一些区间询问问题.那么根据通常的思路,让我们先来介绍一下暴力,再来分析二者的区别. 静态区间第k大 让我们暴力地二分答案来做,应该怎么做呢?既然我们要求区间第\(k\)大,那么区间中就应该有\(k-1\)个数比答案大才对.所以我们二分答案\(mid\),看看序列里有多少个比它小的数,然后缩小值域. 想象一下,如果对于每一个询问操作我们都这样做,会\(T\)成什么样子.对

CDQ分治与整体二分小结

前言 这是一波强行总结. 下面是一波瞎比比. 这几天做了几道CDQ/整体二分,感觉自己做题速度好慢啊. 很多很显然的东西都看不出来 分治分不出来 打不出来 调不对 上午下午晚上的效率完全不一样啊. 完蛋.jpg 绝望.jpg. 关于CDQ分治 CDQ分治,求的是三维偏序问题都知道的. 求法呢,就是在分治外面先把一维变成有序 然后分治下去,左边(l,mid)关于右边(mid+1,r)就不存在某一维的逆序了,所以只有两维偏序了. 这个时候来一波"树状数组求逆序对"的操作搞一下二维偏序 就可

BZOJ 3110: [Zjoi2013]K大数查询 [整体二分]

有N个位置,M个操作.操作有两种,每次操作如果是1 a b c的形式表示在第a个位置到第b个位置,每个位置加入一个数c如果是2 a b c形式,表示询问从第a个位置到第b个位置,第C大的数是多少. N,M<=50000,N,M<=50000a<=b<=N1操作中abs(c)<=N2操作中c<=Maxlongint 之前用树套树抄过一次...然而我并不适合写那玩意儿... 加上时间序的整体二分 普通的整体二分先处理了所有$[l,mid]$的影响因子在计算询问的答案来分组

[整体二分]【学习笔记】【更新中】

先小结一下吧 主要为个人理解 整体二分 理解 $zyz:$整体二分是在权值上进行$CDQ$分治 我觉得更像是说$:$整体二分是在答案上进行$CDQ$分治 整体二分是二分答案在数据结构题上的扩展 因为数据结构题二分的答案通常是第几个操作之后,需要进行一些操作(预处理)之后才能判断,所以每次询问二分还不如从前往后暴力找 整体二分可以解决这样的问题 核心就是维护一个$cur$数组保存当前的影响,分治到$[l,r]$时只需要计算$[l,mid]$的影响再与$cur$里的合并就好了 这样分治里的操作就只与

POJ 2104:K-th Number(整体二分)

http://poj.org/problem?id=2104 题意:给出n个数和m个询问求区间第K小. 思路:以前用主席树做过,这次学整体二分来做.整体二分在yr大佬的指点下,终于大概懂了点了.对于二分能够解决的询问,如果有多个,那么如果支持离线处理的话,那么就可以使用整体二分了. 在这题二分可行的答案,根据这个答案,把询问操作丢在左右两个队列里面分别递归继续按这样处理.注释里写的很详细. 1 #include <iostream> 2 #include <cstdlib> 3 #

整体二分初步

部分内容引自myt论文:树状数组延伸和离线优化(CDQ.整体二分和莫队) 大致思路 1.确定答案范围[L,R],mid=L+R>>1;2.算出答案在[L,mid]内的操作对答案在[mid+1,R]内的操作的贡献;3.将答案在[L,mid]内的操作放入队列Q1,solve(Q1,L,mid)将答案在[mid+1,R]内的操作放入Q2,solve(Q2,mid+1,R) 伪代码 divide(hd,tl,l,r){ if(hd>tl) exit if(l==r){ for(i->hd