#创作计划#整体二分与CDQ分治(一)
2025-12-01 21:30:06
发布于:广东
NOIP太难了,怎么一堆神犇,等我考上广附或者华附或者广外或者没开挂省实或者正常点的二中六中广雅执信铁一我一定不会废成这个样子
前言
整体二分和CDQ分治是与莫队并列的三大离线算法,本质都是分治,不过思想和使用范围不同,通常可以高效解决平衡树套线段树、BIT套主席树这些编码难度较大的问题。它们较少在NOIP和CSP中考察,同时难度也较大(模板题紫),但是可以多做几道紫题不过有助于增进对分治的理解。
第一部分:整体二分
整体二分是一种离线算法 (说了=没说)
可以使用整体二分解决的题目需要满足以下性质:
1.询问的答案具有可二分性
2.修改对判定答案的贡献互相独立,修改之间互不影响效果
3.修改如果对判定答案有贡献,则贡献为一确定的与判定标准无关的值
4.贡献满足交换律,结合律,具有可加性
5.题目允许使用离线算法
——许昊然《浅谈数据结构题几个非经典解法》
整体二分格式:整体二分函数中通常有4个参数,表示答案值域和所执行的操作/询问的范围。函数先计算出和的平均值,用二分思想猜测为答案,并使用数据结构(99%是BIT和线段树)检验每个询问的答案小于还是大于,小于的分到左组,大于的分到右组,分别解决,将两个组的答案值域范围缩小一半。当答案值域大小为1时()将该组所有询问的答案设为()并返回。
更细致的讲解将在例题中展现,最详细的在后文Dynamic Rankings一题的讲解中
区间第k小
在上一篇关于主席树的文章中我们用主席树实现了区间第小的查询,这里我们使用复杂度略高的整体二分尝试实现。
先从一个问题入手:
如果只有一次查询,我们要如何解决?
答案很明了:排序二分。我们把所有数从小到大进行排序(不是区间内,并且排序时记录原来的位置),然后使用二分每次猜一个位置,如何判断答案在的左边还是右边?我们使用树状数组BIT,对于所有在的数,我们在它原来的位置加一,最后计算在询问区间中有多少个一(也就是求和),就可以知道答案在左边还是右边了。如果数量小于,说明答案猜小了,需要增加使统计到的数量增大(统计到的数量其实代表在询问的区间内有多少个数小于,第小值在区间内应有个数小于它)否则,缩小。
小技巧:当答案大于时,我们将设为,将减去,这样就可以减少很多计算量。
如图所示:

