整体二分学习笔记

1. 简介

在一些题目中,可能存在一些题目,对于每次询问直接二分可能会TLE,此时就要用到整体二分

整体二分是一种离线的方法,适用于如下情况:

  1. 询问答案具有可二分性

  2. 修改对判定答案的贡献相互独立,修改之间互不影响效果

  3. 修改如果对判定答案有贡献,则该贡献是一个确定的与判定标准无关的值

  4. 贡献满足交换律,结合律,具有可加性

  5. 题目允许使用离线算法

2. 思想

记[l,r]为答案的值域,[L,R]为答案的定义域,即求解时仅考虑下标[L,R]内的操作和查询,这其中询问的答案在[l,r]里

  1. 将所有操作按时间排序存入数组,然后开始分治

  2. 在分治的过程中,利用数据结构(通常是树状数组)记录当前查询答案与mid之间的关系

  3. 根据查询出来的答案与mid的关系(小于等于和大于)将当前的操作序列分为\(q_1\)\(q_2\)两部分,分别递归处理

  4. 当l==r时,找到答案,记录并返回即可

需要注意,在整体二分的过程中,若当前处理的值域为[l,r],如果某个询问的最终答案的范围不在[l,r]中,则会在其他时候处理

3. 具体过程

3.1. 查询全局第k小

可以分别对每一个序列二分,但是也可以将所有询问放在一起二分

从二分的本质考虑,假设要猜一个[l,r]范围内的数,可以先猜\(\frac{l+r}{2}\),然后根据询问的结果调整查询范围

所以,在有多次询问时,可以先全猜答案是mid,然后将所有询问分为2类,分别处理

3.2. 查询区间第k小

涉及给定区间的查询,直接二分并用check函数判断大小,时间复杂度会很大

考虑值域中点mid与询问的关系,如果区间内小于等于mid的数有t个,询问的是第k小,如果\(k\le t\),答案应小于等于mid,否则大于mid

此时需记录一个区间小于等于指定数的个数,即单点修改,区间求和,可以用树状数组

为提高效率,只对数列中值在值域区间 [l,r] 的数进行统计,即,在进一步递归之前,不仅将询问划分,将当前处理的数按值域范围划为两半。

3.3. 带修区间第k小

3.3.1. 例题

Dynamic Rankings

https://gxyzoj.com/d/gxyznoi/p/P26

题目描述

给定一个含有 \(n\) 个数的序列 \(a_1,a_2 \dots a_n\),需要支持两种操作:

  • Q l r k 表示查询下标在区间 \([l,r]\) 中的第 \(k\) 小的数
  • C x y 表示将 \(a_x\) 改为 \(y\)
输入格式

第一行两个正整数 \(n,m\),表示序列长度与操作个数。
第二行 \(n\) 个整数,表示 \(a_1,a_2 \dots a_n\)
接下来 \(m\) 行,每行表示一个操作,都为上述两种中的一个。

输出格式

对于每一次询问,输出一行一个整数表示答案。

提示

【数据范围】

对于 \(10\%\) 的数据,\(1\le n,m \le 100\)
对于 \(20\%\) 的数据,\(1\le n,m \le 1000\)
对于 \(50\%\) 的数据,\(1\le n,m \le 10^4\)
对于 \(100\%\) 的数据,\(1\le n,m \le 10^5\)\(1 \le l \le r \le n\)\(1 \le k \le r-l+1\)\(1\le x \le n\)\(0 \le a_i,y \le 10^9\)

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;
}
posted @ 2024-05-04 18:00  wangsiqi2010916  阅读(8)  评论(0编辑  收藏  举报