整体二分

整体二分

整体二分是一种离线算法,主要用于解决题目中存在多次询问,每次询问都要二分,并且询问可离线的问题,之前看了网上许多博客感觉大多都很难理解,我们先给出例题,通过题目能更好地理解

例题

题目传送门:Luogu P3332 K大数查询

题目大意

给定 \(n\) 个初始为空的可重集合与 \(m\) 个操作,对于每次插入操作你需要在编号为 \(l\)\(r\) 的集合中插入元素 \(c\),对于每次询问操作你需要给出在编号为 \(l\)\(r\) 的集合中排名为 \(c\) 的元素

题解

首先考虑每一个单独的询问操作如何进行二分答案,对于每一个询问我们都需要在值域上二分 \(mid\),每次check时统计询问的集合中比 \(mid\) 大的数的个数,但是这样显然时间复杂度过高,怎么优化呢?

如果先不管插入操作的话其实我们可以把所有的询问操作一起二分,每次找到一个 \(mid\),利用线段树统计大于 \(mid\) 的数字的个数,如果个数大于询问的排名,也就是说 \(mid\) 太小了,那么把这些询问放在一起,另外把个数小于询问的排名的放在一起,对于这两堆询问,依次分治下去,把值域根据 \(mid\) 应当更大或者更小改为 \([mid+1,r]\) 或者 \([l,mid]\) 即可,递归到 \(mid\) 的值唯一确定时这些询问的答案就是 \(mid\),时间复杂度 \(\Theta(n\log^2n)\)

但是现在有了插入操作,怎么办呢?其实我们发现,插入操作对于二分到的 \(mid\) 来说也是分为有贡献和无贡献的,如果插入的数字小于 \(mid\),那么它对于 \(mid\) 的排名就没有任何影响,如果插入的数字大于 \(mid\),那么它对于 \(mid\) 的排名就已经产生了影响,根据这个我们可以把插入操作也分为两部分

然后我们需要一起考虑插入和查询操作,现在我们已经通过上面的方法根据二分到的 \(mid\) 把询问和插入操作分别分为了两个部分,对于没有贡献的插入操作,我们把它们和 “查询的排名大于比 \(mid\) 大的数的个数” 的询问操作放在一起,因为这些询问操作需要更多的插入操作才能有结果,而已经有贡献的插入操作我们把它们与 “查询的排名小于比 \(mid\) 大的数的个数” 的询问操作放在一起,因为这些询问已经有了足够的插入操作了,我们要减少插入操作的数量才能找到答案 遍历操作后根据 \(mid\) 分堆,如下图

image

现在我们就可以在 \(\Theta(n\log^2n)\) 的时间复杂度下解决这个问题了

再捋一遍思路,我们把所有操作一起二分,每次递归我们需要给定二分的值域和操作的序列(也就是上文中说到的每一堆操作),如果值域只剩下一个数,那么操作序列中所有询问操作的答案就是这个数,记录之后返回即可,否则我们就遍历当前操作序列中的每一个操作,根据 \(mid\) 值来确定这个操作应该被放在哪一堆,对于修改操作如果已经有贡献了就把它的贡献加入线段树中进行统计,最后别忘了把修改的线段树恢复至初始状态,然后分别把值域减半递归计算两堆操作即可

Code