时间复杂度为
那么如何求解多个询问呢?
注意到每次询问都查验了一个,那我们对所有询问查询区间值,然后得出它在左边还是右边,分成两组分治完成不就行了吗?
然后就AC了
参考代码:我没写,可以在OI-Wiki中查看。
这个代码真难懂,加点注释罢~
#include <algorithm>
#include <iostream>
using namespace std;
constexpr int N = 200020;
int n, m;
int ans[N];
// BIT begin
int t[N];
int a[N];
int sum(int p) {
int ans = 0;
while (p) {
ans += t[p];
p -= p & (-p);
}
return ans;
}
void add(int p, int x) {
while (p <= n) {
t[p] += x;
p += p & (-p);
}
}
// BIT end
int tot = 0;
struct Query {
int l, r, k, id, type; // set values to -1 when they are not used!
} q[N * 2], q1[N * 2], q2[N * 2];
void solve(int l, int r, int ql, int qr) {//编号从ql~qr的询问答案范围为l~r
if (ql > qr) return;
if (l == r) {//当l=r,答案已确定
for (int i = ql; i <= qr; i++)
if (q[i].type == 2) ans[q[i].id] = l;
return;
}
int mid = (l + r) / 2, cnt1 = 0, cnt2 = 0;//二分开始
for (int i = ql; i <= qr; i++) {
if (q[i].type == 1) {//一种写法,稍后解释,基础写法在P1527中
if (q[i].l <= mid) {
add(q[i].id, 1);
q1[++cnt1] = q[i];
} else
q2[++cnt2] = q[i];
} else {
int x = sum(q[i].r) - sum(q[i].l - 1);
if (q[i].k <= x)//分组
q1[++cnt1] = q[i];
else {
q[i].k -= x;
q2[++cnt2] = q[i];
}
}
}
// rollback changes
for (int i = 1; i <= cnt1; i++)
if (q1[i].type == 1) add(q1[i].id, -1);//很重要!清空树状数组
// move them to the main array
for (int i = 1; i <= cnt1; i++) q[i + ql - 1] = q1[i];//类似于归并排序的分组
for (int i = 1; i <= cnt2; i++) q[i + cnt1 + ql - 1] = q2[i];
solve(l, mid, ql, cnt1 + ql - 1);//分治
solve(mid + 1, r, cnt1 + ql, qr);
}
pair<int, int> b[N];
int toRaw[N];
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n >> m;
// read and discrete input data
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
b[i].first = x;
b[i].second = i;
}
sort(b + 1, b + n + 1);
int cnt = 0;
for (int i = 1; i <= n; i++) {
if (b[i].first != b[i - 1].first) cnt++;
a[b[i].second] = cnt;
toRaw[cnt] = b[i].first;
}
for (int i = 1; i <= n; i++) {
q[++tot] = {a[i], -1, -1, i, 1};
}
for (int i = 1; i <= m; i++) {
int l, r, k;
cin >> l >> r >> k;
q[++tot] = {l, r, k, i, 2};
}
solve(0, cnt + 1, 1, tot);
for (int i = 1; i <= m; i++) cout << toRaw[ans[i]] << '\n';
}
可过P3834
注意:memset单次复杂度为,会炸,所以对于每一个+1操作,我们在原来的位置-1就行了
总复杂度为,请读者自己证明
把算法讲清楚实在是太难了,请对照代码理解,初学者怎么都懂不了的请移步左神的整体二分
基本用法
首先可以静态做出我们讲过的P4137 Rmq Problem / mex,解法不细讲因为代码没写而且只是蓝题,可以在阅读完全文后自行思考或者参考题解
接下来正经讲讲题
带修改区间第小
首先考虑主席树。
这题不能用普通主席树,需要用BIT套主席树实现修改操作,本质是用树状数组思想修改权值线段树的其中一个版本,询问时用求出区间内的线段树权值和并递归询问。
此题也可用整体二分。
我们把询问和修改操作统称为操作一起二分。
这时我们要用不同写法的整体二分了。不使用排序,二分答案可能的权值(即最小值到最大值),再遍历所有数,把大小小于的插入树状数组。
我们更改一下结构体的结构,有一个标记op表示操作类型,op=1意味着查询,op=2意味着将一个数值插入树状数组。当op=1,x意为查询的,y表示查询的,k表示查询第小,id表示查询的编号,用于输出答案。当op=2,意为将第x个位置改为y并在BIT的第x位增加k。k=1表示插入一个数,k=-1表示删除一个数。
接下来把修改操作拆分为一个删除操作和一个插入操作。插入操作:在第x个位置添加一个数y,其k=1,用于查询;但是我们有时需要修改,添加一个删除操作,x和y与要删除的操作一致,这样当被删除的操作被执行时这个操作也会执行,上个操作在BIT的x位+1,这个操作又在BIT的x位-1,二者抵消。删除完了重新插入,x相等,y为修改后的值,k=1.
注意到不用初始化,可以将初始化操作全部转化为插入操作。
所有操作一起进行二分。由于y为插入操作插入后的值,如果y>,这个操作对答案没有影响(因为你的BIT只计算小于的数),分到右组(只有更大的查询才会用到这个操作,所以去右组等更大的),否则在BIT中实现这个插入/查询操作,并分到左组等更小的。
最后,如果,将该组所有查询操作答案设为并返回。
有的人可能会疑惑:你要怎么保证每个查询刚好在且仅在它前面所有操作执行完后执行呢?即,你怎么保证执行是按时间顺序的呢?答:你按时间顺序挨个加入操作,分治的分组是从左到右遍历的,并不会改变同一组内两个操作的顺序,你在遍历到修改操作时立刻修改,遍历到查询操作就立刻查询,可以保证其有序性。
虽然比较难理解,但是代码是很透彻的,做整体二分一定要多看代码,弄清每一个细节,这样就能轻松理解。
#include<bits/stdc++.h>
using namespace std;
#define int long long
int read(){
int x=0,f=1,ch=getchar_unlocked();
for(;!isdigit(ch);ch=getchar_unlocked())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar_unlocked())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
void write(int x){
if(x<0)putchar('-'),x=-x;
if(x>9)write(x/10);
putchar((x%10)|48);
}
struct order{
int x,y,k,op,id;
}q[300005],q1[300005],q2[300005];
int n,m,ans[300005],tree[100005],x,y,k,maxn,a[100005],cnt;
char c;
void add(int x,int y){
for(;x<=n;x+=x&(-x))tree[x]+=y;
}
int sum(int x){
int ans=0;
for(;x;x-=x&(-x))ans+=tree[x];
return ans;
}
void solve(int l,int r,int ql,int qr){
if(l>r||ql>qr)return;
int cnt1=0,cnt2=0,mid=l+r>>1;
if(l==r){
for(int i=ql;i<=qr;++i)
if(q[i].op==1)
ans[q[i].id]=l;
return;
}
for(int i=ql;i<=qr;++i){
if(q[i].op==1){
int t=sum(q[i].y)-sum(q[i].x-1);
if(q[i].k<=t)
q1[++cnt1]=q[i];
else q[i].k-=t,q2[++cnt2]=q[i];
}
else{
if(q[i].y<=mid)
add(q[i].x,q[i].k),q1[++cnt1]=q[i];
else q2[++cnt2]=q[i];
}
}
for(int i=1;i<=cnt1;++i)
if(q1[i].op!=1)
add(q1[i].x,-q1[i].k);
for(int i=1;i<=cnt1;++i)q[ql+i-1]=q1[i];
for(int i=1;i<=cnt2;++i)q[ql+cnt1+i-1]=q2[i];
solve(l,mid,ql,ql+cnt1-1),solve(mid+1,r,ql+cnt1,qr);
}
signed main(){
memset(ans,-1,sizeof ans);
n=read(),m=read();
for(int i=1;i<=n;++i)a[i]=read(),maxn=max(maxn,a[i]),++cnt,q[cnt]=(order){i,a[i],1,2,cnt};
for(int i=1;i<=m;++i){
while(c=getchar(),!isgraph(c));
x=read(),y=read(),maxn=max(maxn,y);
if(c=='Q')k=read(),++cnt,q[cnt]=(order){x,y,k,1,cnt};
else{
++cnt,q[cnt]=(order){x,a[x],-1,0,cnt};
a[x]=y;
++cnt,q[cnt]=(order){x,a[x],1,2,cnt};
}
}
solve(0,maxn,1,cnt);
for(int i=1;i<=cnt;++i)
if(~ans[i])
write(ans[i]),putchar('\n');
return 0;
}
注意这里把答案先初始化为-1,如果不是-1说明是有答案的,对这个答案进行输出,实际有更简便的写法,见后文。
该代码复杂度:
所用时间:最长运行测试点197ms,使用O2优化21个测试点总时长2.08s
其它解法:树套树、分块
整体二分因为用的BIT,加上没有大量递归,常数较小,是不错的选择。
二维第小
题目看这里:P1527 [国家集训队] 矩阵乘法
很明显可以用二维BIT的单点修改+区间查询完成,其实并不难,套用模板即可。注意这里使用了基础写法
#include<bits/stdc++.h>
using namespace std;
#define int long long
int read(){
int x=0,f=1,ch=getchar_unlocked();
for(;!isdigit(ch);ch=getchar_unlocked())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar_unlocked())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
void write(int x){
if(x<0)putchar('-'),x=-x;
if(x>9)write(x/10);
putchar((x%10)|48);
}
struct num{
int x,y,v;
bool operator < (const num&b)const{return v<b.v;}
}a[250005];
struct order{
int ax,ay,bx,by,k,id;
}q[60005],q1[60005],q2[60005];
int n,N,m,ans[60005],tree[505][505],x,y,k,maxn;
void add(int x,int y,int k){
for(int i=x;i<=n;i+=i&(-i))
for(int j=y;j<=n;j+=j&(-j))
tree[i][j]+=k;
}
int sum(int x,int y){
int ans=0;
for(int i=x;i;i-=i&(-i))
for(int j=y;j;j-=j&(-j))
ans+=tree[i][j];
return ans;
}
int msum(int ax,int ay,int bx,int by){return sum(bx,by)-sum(ax-1,by)-sum(bx,ay-1)+sum(ax-1,ay-1);}
void solve(int l,int r,int ql,int qr){
if(ql>qr)return;
int cnt1=0,cnt2=0,mid=l+r>>1;
if(l==r){
for(int i=ql;i<=qr;++i)
ans[q[i].id]=a[l].v;
return;
}
for(int i=l;i<=mid;++i)add(a[i].x,a[i].y,1);
for(int i=ql;i<=qr;++i){
int t=msum(q[i].ax,q[i].ay,q[i].bx,q[i].by);
if(q[i].k<=t)q1[++cnt1]=q[i];
else q[i].k-=t,q2[++cnt2]=q[i];
}
for(int i=1;i<=cnt1;++i)q[ql+i-1]=q1[i];
for(int i=1;i<=cnt2;++i)q[ql+cnt1+i-1]=q2[i];
for(int i=l;i<=mid;++i)add(a[i].x,a[i].y,-1);
solve(l,mid,ql,ql+cnt1-1),solve(mid+1,r,ql+cnt1,qr);
}
signed main(){
n=read(),m=read();
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
a[++N]={i,j,read()},maxn=max(maxn,a[N].v);
sort(a+1,a+N+1);
for(int i=1;i<=m;++i)q[i]={read(),read(),read(),read(),read(),i};
solve(1,N,1,m);
for(int i=1;i<=m;++i)write(ans[i]),putchar('\n');
return 0;
}
(排序写法,初学者较容易理解)
注意该题复杂度为,最大一个点247ms
此题其它解法:分治(,好像是吧)、权值线段树套小波树(),时间复杂度、小波矩阵()套小波矩阵,时空复杂度
线段树维护整体二分
P3332 [ZJOI2013] K大数查询
注意到不能用普通BIT进行整体二分插入了,需要一种区间插入数据的数据结构。
没错!变体BIT线段树。
其实也就是换了一个方式实现。由于是线段树,我们不需要一个一个操作清空,而是直接用基础区间增加d+区间设为d的线段树,每次操作前给整个区间(节点1)打上清空tag就行啦。
注意到分治代码略有不同,这是因为求的是第大
#include<bits/stdc++.h>
using namespace std;
#define int long long
int read(){
int x=0,f=1,ch=getchar_unlocked();
for(;!isdigit(ch);ch=getchar_unlocked())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar_unlocked())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
void write(int x){
if(x<0)putchar('-'),x=-x;
if(x>9)write(x/10);
putchar((x%10)|48);
}
const int N=50005;
int n,m,tree[N<<2],tag1[N<<2],ans[N],cnt;
bool tag2[N<<2];
struct order{
int op,l,r,k,id;
}q[N],q1[N],q2[N];
void push_up(int p){tree[p]=tree[p<<1]+tree[p<<1|1];}
void addtag1(int p,int l,int r,int d){tree[p]+=d*(r-l+1);tag1[p]+=d;}
void addtag2(int p){tree[p]=tag1[p]=0,tag2[p]=1;}
void push_down(int p,int l,int r){
if(tag2[p])addtag2(p<<1),addtag2(p<<1|1),tag2[p]=0;
if(tag1[p]){
int mid=l+r>>1;
addtag1(p<<1,l,mid,tag1[p]),addtag1(p<<1|1,mid+1,r,tag1[p]);
tag1[p]=0;
}
}
void update(int p,int l,int r,int L,int R,int d){
if(L<=l&&r<=R){addtag1(p,l,r,d);return;}
push_down(p,l,r);
int mid=l+r>>1;
if(L<=mid)update(p<<1,l,mid,L,R,d);
if(mid<R)update(p<<1|1,mid+1,r,L,R,d);
push_up(p);
}
int query(int p,int l,int r,int L,int R){
if(L<=l&&r<=R)return tree[p];
push_down(p,l,r);
int ans=0,mid=l+r>>1;
if(L<=mid)ans+=query(p<<1,l,mid,L,R);
if(mid<R)ans+=query(p<<1|1,mid+1,r,L,R);
return ans;
}
void solve(int l,int r,int ql,int qr){
if(ql>qr)return;
if(l==r){
for(int i=ql;i<=qr;++i)
if(q[i].op==2)
ans[q[i].id]=l;
return;
}
addtag2(1);
int cnt1=0,cnt2=0,mid=l+r>>1;
for(int i=ql;i<=qr;++i){
if(q[i].op==1){
if(q[i].k<=mid)q1[++cnt1]=q[i];
else update(1,1,n,q[i].l,q[i].r,1),q2[++cnt2]=q[i];
}
else{
int t=query(1,1,n,q[i].l,q[i].r);
if(t>=q[i].k)q2[++cnt2]=q[i];
else q[i].k-=t,q1[++cnt1]=q[i];
}
}
for(int i=1;i<=cnt1;++i)q[ql+i-1]=q1[i];
for(int i=1;i<=cnt2;++i)q[ql+cnt1+i-1]=q2[i];
solve(l,mid,ql,ql+cnt1-1),solve(mid+1,r,ql+cnt1,qr);
}
signed main(){
n=read(),m=read();
for(int i=1;i<=m;++i)
q[i]={read(),read(),read(),read(),0},q[i].id=q[i].op==1?0:++cnt;
solve(-n,n,1,m);
for(int i=1;i<=cnt;++i)
write(ans[i]),putchar('\n');
return 0;
}
注意tag1是增加tag2是清空
时间复杂度(因为题目有特殊约定),但是由于是大常数的线段树,最大一个点跑到了273ms。唉!
同时此题还有平衡树套线段树(应该是吧,常数挺大,一般加标记永久化才可过)、BIT套主席树(,常数较大,可用标记永久化)、分块()等解法。
下集预告
你信心满满地去洛谷搜整体二分的题,搜到了这个东西:
动态逆序对
然而,你很快发现,这道题不能用整体二分(这里指普通整体二分)
它属于另一个领域:
全部评论 15
这不好笑,当我在文章第一行看到了我的学校



