整体二分学习笔记
1. 简介
在一些题目中,可能存在一些题目,对于每次询问直接二分可能会TLE,此时就要用到整体二分
整体二分是一种离线的方法,适用于如下情况:
-
询问答案具有可二分性
-
修改对判定答案的贡献相互独立,修改之间互不影响效果
-
修改如果对判定答案有贡献,则该贡献是一个确定的与判定标准无关的值
-
贡献满足交换律,结合律,具有可加性
-
题目允许使用离线算法
2. 思想
记[l,r]为答案的值域,[L,R]为答案的定义域,即求解时仅考虑下标[L,R]内的操作和查询,这其中询问的答案在[l,r]里
-
将所有操作按时间排序存入数组,然后开始分治
-
在分治的过程中,利用数据结构(通常是树状数组)记录当前查询答案与mid之间的关系
-
根据查询出来的答案与mid的关系(小于等于和大于)将当前的操作序列分为
和 两部分,分别递归处理 -
当l==r时,找到答案,记录并返回即可
需要注意,在整体二分的过程中,若当前处理的值域为[l,r],如果某个询问的最终答案的范围不在[l,r]中,则会在其他时候处理
3. 具体过程
3.1. 查询全局第k小
可以分别对每一个序列二分,但是也可以将所有询问放在一起二分
从二分的本质考虑,假设要猜一个[l,r]范围内的数,可以先猜
所以,在有多次询问时,可以先全猜答案是mid,然后将所有询问分为2类,分别处理
3.2. 查询区间第k小
涉及给定区间的查询,直接二分并用check函数判断大小,时间复杂度会很大
考虑值域中点mid与询问的关系,如果区间内小于等于mid的数有t个,询问的是第k小,如果
此时需记录一个区间小于等于指定数的个数,即单点修改,区间求和,可以用树状数组
为提高效率,只对数列中值在值域区间 [l,r] 的数进行统计,即,在进一步递归之前,不仅将询问划分,将当前处理的数按值域范围划为两半。
3.3. 带修区间第k小
3.3.1. 例题
Dynamic Rankings
https://gxyzoj.com/d/gxyznoi/p/P26
题目描述
给定一个含有
Q l r k
表示查询下标在区间 中的第 小的数C x y
表示将 改为
输入格式
第一行两个正整数
第二行
接下来
输出格式
对于每一次询问,输出一行一个整数表示答案。
提示
【数据范围】
对于
对于
对于
对于
3.3.2. 解法
修改操作可以直接理解为从原数列中删去一个数再添加一个数,为方便起见,将询问和修改统称为操作
因后面的操作会依附于之前的操作,不能如3.2一样将统计和处理询问分开,故可将所有操作存于一个数组,用标识区分类型,依次处理每个操作
为便于处理树状数组,修改操作可分拆为擦除操作和插入操作。
3.3.3. 代码
#include<cstdio>
#include<iostream>
using namespace std;
int n,m,tot,inf=1e9+7;
struct node{
int l,r,k,type,id;
}q[300005],q1[300005],q2[300005];
int a[100005],cntC,cntQ;
int ans[100005];
int sum[100005];
int lowbit(int x)
{
return x & (-x);
}
void add(int id,int val)
{
while(id<=n)
{
sum[id]+=val;
id+=lowbit(id);
}
}
int query(int id)
{
int res=0;
while(id)
{
res+=sum[id];
id-=lowbit(id);
}
return res;
}
void solve(int l,int r,int L,int R)
{
if(l>r||L>R) return;
int cnt1=0,cnt2=0,mid=(l+r)>>1;
if(l==r)
{
for(int i=L;i<=R;i++)
{
if(q[i].type) ans[q[i].id]=l;
}
return;
}
for(int i=L;i<=R;i++)
{
if(q[i].type)
{
int t=query(q[i].r)-query(q[i].l-1);
if(q[i].k<=t) q1[++cnt1]=q[i];
else
{
q[i].k-=t;
q2[++cnt2]=q[i];
}
}
else
{
if(q[i].r<=mid)
{
add(q[i].l,q[i].k);
q1[++cnt1]=q[i];
}
else q2[++cnt2]=q[i];
}
}
for(int i=1;i<=cnt1;i++)
{
if(!q1[i].type) add(q1[i].l,-q1[i].k);
}
for(int i=1;i<=cnt1;i++)
{
q[L+i-1]=q1[i];
}
for(int i=1;i<=cnt2;i++)
{
q[L+cnt1+i-1]=q2[i];
}
solve(l,mid,L,L+cnt1-1);
solve(mid+1,r,L+cnt1,R);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
q[++tot]=(node){i,a[i],1,0,0};
}
for(int i=1;i<=m;i++)
{
char opt;
cin>>opt;
if(opt=='Q')
{
cntQ++;
int x,y,k;
scanf("%d%d%d",&x,&y,&k);
q[++tot]=(node){x,y,k,1,cntQ};
}
else
{
cntC++;
int x,y;
scanf("%d%d",&x,&y);
q[++tot]=(node){x,a[x],-1,0,cntC};
q[++tot]=(node){x,y,1,0,cntC};
a[x]=y;
}
}
solve(0,inf,1,tot);
for(int i=1;i<=cntQ;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
4. 类题
4.1. [国家集训队] 矩阵乘法
https://gxyzoj.com/d/gxyznoi/p/27
4.1.1. 思路
和3.3.1很像,只是减少了删除操作,此外,将区间求和变为了矩阵求和,所以,只需要将树状数组的部分换成二维的即可
4.1.2. 代码
#include<cstdio>
using namespace std;
int n,p,a[505][505],tot,inf=1e9+7;
struct node{
int ax,ay,bx,by,k,type,id;
}q[310005],q1[310005],q2[310005];
int ans[60005],sum[505][505];
int lowbit(int x)
{
return x & (-x);
}
void add(int x,int y,int c)
{
for(int i=x;i<=n;i+=lowbit(i))
{
for(int j=y;j<=n;j+=lowbit(j))
{
sum[i][j]+=c;
}
}
}
int query(int x,int y)
{
int res=0;
for(int i=x;i;i-=lowbit(i))
{
for(int j=y;j;j-=lowbit(j))
{
res+=sum[i][j];
}
}
return res;
}
void solve(int l,int r,int L,int R)
{
// printf("%d %d %d %d\n",l,r,L,R);
if(l>r||L>R) return;
int cnt1=0,cnt2=0,mid=(l+r)>>1;
if(l==r)
{
for(int i=L;i<=R;i++)
{
if(q[i].type) ans[q[i].id]=l;
}
return;
}
for(int i=L;i<=R;i++)
{
if(q[i].type)
{
int t=query(q[i].bx,q[i].by)-query(q[i].ax-1,q[i].by)-query(q[i].bx,q[i].ay-1)+query(q[i].ax-1,q[i].ay-1);
if(q[i].k<=t) q1[++cnt1]=q[i];
else
{
q[i].k-=t;
q2[++cnt2]=q[i];
}
}
else
{
if(q[i].k<=mid)
{
add(q[i].ax,q[i].ay,1);
q1[++cnt1]=q[i];
}
else
{
q2[++cnt2]=q[i];
}
}
}
for(int i=1;i<=cnt1;i++)
{
if(!q1[i].type) add(q1[i].ax,q1[i].ay,-1);
}
for(int i=1;i<=cnt1;i++)
{
q[L+i-1]=q1[i];
}
for(int i=1;i<=cnt2;i++)
{
q[L+cnt1+i-1]=q2[i];
}
solve(l,mid,L,L+cnt1-1);
solve(mid+1,r,L+cnt1,R);
}
int main()
{
scanf("%d%d",&n,&p);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
scanf("%d",&a[i][j]);
tot++;
q[tot].ax=i,q[tot].ay=j,q[tot].type=0,q[tot].k=a[i][j];
}
}
for(int i=1;i<=p;i++)
{
tot++;
scanf("%d%d%d%d%d",&q[tot].ax,&q[tot].ay,&q[tot].bx,&q[tot].by,&q[tot].k);
q[tot].id=i,q[tot].type=1;
}
solve(0,inf,1,tot);
for(int i=1;i<=p;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
4.2. [THUPC2017] 天天爱射击
https://gxyzoj.com/d/gxyznoi/p/28
4.2.1. 思路
需要进行一些转换,由记录每颗子弹能打碎哪些木棒变为能记录每个木棒被打碎的次数,二分时间统计即可
注意要用一个新点记录无法被打碎的木棒
4.2.2. 代码
#include<cstdio>
using namespace std;
int n,m,c[200005],inf=200000;
struct node{
int l,r,k,id;
}q[200005],q1[200005],q2[200005];
int ans[200005],sum[200005];
int lowbit(int x)
{
return x & (-x);
}
void add(int id,int val)
{
while(id<=inf)
{
sum[id]+=val;
id+=lowbit(id);
}
}
int query(int id)
{
int res=0;
while(id)
{
res+=sum[id];
id-=lowbit(id);
}
return res;
}
void solve(int l,int r,int L,int R)
{
// printf("%d %d %d %d\n",l,r,L,R);
if(l>r||L>R) return;
int cnt1=0,cnt2=0,mid=(l+r)>>1;
if(l==r)
{
ans[l]+=R-L+1;
return;
}
for(int i=l;i<=mid;i++)
{
add(c[i],1);
}
for(int i=L;i<=R;i++)
{
int t=query(q[i].r)-query(q[i].l-1);
if(t>=q[i].k) q1[++cnt1]=q[i];
else
{
q[i].k-=t;
q2[++cnt2]=q[i];
}
}
for(int i=l;i<=mid;i++)
{
add(c[i],-1);
}
for(int i=1;i<=cnt1;i++)
{
q[i+L-1]=q1[i];
}
for(int i=1;i<=cnt2;i++)
{
q[i+L+cnt1-1]=q2[i];
}
solve(l,mid,L,L+cnt1-1);
solve(mid+1,r,L+cnt1,R);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d%d%d",&q[i].l,&q[i].r,&q[i].k);
q[i].id=i;
}
for(int i=1;i<=m;i++)
{
scanf("%d",&c[i]);
}
c[m+1]=1;
solve(1,m+1,1,n);
for(int i=1;i<=m;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
4.3. 「CTSC2018」混合果汁
https://gxyzoj.com/d/gxyznoi/p/P29
4.3.1. 思路
可以二分最低的美味度,然后考虑判断
这里可以使用贪心,显然,先将价格便宜的买完是最便宜的,所以可以二分
但是,二分的内容必须有序,如果每一次都从新排序的话,必然会T,所以考虑树状数组
每一次要将需要用的加入,离开时清空,但是假设当前二分的为x,则x~n都要加入,暴力显然会T
所以,可以先二分右边,在l=r时将l加入,永不清空,因为l不会再成为左半边
判断无解,只需要在最前面加入一个d为-1的点即可
4.3.2. 代码
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
int n,m,inf=100003;
ll lowbit(ll x)
{
return x & (-x);
}
struct tree{
ll sum[100010];
void add(ll id,ll val)
{
while(id<=inf)
{
sum[id]+=val;
id+=lowbit(id);
}
}
ll query(ll id)
{
ll res=0;
while(id)
{
res+=sum[id];
id-=lowbit(id);
}
return res;
}
}tr[2];
struct juice{
ll d,p,l;
}a[100010];
struct node{
int id;
ll l,g;
}q[100010],q1[100010],q2[100010];
bool cmp(juice x,juice y)
{
return x.d<y.d;
}
int ans[100010];
void solve(int l,int r,int L,int R)
{
// printf("%d %d %d %d\n",l,r,L,R);
// if(l>r||L>R) return;
if(l==r)
{
for(int i=L;i<=R;i++)
{
ans[q[i].id]=a[l].d;
}
tr[0].add(a[l].p,a[l].l);
tr[1].add(a[l].p,a[l].p*a[l].l);
return;
}
int cnt1=0,cnt2=0,mid=(l+r+1)>>1;
for(int i=mid;i<=r;i++)
{
tr[0].add(a[i].p,a[i].l);
tr[1].add(a[i].p,a[i].p*a[i].l);
}
for(int i=L;i<=R;i++)
{
int l1=0,r1=inf;
while(l1<r1)
{
int mid=(l1+r1)>>1;
if(tr[0].query(mid)>=q[i].l)
{
r1=mid;
}
else l1=mid+1;
}
ll tmp1=tr[0].query(l1),tmp2=tr[1].query(l1);
if(tmp1>=q[i].l&&tmp2-(tmp1-q[i].l)*l1<=q[i].g)
{
q2[++cnt2]=q[i];
}
else
{
q1[++cnt1]=q[i];
}
}
for(int i=1;i<=cnt1;i++)
{
q[L+i-1]=q1[i];
}
for(int i=1;i<=cnt2;i++)
{
q[L+cnt1+i-1]=q2[i];
}
for(int i=mid;i<=r;i++)
{
tr[0].add(a[i].p,-a[i].l);
tr[1].add(a[i].p,-a[i].p*a[i].l);
}
solve(mid,r,L+cnt1,R);
solve(l,mid-1,L,L+cnt1-1);
}
int main()
{
scanf("%d%d",&n,&m);
n++;
for(int i=2;i<=n;i++)
{
scanf("%lld%lld%lld",&a[i].d,&a[i].p,&a[i].l);
}
a[1].d=-1,a[1].p=1,a[1].l=1;
sort(a+1,a+n+1,cmp);
for(int i=1;i<=m;i++)
{
scanf("%lld%lld",&q[i].g,&q[i].l);
q[i].id=i;
}
solve(1,n,1,m);
for(int i=1;i<=m;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