浅说二分答案

前言

前面我们讲的主要都是二分查找,二分查找在考试中一般都不会作为独立算法而存在,一般都是一道题中部分算法,它主要是基于单调性的减少枚举次数。而枚举除了枚举操作还可以枚举答案,枚举答案同样可以用二分来减少枚举次数,这就是比赛中常考的二分答案。很多时候当答案不是很容易直接计算的时候,我们可以考虑枚举答案,但是答案范围特别大,但是又具有单调性的时候,直接一个一个枚举答案效率很低,我们可以考虑二分枚举,如果答案有10亿种,那么二分枚举只需要30次左右就可以枚举完,大大提高了枚举效率。

内容分析------整数二分

情况一:最小值最大

我们还是拿一个例题来分析

陶陶是个贪玩的孩子,他在地上丢了A个瓶盖,为了简化问题,我们可以当作这A个瓶盖丢在一条直线上,现在他想从这些瓶盖里找出B个,使得距离最近的2个距离最大,他想知道,最大可以到多少呢?

输入格式:

第一行,两个整数,A,B。(B<=A<=100000)

第二行,A个整数,分别为这A个瓶盖坐标。<=10^9^

因为每个瓶盖的坐标未定,所以找不到任何算法可以直接得到最小距离的最大值,所以我们只能一个答案一个答案的去试,已知A[i]<=1e9,所以一次枚举答案肯定会超时,已知坐标是整数,并且坐标有一个区间,区间是具有单调性的,所以我们可以用二分查找

第一步:

题目中没说每个瓶盖的坐标是已经排好序的,所以先排序

cpp 复制代码
sort(a+1,a+1+n,cmp);

第二步:

确定二分答案的左右端点,即最小可能距离,最大可能距离

cpp 复制代码
int l=1,r=a[n]-a[1];

第三步:

查找中间值,然后带入题目中判断是否满足条件。假设最小值就是当前这个值,说明选取的瓶盖的距离一定是>=最小值,那么我们就从起点开始往后面找,只要距离>=最小值,就选取,然后统计最后一共可以选k个。

注意第一个点可以理解为肯定是必选的

既然是假设,那么就可能有不满足条件的时候:

如果 k < m k<m k<m:说明满足条件的瓶盖少了,即最小值大了。所以 r i g h t = m i d − 1 ; right=mid-1; right=mid−1;

如果 k = m k=m k=m:说明刚好满足条件,最小值是正确的。

如果 k > m k>m k>m:说明满足条件的瓶盖多了,即最大值小了。所以 l e f t = m i d + 1 ; left=mid+1; left=mid+1;

实际上前面的想法是错误的,因为我们要求的是要让他的最小值最大。求最大的最小值,而不是直接求一个满足条件的最小值。
i f ( k < m ) if(k<m) if(k<m):说明最小值大了,最多也没有办法选择m个。
i f ( k = = m ) if(k==m) if(k==m):说明最小值是合理的,但是不一定是最大的,可能还存在稍微大点的值合适合理的。比如

1 4 9(3选3)

第一次mid=4,不满足条件,第二次mid=2,满足条

件,但是实际值应该是3
i f ( k > m ) if(k>m) if(k>m):说明最多可以选择的瓶盖比m多,但是并不代表不可以选m个,所以这种情况也是合理的,但是也不一定是最小值。比如

1 2 3 4(4选3)

最小值只能是1,但是是1时可以选4个。

此类二分答案的模板

cpp 复制代码
while(left<=right){
//求最小值的最大值。 
	int mid=(left+right)/2;
	if(check(mid)){ //满足条件 
		ans=mid; //记录当前答案
		left=mid+1; //查找有没有更大的值
		//如果有更大的值,就会自动更新ans
	}
	else{
		right=mid-1; //不合理,肯定不满足条件
	}
} 
printf("%d",ans); 

会了吗?不会的打回去重来

情况二:最大值最小

老样子,还是用例题来分析

对于给定的一个长度为N的正整数数列A[i],现要将其分成M(M≤N)段,并要求每段连续,且每段和的最大值最小。

