整体二分算是一个比较重要的算法,我个人看来也挺难的(主要是我菜)
目前也写了三道模板题,来小小的总结一下
整体二分的适用情况
当题目的询问需要进行二分,且有多组操作的时候(包括赋值操作),我们可以同时对所有操作二分,每次将左区间的贡献加到右区间继续分治下去即可,主体架构很像CDQ分治
当然也可以用一些大数据结构写(明示树套树)
算法流程
我们首先确定范围,答案的值域和所有询问
对于每次二分到的mid,我们对于不同的要求,把mid左边的贡献统计出来
对于当前区间所有操作判断,如果合法,放进左边的数组,不然放进右边数组,同时把左边的贡献存进一个随时间更新的数组里,因为接下来他们二分的值域区间是
[
m
i
d
+
1
,
r
]
[mid+1,r]
[mid+1,r],不会包含
[
l
,
m
i
d
]
[l,mid]
[l,mid]区间的贡献,然后清空当前的状态量,分为左右数组继续分治,如果
l
=
=
r
l==r
l==r,即找到可行解,统计答案。
接下来是三道题目
洛谷P3527 [POI2011]MET-Meteors
这一题显然对于每一个点,进行二分是最好的,但是它又要求多组解
那么我们整体二分,这里的值域是k个陨石,问题是n个王国
我们维护一个时间戳,每次暴力把当前状态更新到mid处,想一下,如果每次清空的话,那么每次二分都回覆盖
[
l
,
m
i
d
]
[l,mid]
[l,mid],是会超时的,但是这样每次更新只会有半个区间长,更快
因为是区间加,所以要差分,但又因为是环,我们要特殊判断
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
typedef long long lll;
const int maxn = 300007;
const int INF = 2147483647;
long long tr[maxn],val[maxn];
int n,m,k;
vector<int>cont[maxn];
int ans[maxn];
int ll[maxn],rr[maxn],num[maxn],t;
int lt[maxn],rt[maxn],pos[maxn],want[maxn];
int lowbit(int x){return x&-x;}
void jb(int x,int num){
for(;x<=m;x+=lowbit(x))
tr[x]+=num;
}
lll que(int x){
lll res=0;
for(;x;x-=lowbit(x))res+=tr[x];
return res;
}
void change(int l,int r,int num){
if(l<=r){
jb(l,num),jb(r+1,-num);
}
else{
jb(l,num),jb(m+1,-num);
jb(1,num),jb(r+1,-num);
}
}
void query(int l,int r,int p1,int p2){
if(l>r)return;if(p1>p2)return;
if(l==r){
for(int i=p1;i<=p2;i++){
ans[pos[i]]=l;
}
return ;
}
int mid=(l+r)>>1;
while(t<mid)++t,change(ll[t],rr[t],num[t]);
while(t>mid)change(ll[t],rr[t],-num[t]),t--;
for(int i=p1;i<=p2;i++){
val[pos[i]]=0;
for(int j=0;j<cont[pos[i]].size();j++){
val[pos[i]]+=que(cont[pos[i]][j]);
if(val[pos[i]]>=want[pos[i]])break;
}
}
int ls=0,rs=0;
for(int i=p1;i<=p2;i++){
if(val[pos[i]]>=want[pos[i]]){
lt[++ls]=pos[i];
}
else rt[++rs]=pos[i];
}
int mid2=ls;
for(int i=1;i<=ls;i++)pos[i+p1-1]=lt[i];
for(int i=1;i<=rs;i++)pos[i+p1+ls-1]=rt[i];
query(l,mid,p1,p1+mid2-1);
query(mid+1,r,p1+mid2,p2);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x;
scanf("%d",&x);
cont[x].push_back(i);
}
for(int i=1;i<=n;i++)
scanf("%d",&want[i]),pos[i]=i;
scanf("%d",&k);
for(int i=1;i<=k;i++){
scanf("%d%d%d",&ll[i],&rr[i],&num[i]);
}
++k,ll[k]=1,rr[k]=m;num[k]=INF;
query(1,k,1,n);
for(int i=1;i<=n;i++){
if(ans[i]==k){
printf("NIE\n");
}
else printf("%d\n",ans[i]);
}
return 0;
}
洛谷P1527 [国家集训队]矩阵乘法
这题要去二维区间第K大,我们使用树套树当然是没有问题的,但是难写啊!
我们还是使用整体二分,我们将所有点,记录在一个结构体里,存下
(
x
,
y
)
(x,y)
(x,y)和
v
a
l
val
val
然后我们开始二分
我们先开始染色,他要求的是二维区间第K大,那么对于我们二分的mid,如果任务区间内
大于它的数有k-1个,不就正好是答案了吗,那么我们先对
[
l
,
m
i
d
]
[l,mid]
[l,mid]染色,全部变成1
这里我们使用了二维树状数组,然后判断放进左右数组的时候,我们树状数组二维区间求和即可
判断所求值和目标排名的关系即可
注意这次树状数组就要清空了,但是切记不能用memset,怎么加的怎么删就行
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<cstdlib>
using namespace std;
const int maxn = 507;
const int N = 60007;
int lowbit(int x){return x&-x;}
struct number{
int x,y,v;
friend bool operator <(number a,number b){return a.v<b.v;}
}num[maxn*maxn];
struct task{int f1,f2,f3,f4,k;}Q[N];
int id[N],s1[N],s2[N];
int n,q,tr[maxn][maxn];
void add(int x,int y,int val){
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=n;j+=lowbit(j))
tr[i][j]+=val;
}
int queryPoint(int x,int y){
int ret=0;
for(int i=x;i;i-=lowbit(i))
for(int j=y;j;j-=lowbit(j))
ret+=tr[i][j];
return ret;
}
int querySection(int f1,int f2,int f3,int f4){
return queryPoint(f1-1,f2-1)
+queryPoint(f3,f4)
-queryPoint(f1-1,f4)
-queryPoint(f3,f2-1);
}
int cur[N],ans[N];
void solve(int l,int r,int L,int R){
if(R<L)return;
if(l==r){
for(int i=L;i<=R;i++)ans[id[i]]=num[l].v;
return;
}
int mid=l+r>>1;
for(int i=l;i<=mid;i++)add(num[i].x,num[i].y,1);
int S1=0,S2=0;
for(int i=L;i<=R;i++){
int u=id[i];
int sum=cur[u]+querySection(Q[u].f1,Q[u].f2,Q[u].f3,Q[u].f4);
if(sum>=Q[u].k) s1[++S1]=u;
else s2[++S2]=u,cur[u]=sum;
}
int start=L-1;
for(int i=1;i<=S1;i++)id[++start]=s1[i];
for(int i=1;i<=S2;i++)id[++start]=s2[i];
for(int i=l;i<=mid;i++)add(num[i].x,num[i].y,-1);
solve(l,mid,L,L+S1-1);solve(mid+1,r,L+S1,R);
}
int temp=0;
int main(){
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
int x;scanf("%d",&x);
num[++temp]=(number){i,j,x};
}
sort(num+1,num+1+temp);
for(int i=1;i<=q;i++)id[i]=i,scanf("%d%d%d%d%d",&Q[i].f1,&Q[i].f2,&Q[i].f3,&Q[i].f4,&Q[i].k);
solve(1,temp,1,q);
for(int i=1;i<=q;i++)printf("%d\n",ans[i]);
}
最后这一题有点难
洛谷P3332 [ZJOI2013]K大数查询
这一题比较艹,因为有修改,然而实际上问题并不大,我们要求的还是区间第K大
但是这次是一维数列还有区间加,好像必须树套树了耶。。。
我会屈服吗?不会,为此我特地去学了超级树状数组,发现出人意料的好写…
回归题目,我们考虑在分治过程中遇到区间加操作的怎么办
我们考虑值域范围内二分出来的mid,如果区间加的C大于mid,我们就统计
将任务区间全部加一,并放到右区间
不然就不动,放到左区间
我们考虑为什么,当时我想了很久,这不是一点不考虑以前的贡献吗?
然后我发现我傻了,因为我tm看错题了,看完样例解析,我豁然开朗
原来加一个数的意思就是,当前位置多一个数,当前位置可以有很多数!
那么这题一下子就简单了,上面的操作原因也就很显然了,因为C小于mid时
是有可能对左区间产生贡献的,所以放下去继续二分
但是大于mid时,就只会对右边产生贡献了,我们往右边继续分治,但是要更新右区间的目标值.
即减去之前的区间加对他的贡献
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int maxn = 50007;
typedef long long ll;
const int N = 200000;
int lowbit(int x){return x&-x;}
struct node{
int tp,a,b,id;
ll c;
}tk[maxn],s1[maxn],s2[maxn];
int out[maxn],n,m;
ll tr1[maxn],tr2[maxn];
void add(ll tr[],int x,int num){
for(;x<=n;x+=lowbit(x))
tr[x]+=num;
}
ll query(ll tr[],int x){
ll ret=0;
for(;x;x-=lowbit(x))ret+=tr[x];
return ret;
}
void solve(int l,int r,int L,int R){
if(L>R)return;
if(l==r){
for(int i=L;i<=R;i++)
out[tk[i].id]=l;
return;
}
int mid=l+r>>1;int S1=0,S2=0;
for(int i=L;i<=R;i++){
if(tk[i].tp==2){
ll sum=tk[i].b*query(tr1,tk[i].b) - query(tr2,tk[i].b) - (tk[i].a-1)*query(tr1,tk[i].a-1) + query(tr2,tk[i].a-1);
if(sum<tk[i].c){
tk[i].c-=sum;
s1[++S1]=tk[i];
} else s2[++S2]=tk[i];
}
else{
if(tk[i].c>mid){
add(tr1,tk[i].a,1),add(tr1,tk[i].b+1,-1);
add(tr2,tk[i].a,tk[i].a-1),add(tr2,tk[i].b+1,-tk[i].b);
s2[++S2]=tk[i];
} else s1[++S1]=tk[i];
}
}
for(int i=L;i<=R;i++){
if(tk[i].tp==1&&tk[i].c>mid){
add(tr1,tk[i].a,-1),add(tr1,tk[i].b+1,1);
add(tr2,tk[i].a,-tk[i].a+1),add(tr2,tk[i].b+1,tk[i].b);
}
}
int mxt=L-1;
for(int i=1;i<=S1;i++)tk[++mxt]=s1[i];
for(int i=1;i<=S2;i++)tk[++mxt]=s2[i];
solve(l,mid,L,L+S1-1);
solve(mid+1,r,L+S1,R);
}
int mxt;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d%lld",&tk[i].tp,&tk[i].a,&tk[i].b,&tk[i].c);
if(tk[i].tp==2)tk[i].id=++mxt;
}
solve(-n,n,1,m);
for(int i=1;i<=mxt;i++)
printf("%lld\n",out[i]);
return 0;
}
好像模板的就这三题,我刷题量还是太少了嘤嘤嘤
还有一些多组询问的也可以用整体二分写,不过我还是喜欢写BIT套主席树
比如洛谷P2617 Dynamic Rankings
我 永 远 喜 欢 主 席 树 . j p g ! ! ! 我永远喜欢主席树.jpg!!! 我永远喜欢主席树.jpg!!!