!二分算法学习!(洛谷官方算法题单:二分查找与二分答案)

作者上次忘搞专栏了,所以重新搞了一次(狗头见谅)

写在前面

Hi,小伙伴们,I am coming🌹。算法是一门必修课,也是计算机程序的基础。神犇们虽说已是轻舟已过万重山,但也可以留下来回忆回忆,重温一下,提供宝贵的经验与见解🌹🌹。还在学习算法的小伙伴们也别急,静下心来,学习算法,扩宽思维,体会算法的“优美”🌹。

/*想看题解的小伙伴直接点击目录,选择题目即可*/

/*这篇题解只讲思路,不会太详细,但该讲的都会讲,我相信小伙伴都有实力,实力强大👍👍*/

二分

二分可以分为 二分查找&&二分答案。

二分查找是对问题的一种快速搜索,容易理解,主要作用——大大降低时间复杂度。

二分答案呢,是在(假定)已知道答案的基础上,对题目进行验证,即化求解为判定,降维打击,极大程度降低题目难度 

ps:一般二分答案的外皮只求结果,不问过程

     /*一道题正着来时间复杂度不仅高还难写,但带着答案(假定)验证简单得多(但是小伙伴们要注意啊,现在是逆着来,思路也该换换了。是化求解为判定,可能有些小伙伴们就卡住了,作者当初也卡这里了,思路没有立马换过来😂)*/

通俗一点  就是选择题带选项进行验证,只不过现在验证的手段换成了二分查找

现在来说说 二分查找(int||float)的模板:

(妈妈再也不用担心我为边界问题而困惑了)

先是int模板

&1 check(mid) 你可以抽象成 函数,也可以看作 判定条件 

/*作者一般是当作if的判定条件使用的,小伙伴们按照个人喜好来写即可*/
&2 >>1 相当于 /2

&模板1:

当我们将区间 [l,r] 划分成[l,mid] 和 [mid + 1,r] 时,其更新操作是 r = mid 或者l = mid + 1,计算 mid 时不需要加 1

while(l<r)
{
	mid=(l+r)>>1;
	if(check(mid)) r=mid;
	else l=mid+1;
}