#include<bits/stdc++.h>
#define in read()
#define MAXN 100005
using namespace std;
typedef long long ll;
//IO优化
inline int read()
{
    char c=getchar();
    int x=0,f=1;
    while(c<48){if(c=='-')f=-1;c=getchar();}
    while(c>47)x=(x*10)+(c^48),c=getchar();
    return x*f;
}
inline void mwrite(int a)
{
    if(a>9)mwrite(a/10);
    putchar((a%10)|48);
}
inline void write(int a,char c)
{
	mwrite(a<0?(putchar('-'),a=-a):a);
	putchar(c);
}
//Segment Tree-------------------------------------
struct Node
{
	int l,r;
	ll lazy,sum;
}node[MAXN<<2];
inline void pushup(int pos){node[pos].sum=node[pos<<1].sum+node[pos<<1|1].sum;}//维护节点信息
inline void down(int pos)//下传懒标记
{
	if(!node[pos].lazy)return;
	node[pos<<1].lazy+=node[pos].lazy,node[pos<<1|1].lazy+=node[pos].lazy;
	node[pos<<1].sum+=node[pos].lazy*(node[pos<<1].r-node[pos<<1].l+1);
	node[pos<<1|1].sum+=node[pos].lazy*(node[pos<<1|1].r-node[pos<<1|1].l+1);
	node[pos].lazy=0;
}
inline void build(int l,int r,int pos)//建树
{
	node[pos]=(Node){l,r,0,0};
	if(l==r) return;
	int mid=(l+r)>>1;
	build(l,mid,pos<<1);
	build(mid+1,r,pos<<1|1);
}
inline void modify(int l,int r,ll v,int pos)//区间修改
{
	if(l<=node[pos].l&&node[pos].r<=r)
		return node[pos].lazy+=v,node[pos].sum+=v*(node[pos].r-node[pos].l+1),void(0);
	down(pos);
	int mid=(node[pos].l+node[pos].r)>>1;
	if(l<=mid) modify(l,r,v,pos<<1);
	if(r>mid) modify(l,r,v,pos<<1|1);
	pushup(pos);
}
inline ll query(int l,int r,int pos)//查询区间和
{
	if(l<=node[pos].l&&node[pos].r<=r) return node[pos].sum;
	down(pos);
	int mid=(node[pos].l+node[pos].r)>>1;
	ll ans=0;
	if(l<=mid) ans+=query(l,r,pos<<1);
	if(r>mid) ans+=query(l,r,pos<<1|1);
	return ans;
}
//Segment Tree-------------------------------------
struct Query
{
	int opt,l,r,id;
	ll v;
}q[MAXN],q1[MAXN],q2[MAXN];
int n,m,qnum,Ans[MAXN];
void solve(ll l,ll r,int ql,int qr)//值域,操作区间 
{
	if(ql>qr||l>r)return;
	if(l==r)//答案唯一,直接赋值 
	{
		for(int i=ql;i<=qr;++i)
			if(q[i].opt==2) Ans[q[i].id]=l;
		return;
	}
	ll mid=(l+r)>>1,tmpcnt;
	int lcnt=0,rcnt=0;
	bool queryr=0,queryl=0;
	for(int i=ql;i<=qr;++i)//遍历询问 
	{
		if(q[i].opt==1)//修改操作 
		{
			if(q[i].v>mid)//比二分到的答案大,会有贡献,需要在线段树上修改
			{
				modify(q[i].l,q[i].r,1,1);//线段树上询问区间都加上1,表明有一个更大的数 
				q2[++rcnt]=q[i];//放进右侧
			}
			else q1[++lcnt]=q[i];//小于二分到的答案,暂时没有贡献,放入左侧
		}
		else//询问操作
		{
			tmpcnt=query(q[i].l,q[i].r,1);//查询询问区间中比mid大的数的个数
			if(tmpcnt<q[i].v)//当前排名比询问排名小,要等加入更多修改后再处理
			{
				q[i].v-=tmpcnt;//下次询问的时候就可以忽略已有的比它大的数,只需要找到不够的比他大的数
				q1[++lcnt]=q[i];//需要更多的当前没有贡献的修改操作,放入左侧
				queryl=1;
			}
			else q2[++rcnt]=q[i],queryr=1;//当前有的比它大的数字已经多于询问的排名了,放入右侧 
		}
	}
	for(int i=1;i<=rcnt;++i)
		if(q2[i].opt==1) modify(q2[i].l,q2[i].r,-1,1);//还原线段树的修改操作,方便继续递归下次使用 
	for(int i=ql;i<ql+lcnt;++i) q[i]=q1[i-ql+1];//把询问分成两堆放回原数组中 
	for(int i=ql+lcnt;i<=qr;++i) q[i]=q2[i-ql-lcnt+1];
	if(queryl)solve(l,mid,ql,ql+lcnt-1);//右边的修改操作贡献未统计,询问操作还需要更多的修改 
	if(queryr)solve(mid+1,r,ql+lcnt,qr);//左边的修改操作贡献已统计,询问操作不需要更多的修改 
}

signed main()
{
    n=in,m=in;
    build(1,n,1);//建立维护区间和线段树 
    for(int i=1,opt,l,r,c;i<=m;++i)
    {
		opt=in,l=in,r=in,c=in;
		q[i]=(Query){opt,l,r,(opt==2)?++qnum:0,c};//读入操作
	}
	solve(-n,n,1,m);//前两个参数为值域下界上界,后两个参数为操作序列下标起点和终点
	for(int i=1;i<=qnum;++i) write(Ans[i],'\n');//打印答案
	return 0;
}

小结

相信看到这里你应该理解了整体二分的大致思想了,整体二分就是通过同时进行多个操作的二分,在优秀的时间复杂度下解决问题,看到可离线操作,可二分找答案,贡献可叠加,修改之间相互不影响就能尝试使用二分了

可以尝试一些整体二分练习题:

Luogu P2617 [ZJOI2013]Dynamic Rankings 带插入删除区间第 \(k\)

Luogu P1527 [国家集训队]矩阵乘法 二维情况


该文为本人原创,转载请注明出处

博客园传送门

posted @ 2021-11-12 19:16  人形魔芋  阅读(1756)  评论(4编辑  收藏  举报