二分,三分,整体二分

(我闲着没事干写这个干什么)(主要是看了整体二分然后心血来潮想补一下)(好了当天下午考了个三分函数极值卡精度了)

一类思想:二分法。

二分查找

顾名思义,二分一个元素的位置然后查找。前提是数组有序。这样我们可以每次二分中间的元素,若需要查询的元素比它小则把右端点跳到中间,反之跳左端点。

举个例子,查找有序数组内x的排名。

int find(int x){
	int l=0,r=n;
	while(l<r){
		int mid=(l+r)>>1;
		if(a[mid]<x)l=mid+1;
		else r=mid;
	}
	return l;
}

然后stl也自带二分查找:lower_bound和upper_bound,分别查找\(\ge x\)的第一个元素和\(>x\)的第一个元素。格式为:lower_bound(头指针,尾指针,元素)。

lower_bound(a+1,a+n+1,x);//查找大于等于x的最小元素 返回它的值 
upper_bound(a+1,a+n+1,x);//查找大于x的最小元素 返回它的值 

二分答案

这个有时候会在意想不到的地方用到。通常来说,如果我们要求的答案满足单调性,就可以通过二分答案的值来判断。(有的时候这个东西真的不好看出来,比如这个这个

但是刚开始我觉得应该整个比较显然的题来说明。比如跳石头这个题,跳的最短距离越大,移除的石头数量就越多。再比如这个题,高度越高砍的就越少。

所以二分答案有什么用处呢?我们可以将一个求值问题转化为判定问题,也就是判断我们当前二分的值是否合法。根据计算机理论,它往往要更简单。

比如跳石头,我们每次二分要移除多少石头,然后贪心地移除石头,也就是每次有两块石头距离不超过我们二分的值的时候就移除,最后和\(M\)比较。

bool check(int x){
	int cnt=0,d=0;
	for(int i=0;i<n;i++){
		if(a[i]-d<x)cnt++;
		else d=a[i];
	}
	if(cnt<=m)return true;
	return false;
}
while(l<r){
	int mid=(l+r+1)>>1;
	if(check(mid))l=mid;
	else r=mid-1;
}

三分法

单峰函数的极值也可以通过二分法衍生的三分法求出。也就是这个题

具体的,对于\([l,r]\)内的一个单峰函数(就像二次函数一样的,这里拿\(y=x^2\)举例子),我们随便在上面取两个点\(lmid<rmid\),如果\(f(lmid)<f(rmid)\),则最大值必然不在\([rmid,r]\)区间内(因为这一段肯定单调递增,具体的可以尝试画个图象分讨)。反之亦然。如果相等,随便舍掉一段。

我们发现,我们每次舍去\([l,lmid]\)\([rmid,r]\)中的一段,所以为减少操作次数,我们将两个数设置为\(mid\pm \epsilon\),其中\(\epsilon\)就是你平时设置的double类型里的那个误差,小于它就是0的那个。

然后是上面那个题的代码。(这题的题解一大群科技)

#include <bits/stdc++.h>
using namespace std;
const double eps=1e-6;
double a[15],l,r;
int n;
double f(double x){
	double ans=0;
	for(int i=n;i>=0;i--)ans=ans*x+a[i];//一个小的科技 
	//具体的可以看我之前简单多项式求值的题解 
	//稍微讲讲就是累积每项贡献 
	return ans;
}
int main(){
	scanf("%d%lf%lf",&n,&l,&r);
	for(int i=n;i>=0;i--)scanf("%lf",&a[i]);
	while(fabs(r-l)>=eps){
		double mid=(l+r)/2,lmid=mid-eps,rmid=mid+eps;
		if(f(lmid)<=f(rmid))l=mid;//这个题是个上凸的所以反过来 
		else r=mid;
	}
	printf("%.5lf",l);
	return 0;
}

整体二分

首先我们考虑一般的二分。二分的经典问题就是第\(k\)小。所以拿第\(k\)小举例子。
(所以说接下来的所有东西不要跟我讲排序否则右上角谢谢)

  1. 单次询问数列中第\(k\)小。

很简单,每次二分值域中点\(mid\),判断有多少数小于\(mid\)然后取一段区间。

  1. 给你一大堆询问数列中第k小。

显然我们可以每个二分。当然询问多了就会t掉。所以我们考虑一种把所有问题一次性处理的方法:整体二分(所以它是离线的)。

具体地,我们二分值域中点\(mid\),然后顺序扫描询问并将询问划成两块:答案小于\(mid\)的和答案大于\(mid\)的。对于答案小于mid的,可以直接递归向下处理。答案大于mid的,需要将它们要查询的值减去mid的排名(可以类比权值线段树或者主席树解决第\(k\)小的部分)然后递归向下处理。递归边界当然是\(l=r\),直接对所有该区间内的询问统计答案。

把oi-wiki的代码粘下来了顺便加了点注释。

void solve(int l, int r, vector<Query> q) {//q是当前的询问集合 l r是询问的值域区间 
  int m = (l + r) / 2;
  if (l == r) {
    for (unsigned i = 0; i < q.size(); i++) ans[q[i].id] = l;//递归边界 直接统计答案 
    return;
  }
  vector<int> q1, q2;//将询问分成两组 
  for (unsigned i = 0; i < q.size(); i++)
    if (q[i].k <= check(m))
      q1.push_back(q[i]);//如果小于m的排名则进入左半 
    else
      q[i].k -= check(m), q2.push_back(q[i]);//反之减去m的排名并进入右半 
  solve(l, m, q1), solve(m + 1, r, q2);//递归向下分治处理 
  return;
}
  1. 静态区间第k小(也就是主席树板子

当然你可以主席树做。事实上洛谷上这几个带整体二分标签的紫题都可以主席树。

我们还是考虑二分值域。但是我们显然没办法一边查出来所有区间中\(mid\)的排名。于是我们可以尝试用一点数据结构。此时,我们需要查出所有区间中\(mid\)的排名,转化一下就是比\(mid\)小的数有多少个。想到了什么?偏序关系!树状数组!

具体地说,我们可以把所有数组中的数抽象成修改操作,和询问一起整体二分。由于所有修改都是在询问之前且顺序是稳定的,所以无论怎样都是先扫描到修改再扫描到询问。这样,我们先把所有比\(mid\)小的数在它的位置插入树状数组,然后查询的时候可以直接找到\([l,r]\)内比\(mid\)小的数的个数,然后套用板子就行。

struct ques{
	int l,r,k,id,type;
	//对于修改 type=0 l为位置 r为值 k=1
	//对于询问 type=1 l r为区间 k为k小 id为第几个询问 
}q[400010],q1[400010],q2[400010];
void solve(int l,int r,int ql,int qr){
	if(ql>qr)return;
	if(l==r){
		for(int i=ql;i<=qr;i++){
			if(q[i].type==1)ans[q[i].id]=l;
		}
		return;
	}
	int mid=(l+r)>>1,cnt1=0,cnt2=0;
	for(int i=ql;i<=qr;i++){
		if(q[i].type==1){
			int x=query(q[i].r)-query(q[i].l-1);//查询区间内小于等于mid的数的个数 
			if(q[i].k<=x)q1[++cnt1]=q[i];//不超过mid划到左边 
			else{
				q[i].k-=x;q2[++cnt2]=q[i];//超过mid减去mid划到右边 
			}
		}
		else{
			if(q[i].r<=mid){
				update(q[i].l,q[i].k);//小于mid更新并划到左边(因为只有左边才会对小于mid的询问有贡献)
				q1[++cnt1]=q[i];
			}
			else q2[++cnt2]=q[i];//大于mid直接划到右边(因为右边小于mid的数的贡献已经在上边减掉了 所以只需要考虑大于mid的数) 
		}
	}
	for(int i=1;i<=cnt1;i++){
		if(q1[i].type==0)update(q1[i].l,-q1[i].k);//清空树状数组 
		q[i+ql-1]=q1[i];
	}
	for(int i=1;i<=cnt2;i++)q[i+cnt1+ql-1]=q2[i];//合并到原数组中 
	solve(l,mid,ql,cnt1+ql-1);
	solve(mid+1,r,cnt1+ql,qr);
}
int main(){
		scanf("%d%d",&n,&m);
		for(int i=1;i<=n;i++){
			scanf("%d",&a[i]);
			q[++t]={i,a[i],1};
		}
		for(int i=1;i<=m;i++){
			int l,r,k;scanf("%d%d%d",&l,&r,&k);
			q[++t]={l,r,k,i,1};
		}
		solve(-1e9,1e9,1,t);
		for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
	return 0;
}
  1. 动态区间第k小(也就是Dynamic Rankings

首先这个题我的树套树题库能过然后洛谷上t了一半 然后整体二分洛谷过了题库快了5倍。树状数组套主席树还没打。

修改其实我们之前也处理过了,不过是所有修改在询问之前。这次我们可以借鉴上个题的经验。

我们仍然把原数组抽象成修改。不过新增的修改操作要拆成两个操作:删除和插入。这样我们的树状数组才能正常维护,也就是删掉旧的数并加上新的数。我们同样没有排过序,所以数组顺序仍然是稳定的。因此我们仍然扫描到哪个就对哪个操作而不是先操作修改后操作查询。

就不写注释了,和上边一个的solve部分是一模一样的。

struct ques{
	int l,r,k,id,type;
	//询问同上 
	//修改时删除则k=-1 插入则k=1 
}q[400010],q1[400010],q2[400010];
void solve(int l,int r,int ql,int qr){
	if(ql>qr)return;
	if(l==r){
		for(int i=ql;i<=qr;i++){
			if(q[i].type==1)ans[q[i].id]=l;
		}
		return;
	}
	int mid=(l+r)>>1,cnt1=0,cnt2=0;
	for(int i=ql;i<=qr;i++){
		if(q[i].type==1){
			int x=query(q[i].r)-query(q[i].l-1);
			if(q[i].k<=x)q1[++cnt1]=q[i];
			else{
				q[i].k-=x;q2[++cnt2]=q[i];
			}
		}
		else{
			if(q[i].r<=mid){
				update(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==0)update(q1[i].l,-q1[i].k);
		q[i+ql-1]=q1[i];
	}
	for(int i=1;i<=cnt2;i++)q[i+cnt1+ql-1]=q2[i];
	solve(l,mid,ql,cnt1+ql-1);
	solve(mid+1,r,cnt1+ql,qr);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		q[++t]={i,a[i],1};
	}
	for(int i=1;i<=m;i++){
		ans[i]=0;
		char od[2];scanf("%s",od);
		if(od[0]=='Q'){
			int l,r,k;scanf("%d%d%d",&l,&r,&k);
			q[++t]={l,r,k,i,1};
		}
		else{
			int x,y;scanf("%d%d",&x,&y);
			q[++t]={x,a[x],-1};
			a[x]=y;
			q[++t]={x,a[x],1};
		}
	}
	solve(0,1e9,1,t);
	for(int i=1;i<=m;i++){
		if(ans[i]!=0)printf("%d\n",ans[i]);
	}
}

洛谷上的整体二分大多数都是\(k\)小值这样的板子。比如这个就搞个二维前缀和然后套个板子就行了。这个要区间插入当然可以线段树。这个二分射第几颗子弹时木板会碎就变成了蓝题。这个稍微特殊一点,有两维,不过首先将所有果汁按价格排序然后贪心就行了。

posted @ 2022-09-03 19:08  gtm1514  阅读(34)  评论(0编辑  收藏  举报