前言
前面我们讲的主要都是二分查找,二分查找在考试中一般都不会作为独立算法而存在,一般都是一道题中部分算法,它主要是基于单调性的减少枚举次数。而枚举除了枚举操作还可以枚举答案,枚举答案同样可以用二分来减少枚举次数,这就是比赛中常考的二分答案。很多时候当答案不是很容易直接计算的时候,我们可以考虑枚举答案,但是答案范围特别大,但是又具有单调性的时候,直接一个一个枚举答案效率很低,我们可以考虑二分枚举,如果答案有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.