你可以理解为这是一个往左找答案的(建议不是很懂的小伙伴可以停下来思考思考,还是不懂跳到例1

&模板2:

当我们将区间[l,r] 划分成 [l,mid -1 ] 和[mid,r] 时,其更新操作是 r = mid - 1或者I = mid;,此时为了防止死循环,计算 mid 时需要加 1

(理解理解,理解为重,作者写的时候也是在重温的

while(l<r)
{
	mid=(l+r+1)>>1;
	if(check(mid)) l=mid;
	else r=mid-1;
}

同样的,可以理解为往右找答案

最后是float模板

这就简单的多了,直接上操作

&模板3:

while(r-l>1e-5)//精度问题 
{
	mid=(l+r)/2;
	if(check(mid)) r=mid;
	else l=mid;
}

/*模板里的if-else都是可以换的,小伙伴们想怎么来怎么来*/

其实一般情况下  l,r  选取是无所谓的,相对差距大即可(其他情况依题而定)

但是还是建议     lr   按     题目给定区间选取,否则可能有1-2点wa摸不着头脑

由于if(check(mid))过于抽象,现在先来一题助小伙伴们理解

二分查找例题:

例1:P1102 A-B 数对

例1:P1102 A-B 数对

思路非常清晰,找到左右出现位置即可

法一(模板1&&2):

#include <iostream>
#include <algorithm>
using namespace std;

int a[200005];
int main()//1102
{
	long long count=0;//有一个测试点count很大	
	int i,n,k,l,r,mid,com;
	cin>>n>>k;
	for(i=0;i<n;i++) cin>>a[i];
	sort(a,a+n); 
	for(i=0;i<n-1;i++)//n-1 防止越界 
	{
		r=n-1;//防止越界 
		l=0;//每次进来都要变为0 
		while(l<r)//左
		{
			mid=(l+r)>>1;
			if(a[mid]>=a[i]+k) r=mid;//大于等于 如果不等于找不到最左 
			else l=mid+1;
		}//出来后l==mid==r 
		if(a[l]!=a[i]+k) continue;//直接跳过 
		com=l;
		l--;
		r=n-1;
		while(l<r)//右 
		{
			mid=(l+r+1)>>1;
			if(a[mid]<=a[i]+k) l=mid;//同样的等于 否则r会退 
			else r=mid-1;
		}
		count+=r-com+1;
	}
	cout<<count;
	return 0;
}

法二(map):

#include<map>  是map的头文件

使用为:
键                        映射
数据类型             数据类型       

例: map<int ,string>   键-映射初值都是空(可以理解为   空格 || 0    )

#include<iostream>
#include<map>
using namespace std;

int main()
{
	long long count=0,a[200005];
	int i,x,k;
	map<int,int> b;
	cin>>x>>k;
	for(i=0;i<x;i++) 
	{
		cin>>a[i];
		b[a[i]]++;//a[i]键 b[a[i]]映射 
	}
	for(i=0;i<x;i++) count+=b[a[i]+k];
	cout<<count;
	return 0;
}

法三( lower_bound && upper_bound ):

lower_bound && upper_bound   的头文件是   algorithm

lower_bound 是对排序后数组第一个可插入且不改变单调性,且返回的是地址

upper_bound  即最后一个

/*用法自行体会(狗头)*/

#include<iostream>
#include<algorithm>
using namespace std;

long long a[200005],mycount=0,i,x,k;
int main()//1102
{
	cin>>x>>k; 
	for(i=0;i<x;i++) cin>>a[i];
	sort(a,a+x);
	for(i=0;i<x;i++) mycount+=upper_bound(a,a+x,a[i]+k)-lower_bound(a,a+x,a[i]+k);
	cout<<mycount;
	return 0;
}//当count是全局变量时会报错

现在请小伙伴们重温一下模板,再次理解,即将开始  ‘攻城掠地’

例2:P2249 【深基13.例1】查找

例2:P2249 【深基13.例1】查找

思路:左

(纯纯模板题,小伙伴们肯定都会)

#include<iostream>
using namespace std;

int a[1000005];
int main()
{
	int n,m,i,com,l,r,mid;
	cin>>n>>m;
	for(i=1;i<=n;i++) cin>>a[i];
	for(i=0;i<m;i++)
	{
		cin>>com;
		l=1;
		r=n;
		while(l<r)
		{
			mid=(l+r)/2;
			if(a[mid]>=com) r=mid;
			else l=mid+1;
		}
		if(a[l]==com) cout<<l<<" ";
		else cout<<"-1 ";
	} 
	return 0;
}

例3:P1678 烦恼的高考志愿

例3:P1678 烦恼的高考志愿

思路:贪心判断+  左右二分  ||  algorithm库函数

#include <algorithm>
using namespace std;


int a[100005];
int main()
{
	int m,n,i,k;
	long long ans=0;
	cin>>m>>n;
	for(i=1;i<=m;i++) cin>>a[i];
	sort(a+1,a+m+1);
	a[0]=-5000000,a[m+1]=5000000;//这是唯一注意点,也是最大错因
	for(i=1;i<=n;i++)
	{
		cin>>k;
		if(a[lower_bound(a+1,a+m+1,k)-a]-k<k-a[lower_bound(a+1,a+m+1,k)-a-1]) ans+=a[lower_bound(a+1,a+m+1,k)-a]-k;
		else ans+=k-a[lower_bound(a+1,a+m+1,k)-a-1];
	}
	cout<<ans;
	return 0;
}

以上这些就是洛谷官方提单二分算法的二分查找例题了,对小伙伴们来说都是洒洒水的事👍

下面进入二分答案的环节

二分答案例题:

前面已经说过——二分答案就是选择题带选项进行验证,只不过现在验证的手段换成了二分查找。前面也已经说过,二分答案题具有以下几个特征:

&1 顺着麻烦,逆着简单(现在是验证,换换思路)

&2 只求结果,不问过程

&3 问题所求,最大最小

至于模板  1||2  ,最小值最大就是尽量往右,最大值最小就是尽量往左

废话不多说,上题 

int二分答案

例4:P1873 [COCI 2011/2012 #5] EKO / 砍树

例4:P1873 [COCI 2011/2012 #5] EKO / 砍树

法一(数学):

这个主体时间复杂度是小于o(n)的,我看到这题第一反应就是正着来,因为很容易想到。不过二分答案不用想多少的(捂脸),“暴力”即可……

思路:排序后高树被砍掉部分大于等于所需即可,但是树高是整数,所以h是浮点数的话要 --

#include <iostream>
#include <algorithm>
using namespace std;

long long a[1000005],x,i,h;//数组太大放外面 
int main()//1873
{
	long long ans=0,m;
	cin>>x>>m;
	for(i=0;i<x;i++) cin>>a[i];
	sort(a,a+x);
	for(i=x-1;i>=0;i--)
	{
		ans+=a[i];
		if(ans-a[i-1]*(x-i)>=m)
		{
			h=a[i]-(m-(ans-a[i]*(x-i)))/(x-i);
			if((m-(ans-a[i]*(x-i)))%(x-i)!=0) h--;
			cout<<h;
			break; 
		}
	}
	return 0;
}
法二:

思路:右(最大右逼近)+验证(如果一个数是答案,那么如果      大于的树减去答案的部分  的和  就是所需木材

验证主体内  不能等于mid,不懂的小伙伴们先思考,下面的例题一起讲清楚

因为是往右逼近,所以if内ans含=(一般情况下都是含等于的,还没遇到不等于的大概也许是作者太蒟蒻了……)(欢迎小伙伴们和神犇们提高见)

#include <iostream>
#include <algorithm>
using namespace std;

long long a[1000005],x,i,h;
int main()//1873
{
	long long ans,m,l=0,r,mid;
	cin>>x>>m;
	for(i=0;i<x;i++) cin>>a[i];
	sort(a,a+x);
	r=a[x-1];
	while(l<r)
	{
		mid=(l+r+1)>>1;
		ans=0;
		for(i=0;i<x;i++)
		{
			if(a[i]>mid) ans+=a[i]-mid;
		}
		if(ans>=m) l=mid;
		else r=mid-1;
	}
	cout<<l;
	return 0;
}

例5:P2440 木材加工

例5:P2440 木材加工

思路:右(最大右逼近)+验证(如果一个数是答案,那么如果     每棵数除与答案的整数部分     的和     就是所需段数

因为是往右逼近,所以if内ans含=

#include<iostream>
using namespace std;

int main()//2440
{
	long long a[100005],ans,mid,k,l=0,r=100000000;
	int n,i;
	cin>>n>>k;
	for(i=0;i<n;i++) cin>>a[i];
	while(l<r)
	{
		ans=0;
		mid=(l+r+1)>>1;
		for(i=0;i<n;i++) ans+=a[i]/mid;
		if(ans>=k) l=mid;
		else r=mid-1;
	}
	cout<<r;
	return 0;
}

例6:P2678 [NOIP2015 提高组] 跳石头

例6:P2678 [NOIP2015 提高组] 跳石头

思路:右(最大右逼近)+验证

(回应上面的验证主体内等于问题:)

if(a[i]-a[com]<mid) count++;//不加等于,如果等于了,r就会变相往左了,此时不能保证右逼(也即假定有更大的答案)但是if内count还是要等于的
else com=i;(验证主体)

如果一个数是答案,并且两个石头的距离小于这个数——移,否则更新石头。然后count++,判定  count与给定移走数   的差距

#include<iostream>
using namespace std;

int main()
{
	long long a[50005],l=0,r=1000000000,mid;
	int len,n,m,i,count,com;
	cin>>len>>n>>m;
	for(i=1;i<=n;i++) cin>>a[i];
	a[0]=0;
	a[n+1]=len;
	if(n==0)//不用移石头,直接输出
	{
		cout<<len;
		return 0;
	}
	while(l<r)
	{
		mid=(l+r+1)>>1;
		count=0;
		com=0;
		for(i=1;i<=n+1;i++)
		{
			if(a[i]-a[com]<mid) count++;
			else com=i;
		}
		if(count<=m) l=mid;
		else r=mid-1;
	}
	cout<<r;
	return 0;
}

例7:P3853 [TJOI2007] 路标设置

例7:P3853 [TJOI2007] 路标设置

这题……先前是绿的,现在是降级了(不过也确实,和例6差不多)

以及这题新增一个测试点

2 2 1
0 2

导致法二错这里

法二(与跳石头差不多,路标增):

思路:右(最大右逼近)+验证

while(com>mid)//是大于 不能含等于 
 {
         count++;
         com-=mid;
 }(验证主体)

如果一个数是答案,那么如果   相邻两个路标差    大于这个数   就要设置路标,然后更新路标差。count++   然后判定count与给定路标差距

验证部分可以写成除法,但是  和com不能等于mid 一样,当除法结果是整数时  --  (同例6)

#include<iostream>
using namespace std;

int a[100005];
int main()//3853
{
	int len,n,k,now,before,i,l=0,r,com,mid,count;
	cin>>len>>n>>k;
	for(i=0;i<n;i++) 
	{
		cin>>now;
		if(i>0) a[i]=now-before;
		before=now;
	}
	r=len;
	while(l<r)
	{
		mid=(l+r)>>1;
		if(mid==0)//mid==0卡死 
		{
			cout<<"1";
			return 0;
		}
		count=0;
		for(i=1;i<n;i++)
		{
			com=a[i];
			while(com>mid)//是大于 不能含等于 
			{
				count++;
				com-=mid;
			}
		}
		if(count<=k) r=mid;
		else l=mid+1;
	}
	cout<<l;
	return 0;
}
法一(路标减):

思路:右(最大右逼近)+验证  (自行体会(狗头))

            if(a[i]-com<=mid) com=a[i];//含等于 
            else
            {
                com+=mid;
                i--;//  --后++还是等于本身 
                count--;
            }(验证主体)

至于为什么含等于,现在给定一组数据

17 4 1

0 12 14 17

但 mid==6 时,你会发现count只是减少了一次,为什么?因为  if内是更新当前路边位置啊(同例6),作者写出这种方法就是想提醒你   验证主体内 = 不能乱来

#include<iostream>
using namespace std; 

int a[100005];
int main()//3853
{
	int len,n,k,i,l=0,com,r,mid,count;
	cin>>len>>n>>k;
	for(i=0;i<n;i++) cin>>a[i];
	r=len;
	while(l<r)
	{
		mid=(l+r)>>1;
		count=k;
		com=0;
		for(i=1;i<n;i++)
		{
			if(count<0) break;
			if(a[i]-com<=mid) com=a[i];//含等于 
			else
			{
				com+=mid;
				i--;//  --后++还是等于本身 
				count--;
			}
		}
		if(count>=0) r=mid;
		else l=mid+1;
	}
	cout<<l;
	return 0;
}

例8:P1182 数列分段 Section II

例8:P1182 数列分段 Section II

思路:左(最小左逼近)+验证

害怕了吧我的小伙伴,l=0第4个点wa

先看数据   作者这里也错了l=0(捂脸),还是看讨论板和题解知道的

5 4

1 2 3 4 5

易知答案为5,但是如果l=0,输出的是3(虽然也是被分成4段)显然不行,understand?

所以还是   建议  lr 按照题目选取   作者也会改正(狗头)

至于验证就是:如果一个数是答案,那么如果     这一段的和   加上下一段的一个数    的和    就会大于答案(理解成这样即可,贪心法理解),count++(分段),判定count

#include<iostream>
using namespace std;

int main()
{
	long long r=0,sum,l=0,mid,a[100005];
	int n,m,i,count;
	cin>>n>>m;
	for(i=0;i<n;i++) 
	{
		cin>>a[i];
		r+=a[i];
		if(a[i]>l) l=a[i];
	}
	while(l<r)
	{
		mid=(l+r)>>1;
		sum=0;
		count=0;
		for(i=0;i<n-1;i++)
		{
			sum+=a[i];
			if(sum+a[i+1]>mid)
			{
				sum=0;
				count++;
			}
		}
		if(count<=m-1) r=mid;//加上最后一段 
		else l=mid+1;
	}
	cout<<l;
	return 0;
}

float二分答案

终于没有麻烦的左右判定了🌹,也终于可以交给小伙伴自己了👍

例9:P1024 [NOIP2001 提高组] 一元三次方程求解

例9:P1024 [NOIP2001 提高组] 一元三次方程求解

思路:零点定理(麻烦一点点的浮点数二分答案模板题)

值得一提的点就是     if(fun(mid)*fun(r)>=0) r=mid;  (写成l也是可以的)

#include<iostream>
using namespace std;

double a,b,c,d;
double fun(double x)
{
	return a*x*x*x+b*x*x+c*x+d;
};
int main()
{
	double l,r,mid,llim,rlim;
	int i,res=0;
	cin>>a>>b>>c>>d;
	for(i=-100;i<101;i++)
	{
		l=i,r=i+1;
		llim=fun(l);
		rlim=fun(r);
		if(llim==0.0)
		{
			printf("%0.2lf ",l);
			res++;
		}
		if(llim*rlim<0.0)
		{
			while(r-l>1e-5)
			{
				mid=(l+r)/2;
				if(fun(mid)*fun(r)>=0) r=mid;
				else l=mid;
			}
			printf("%.2lf ",l);
			res++;
		}
		if(res==3) return 0;
	}
	return 0;
}

例10:P1163 银行贷款

例10:P1163 银行贷款

思路:(学理没有公式动不了一点)

公式:100/(1+0.029)^1+……+100/(1+0.029)^12=1000

#include<iostream>
#include<math.h>
using namespace std;

double x,m,n,l=0.0,r=3.0,mid;
double fun(double y)
{
	double ans=0;
	int i;
	for(i=1;i<=n;i++) ans+=1/pow(1+y,i);
	return x/m-ans;
}
int main()
{
	cin>>x>>m>>n;
	while(r-l>1e-5)
	{
		mid=(l+r)/2;
		if(fun(mid)>=0) r=mid;
		else l=mid;
	}
	printf("%0.1lf",l*100);
	return 0;
}

例11:P3743 kotori的设备

例11:P3743 kotori的设备

思路:特判+贪心验证

注意:这题是浮点答案,不是以1秒为单位的。当然也是这个点,使得贪心验证法简单。

if(b[i]-a[i]*mid<0) sum+=(a[i]*mid-b[i]);//需要充电的  (验证主体)

至于特判很容易理解,所有设备用电量小于等于供电量即可。

如果一个数是答案,那么如果    每个不能在答案时间内   自给自足   的设备都需要充电。然后判定     这些设备在这个答案时间内所需充电的总和    与    供电设备*答案时间       的差距即可

#include<iostream>
using namespace std;

int a[100005],b[100005];
int main()
{
	int n,p,i;
	double sum=0,mid,l,r=1e10;
	cin>>n>>p;
	for(i=0;i<n;i++)
	{
		cin>>a[i]>>b[i];
		sum+=a[i];
	}
	if(sum<=p)
	{
		cout<<"-1";
		return 0;
	}
	while(r-l>1e-6)
	{
		mid=(l+r)/2;
		sum=0;
		for(i=0;i<n;i++)
		{
			if(b[i]-a[i]*mid<0) sum+=(a[i]*mid-b[i]);//需要充电的 
		}
		if(sum>=p*mid) r=mid;
		else l=mid; 
	}
	cout<<l;
	return 0;
}

此至,小伙伴们11题全部AC🌹

最后

之前见过一个动图,情况是这样:里面有个写着算法的海,有个人在沙滩上往大海跑,跑着跑着被一块写着二分的石头绊倒了(这个动图作者应该是想表达对二分学习过程的痛苦吧,和作者曾经挺相似的(捂脸))

神犇和小伙伴们对此文章有任何高见都可以提,毕竟作者只是个蒟蒻(大哭)

二分呢,本人觉得吧,是基础算法里比较难搞的一个吧(还是那句话,算法一路——长路漫浩浩),当然我相信小伙伴们实力强大,功力深厚……

写完这篇题解,作者也是大彻大悟了,对二分算法看的多了一点。当然目光还是要往前的,二分也不能落,算法一路——长路漫浩浩啊。

期待下一次与你的相遇……

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值