昨天 来自 广东
2%%%
昨天 来自 广东
0ip正确
昨天 来自 广东
0如此强
其实都是我中考目标(
坐落在这个学校我真的要废掉了昨天 来自 广东
0
榜前十唯一学术贴,ACGO正在蒸蒸日上!
昨天 来自 上海
1ACGOer不看主席树扎堆看整体二分也是个人物
昨天 来自 广东
0嗯……主要是数据结构有点难,只能找个算法看看了,你写的很不错,但是请速速更新。
嗯。15小时前 来自 上海
0我也想啊,这周末试试看
解决这种问题的算法不好学,整体二分相对来说比较好写,网上这些资源都很难懂3小时前 来自 广东
0
可以给个
鸡精昨天 来自 广东
1细节说可以加精然后不点赞
昨天 来自 广东
0
昨天 来自 广东
0
大佬牛逼666辛苦了



2天前 来自 广西
1QvQ 你现在还会上线回我,好感动
2天前 来自 广东
0
我去,我竟然懂了
4天前 来自 广东
1%
你肯定能懂,毕竟我都能懂3天前 来自 广东
0Bro看懂了不点赞(
3天前 来自 广东
0
已严肃完成今日之整体二分大学习
4天前 来自 广东
1严肃!
3天前 来自 广东
0
这里打错了
我们使用树状数组BIT,对于所有在的数,我们在它原来的位置加一
应改为
我们使用树状数组BIT,对于所有在 的数,我们在它原来的位置加一
还有你这 Markdown 怎么这么乱
4天前 来自 广东
1并非,如果分到右组,直接减少k,修改l,这样以前的答案就可以重复利用,不应改为1,mid
3天前 来自 广东
0Markdown的混乱是我的本色(
3天前 来自 广东
0哦
3天前 来自 广东
0
虽然看不懂但是还是帮顶
4天前 来自 浙江
1蟹蟹IoI
4天前 来自 广东
0其实写的真的挺好
4天前 来自 浙江
0
怎么榜九了
3小时前 来自 广东
0八,ACGO快管管怎么让学习贴进第八了
2小时前 来自 广东
0怎么榜七了
1小时前 来自 广东
0
居然上榜十了,看来大局要逆转了吗
昨天 来自 广东
0难绷榜十唯一学习贴
昨天 来自 广东
0
!阅读量怎么涨这么快
昨天 来自 广东
0注:动态逆序对其实可以用整体二分做,只是这种整体二分本质上还是CDQ分治的思想,需要先学习CDQ分治的知识
4天前 来自 广东
0ddd
4天前 来自 福建
0笑点解析:第一部分第一句爆Markdown了
4天前 来自 上海
0并不是
4天前 来自 广东
0整个灰色框都是引用部分,为的是与正文区分开,不是爆Markdown了QvQ
4天前 来自 广东
0我说的是框前面那句
4天前 来自 上海
0
比较浅显,仅供参考
4天前 来自 广东
0































有帮助,赞一个