整体二分学习笔记
0.前言
整体二分算法在一定程度上推翻了本蒟蒻之前学习的一些内容、颠覆了本蒟蒻的认知、打开了全新世界的大门。故本蒟蒻认为有必要写个博客记录一下。
1.问题引入
1.1
有一道非常简单的题目:
例一、求区间内第
给出
,求 中第 小的数(即将 从小到大排序后排在第 位的数)。
, , 。
当然,这题可以使用排序算法以及 STL 中的 sort
,priority_queue
或者 kth_element
等做。但是为了贴合主题,考虑使用二分答案。
单调性很显然,若第
所以二分一个 check
函数里判断是否满足
时间复杂度为
#include<bits/stdc++.h>
using namespace std;
#define N 200005
int n,l,r,k,a[N],ql=-1e9,qr=1e9,ans;
bool check(int x){
int tot=0;
for(int i=l;i<=r;++i){
tot+=(a[i]<=x);
}
return tot>=k;
}
int main(){
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
cin>>n>>l>>r>>k;
for(int i=1;i<=n;++i){
cin>>a[i];
}
while(ql<=qr){
int mid=ql+(qr-ql>>1);//由于有负数,取整、位运算可能会出奇奇怪怪的问题,所以通过取长度的一半来实现取中点
if(check(mid)){//查找更小的值域
ans=mid;//更新答案
qr=mid-1;
}else{//查找更大的值域
ql=mid+1;
}
}
cout<<ans;
}
1.2
现在,我们不妨将例一进行扩展:
给出
,共有 次询问,第 次询问 中第 小的数(第 小的数意义同 1.1)。
, , , 。
自然地,可以参照例一的思路,对每一次询问进行二分答案。
时间复杂度为
于是,我们需要引进一种全新的、更高效的算法——整体二分。
2.算法介绍
2.1 算法思想
整体二分是一种离线的分治算法,其主要思想是将多个询问一起二分答案,从而得到提升效率的目的。
2.2 算法步骤(针对例二)
-
既然是分治算法,就可以递归求解。
-
设当前有询问集合
和数集 。为了方便,记询问集合中的元素为 ( 意义同题目, 表示询问编号,即第几个输入),数集中的元素为 ( 表示数值, 表示在数组中的下标)。令当前二分值域为 ,得到 (取数轴上长度的一半,在例一的代码中已经解释过)。 -
若
,则将当前询问集合 中的所有询问 的答案 赋值为 并返回,否则继续执行后续的步骤。 -
将询问集合
分成两个子集 ,数集 分成两个子集 。把区间中所有 的数(数的意义为 中的 ,下同)插入数集 , 的数插入数集 。 -
对于询问集合
中的每一个询问 ,查询在 区间内是否有 个 的数。这一步可以使用树状数组做,在将 的数 插入 集合时,把树状数组上 的位置 ,这样一来,就维护了一个 数在前缀 中总共出现的个数 (算是一种个数的前缀和), 就是 内 的数的个数 。若 ,则说明询问 的答案 ,将询问 插入询问集合 ,否则将询问 插入询问集合 。注意这里变成了第 小。因为容易得出 ,即 集合中的任意数小于 集合中的任意数。当前询问已经在 集合中找到了 个数,在 集合中只要找到 个 的数( 集合对应的递归函数中的 ,不是当前的 ),就满足判定 是否 (意义同上)的条件 。 -
把树状数组清空(对于
集合中的数 ,在树状数组上 的位置 ),因为在接下来的二分中, (是对应递归函数中的 ,不是当前的 )的数的个数是要重新计数的。 -
现在已经将询问和数分成了二分答案值域为
(询问集合 和数集 )和 (询问集合 和数集 )的两部分,分别对这两部分递归、重复上述步骤进行整体二分求解。
2.3 算法复杂度分析(针对例二)
注:以下分析中时间复杂度和空间复杂度已经次数、层数等均为近似值。
设值域(上界减下界)为
通过上面的分析,容易得出整体二分算法的时间复杂度为
由于
离散化之后,可以将时间复杂度优化成
2.4 算法的代码实现(针对例二)
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,bit[N],ans[N];//bit 是树状数组。
struct query{//询问。
int l,r,k,id;//左端点、右端点、第 k 小、询问编号(即第几个输入)。
}Q[N];//询问集合。
struct num{//数。
int x,id;//数值、下标。
}A[N];//数集。
//树状数组。
void add(int x,int k){//单点加。
for(;x<=n;x+=x&-x){
bit[x]+=k;
}
}
int sum(int x){//前缀和。
int ret=0;
for(;x;x-=x&-x){
ret+=bit[x];
}
return ret;
}
//整体二分。
void solve(query q[],num a[],int l,int r,int lenq,int lena){//当前询问集合、当前数集、二分下界、二分上界、询问集合大小、数集大小。
if(!lenq){//如果没有询问的答案在 [l,r] 内,就返回不做了。
return;
}
if(l==r){//得到答案,赋值、返回。
for(int i=1;i<=lenq;++i){
ans[q[i].id]=l;
}
return;
}
int mid=l+(r-l)/2,lenq1=0,lenq2=0,lena1=0,lena2=0;//mid 取长度的一半,4 个 len 分别对应 4 个数组的长度。
query q1[lenq+5],q2[lenq+5];//q1 和 q2 分别对应上文说到的 M 和 N。
num a1[lena+5],a2[lena+5];//a1 和 a2 分别对应上文说到的 B 和 C。
for(int i=1;i<=lena;++i){
if(a[i].x<=mid){//如果 x<=mid。
add(a[i].id,1);//更新到树状数组上。
a1[++lena1]=a[i];//插入 a1(B)。
}else{
a2[++lena2]=a[i];//插入 a2(C)。
}
}
for(int i=1,p;i<=lenq;++i){
p=sum(q[i].r)-sum(q[i].l-1);//前缀和得到 num。
if(p>=q[i].k){//如果 num>=k。
q1[++lenq1]=q[i];//插入 q1(M)。
}else{
q[i].k-=p;//已经解释过,新的 k 要变成原来的 k 减去 p(num)。
q2[++lenq2]=q[i];//插入 q2(N)。
}
}
for(int i=1;i<=lena1;++i){//清空树状数组。
add(a1[i].id,-1);
}
solve(q1,a1,l,mid,lenq1,lena1);//整体二分值域为 [l,mid] 的部分。
solve(q2,a2,mid+1,r,lenq2,lena2);//整体二分值域为 [mid+1,r] 的部分。
}
int main(){
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
cin>>n>>m;
for(int i=1,x;i<=n;++i){
cin>>x;
A[i]={x,i};//插入初始数集 A。
}
for(int i=1,l,r,k;i<=m;++i){
cin>>l>>r>>k;
Q[i]={l,r,k,i};//离线询问。
}
solve(Q,A,-1e9,1e9,m,n);//整体二分。
for(int i=1;i<=m;++i){//输出答案。
cout<<ans[i]<<'\n';
}
}
本蒟蒻的评测记录:link。
经过实测,在 solve
中 q(Q),a(A),q1(M),q2(N),a1(B),a2(C)
使用数组比使用 vector
效率高许多。
3 习题
3.1 CF484E Sign on Fence
注:以下内容均搬运与我的博客 CF484E Sign on Fence。
给定一个长度为
的数列 ,有 次询问,每次在给出 中选一个长度为 的子区间,使得选出区间的最小值最大。
, 。
首先看到最小值最大,不难想到二分。由于是多组询问,所以考虑整体二分。
在整体二分中,我们套路地将所有询问当前答案的值域
具体来讲,可以参照洛谷 P4513 的思路,使用线段树维护所有
令
-
若
,说明左起最长连续段不横跨左右儿子的区间, ;否则说明横跨左右儿子的区间, 。 类似维护即可。 -
。显而易见。 -
。表示分别考虑答案完全在左儿子区间内、完全在右儿子区间内和横跨左右儿子的区间。
此外,这题并不像模板题一样答案符合可加性,因为模板题只有数量的限制,可以加加减减。但是这题还有连续的限制,就不能像模板题那样,若查询结果为 TLE
。由于
令
实现细节:
-
我是用
vector
储存整体二分中的数与询问的。 -
使用指令集或离散化(嫌麻烦没写 qwq)可以加快代码运行效率。
#pragma GCC optimize("Ofast")
#include<bits/stdc++.h>
using namespace std;
#define ls x*2
#define rs x*2+1
const int N=100005;
int n,m,ans[N];
struct queries{
int l,r,k,id;
};
vector<queries>Q;
struct num{
int x,p;
};
vector<num>A;
struct tree{
int len,llen,rlen,sz;
}sg[N*4];
void up(int x){
sg[x].sz=sg[ls].sz+sg[rs].sz;
sg[x].len=max({sg[ls].len,sg[rs].len,sg[ls].rlen+sg[rs].llen});
sg[x].llen=(sg[ls].len==sg[ls].sz?sg[ls].len+sg[rs].llen:sg[ls].llen);
sg[x].rlen=(sg[rs].len==sg[rs].sz?sg[rs].len+sg[ls].rlen:sg[rs].rlen);
}
void build(int x,int l,int r){
if(l==r){
sg[x]={0,0,0,1};
return;
}
int mid=l+r>>1;
build(ls,l,mid);
build(rs,mid+1,r);
up(x);
}
void change(int x,int l,int r,int k,int v){
if(l==r&&l==k){
sg[x]={v,v,v,1};
return;
}
int mid=l+r>>1;
if(k<=mid){
change(ls,l,mid,k,v);
}else{
change(rs,mid+1,r,k,v);
}
up(x);
}
tree query(int x,int l,int r,int ql,int qr){
if(ql<=l&&qr>=r){
return sg[x];
}
int mid=l+r>>1;
if(qr<=mid){
return query(ls,l,mid,ql,qr);
}else if(ql>mid){
return query(rs,mid+1,r,ql,qr);
}else{
tree t1=query(ls,l,mid,ql,qr),t2=query(rs,mid+1,r,ql,qr);
return {max({t1.len,t2.len,t1.rlen+t2.llen}),(t1.len==t1.sz?t1.len+t2.llen:t1.llen),(t2.len==t2.sz?t2.len+t1.rlen:t2.rlen),t1.sz+t2.sz};
}
}
void solve(vector<queries>q,vector<num>a,int l,int r){
if(!q.size()||!a.size()){
return;
}
if(l^r){
int mid=l+r>>1;
vector<queries>q1,q2;
vector<num>a1,a2;
for(auto i:a){
if(i.x>mid){
a1.push_back(i);
change(1,1,n,i.p,1);
}else{
a2.push_back(i);
}
}
for(auto i:q){
tree tmp=query(1,1,n,i.l,i.r);
if(tmp.len>=i.k){
q1.push_back(i);
}else{
q2.push_back(i);
}
}
solve(q2,a2,l,mid);
for(auto i:a){
if(i.x>mid){
change(1,1,n,i.p,0);
}
}
solve(q1,a1,mid+1,r);
}else{
for(auto i:q){
ans[i.id]=l;
}
}
}
signed main(){
cin>>n;
for(int i=1,x;i<=n;++i){
cin>>x;
A.push_back({x,i});
}
cin>>m;
for(int i=1,l,r,k;i<=m;++i){
cin>>l>>r>>k;
Q.push_back({l,r,k,i});
}
build(1,1,n);
solve(Q,A,1,1e9);
for(int i=1;i<=m;++i){
cout<<ans[i]<<'\n';
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】