关于最大值最小:

例如一数列4 2 4 5 1要分成3段

将其如下分段:

[4 2][4 5][1]

第一段和为6,第2段和为9,第3段和为1,和最大值为9。

将其如下分段:

[4][2 4][5 1]

第一段和为4,第2段和为6,第3段和为6,和最大值为6。

并且无论如何分段,最大值不会小于6。

所以可以得到要将数列4 2 4 5 1要分成3段,每段和的最大值最小为6。

题目中有最大值最小,很容易想到是一道二分答案的题。

第一步:确定二分答案的左右端点

左端点:序列中的最大值(再小就不能分了)

右端点:序列之和(分成一段)

第二步:二分框架(该题为最大值最小)

此种类型的二分框架

cpp 复制代码
while(l<=r){
	int mid=(l+r)/2;
	if(check(mid)){
		ans=mid; r=mid-1;
	}
	else l=mid+1;
}

注:如果当前答案满足条件,那么当前答案可能就是正确答案,但是也可能正确答案比当前答案小,所以先记录当前答案,如果有更小的再替换

内容分析------实数二分

有N条绳子,它们的长度分别为Li。如果从它们中切割出K条长度相同的绳子,这K条绳子每条最长能有多长?答案保留到小数点后2位(不四舍五入)。

前面我们使用的二分答案的模板是针对整数而言的,如果当前值不合理,那么就从当前值的下一个(上一个)整数开始继续寻找,但是该题就不可以这样做了,因为他可能是小数,所以我们就不可以Left=mid+1,right=mid-1了,这样很容易丢失正解,因为正解可能就在相邻两个整数之间。

cpp 复制代码
while(right-left>0.0001){
	double mid=(left+right)/2;
	if(check(mid)){
		ans=mid;
		left=mid;
	}else right=mid;
}

因为保留两位小数,所以要让left和right的值精确到两位小数数值肯定是一样的,所以这里精度要高一点。

但是在实际做题中,我们会发现一个问题,就是经过多次计算后,由浮点数只是一个近似值,会有一定误差,在一定限度内,误差可以忽略不计,但是多次计算之后,误差会导致结果错误。比如1.0000在多次计算之后会变成0.99999,这样最后的结果就是错误的。比如洛谷上面切绳子这题,如果最后输出的答案是ans的值或者left的值,答案会错一部分,但是使用right的值就是正确的,但是理论上这两者的值精确到1e-4是正确的,实际上就算精确到1e-6都还是会错。所以当我们需要精确到较高的时候最好不使用浮点数。最好想办法转换成整数再计算。

所以切绳子这道题最好的做法就是把所有绳子长度扩大10000倍(保证保留两位小数时,近似值一样),然后按照前面对整数二分答案的做法来做。但是最后要求是保留两位小数,但是不四舍五入,所以当得到ans的值的时候,还要想办法变回原来的值,并且保留两位小数。

比如:ans=12358

正确结果应该是1.23,如果直接/10000,再保留整数结果是1.24.

相关推荐
手握风云-7 分钟前
数据结构(Java版)第二期:包装类和泛型
java·开发语言·数据结构
怀澈1221 小时前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
chnming19871 小时前
STL关联式容器之set
开发语言·c++
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
爱敲代码的憨仔1 小时前
《线性代数的本质》
线性代数·算法·决策树
威桑2 小时前
MinGW 与 MSVC 的区别与联系及相关特性分析
c++·mingw·msvc
熬夜学编程的小王2 小时前
【C++篇】深度解析 C++ List 容器:底层设计与实现揭秘
开发语言·数据结构·c++·stl·list
yigan_Eins2 小时前
【数论】莫比乌斯函数及其反演
c++·经验分享·算法
Mr.132 小时前
什么是 C++ 中的初始化列表?它的作用是什么?初始化列表和在构造函数体内赋值有什么区别?
开发语言·c++
阿史大杯茶2 小时前
AtCoder Beginner Contest 381(ABCDEF 题)视频讲解
数据结构·c++·算法