二分答案法、三分法

二分法

经常有这样的问题,求xxx最小值的最大值,即求符合条件的值里的最大值,这种问题有个解法叫二分答案法。一听,什么,不知道的答案也能二分?嗯没错,关键在于这个答案是可以判断是不是符合条件的。

算法思想

以求最小值的最大值(最小值最大化)为例,尝试一个可能的答案,如果这个答案符合题目条件,那么它肯定是“最小”(可行解),但不一定是“最大”(最优解),然后我们换个更大的可能答案,如果也符合条件,那这个新可行解就更优,不断重复即可。怎么找呢?这时就该二分上场了。

二分前提

1.答案区间上下限确定,即最终答案在哪个范围是容易知道的。

2.检验某值是否可行是个简单活,即给你个值,你能很容易的判断是不是符合题目要求。

3.可行解满足区间单调性,即若x是可行解,则在答案区间内x+1(也可能是x-1)也可行。

两种情况

下图中L,R为当前答案区间,M为中心点,根据二分思想判断M是否符合条件,再移动L或R,变成L',R',图中的T和F表示是否符合条件。

1.最小值最大化

int l = min_ans, r = max_ans;
while (l < r) {
	int mid = (l + r + 1) / 2;   //+1避免 r == l + 1 时mid一直等于l,从而死循环
	if (ok(mid))	//符合条件返回True
		l = mid;
	else
		r = mid - 1;
}
希望答案尽可能大,所以我们需要确保左区间L点符合题目条件(最小),至于R是否符合条件是不确定的,首先判断M点符合与否,符合则将L移到M点,维持了L的True属性,也增大了所要的最小值所在区间,如果不符合,没办法在保持L的True属性情况下移动L,那就移动R。
 
 
 
 
 

2.最大值最小化

 

int l = min_ans, r = max_ans;
while (l < r) {
	int mid = (l + r) / 2;
	if (ok(mid))	//符合条件返回True
		r = mid;
	else
		l = mid + 1;
}

 

按同样道理分析,维持R的True属性即可。这里的mid就不需要加1了,因为 mid 跟 l 重合时,l = mid + 1;会自增,而当 mid 和 r 重合时 l 也跟 r 重合,结束循环了。

 

注意点

1. 每次循环都要确保L和R有一个被更新,否则死循环就呵呵了。
2. 答案是浮点数的情况:区间更新不能加1,这样变动太大,直接
 
l = mid;
r = mid;

关于 l < r 还是 l <= r,mid是否加减1

我们先看看最普通的二分,在一个有序数组里找一个数,返回下标:

如下两个写法都是对的,区别只是查找区间是 左闭右开,还是左右都闭,只要保证所有值都能覆盖到,不会遗漏就行

 /**
     * 最普通的二分找一个数字
        两个写法都是对的,一般人可能更偏向于 <= 的写法,左右都闭区间 [left,right],注意 right是 len-1
     * @param nums
     * @param target
     * @return
     */
    public int search(int[] nums, int target) {
        int left = 0,right = nums.length - 1;
        while(left <= right){
            int mid = (left + right)/2;
            if(nums[mid] == target){
                return mid;
            }
            if(nums[mid] < target){
                left = mid + 1;
            }
            if(nums[mid] > target){
                right = mid - 1;
            }
        }
        return -1;
    }
    /**
        两个写法都是对的
        我自己的二分答案偏向于 左闭右开 [left,right),注意right
        另外,在破循环的时候,会是 left == right ,能覆盖到 答案刚好是right最大值的情况
     **/
    public int search_2(int[] nums, int target) {
        //在 [0,length) 里找
        int left = 0,right = nums.length ;
        while(left < right){
            int mid = (left + right)/2;
            //mid 已经查找过了
            if(nums[mid] == target){
                return mid;
            }
            //在 [mid+1,right) 里找
            if(nums[mid] < target){
                left = mid + 1;
            }
            //在 [left,mid) 里找
            if(nums[mid] > target){
                right = mid;
            }
        }
        return -1;
    }

然后你再开我们上面的

我们的写法里的隐含条件:
1. 我们都是左闭右开区间查找的,即在 [min_ans,max_ans) 里找
2. 查找最后一定有结果存在,不会出现普通二分那样的找不到(返回-1)的情况,跟普通二分比,我们的题意会是 类似于找第一个>=目标的值 或者 <=目标 这种含义

最大值最小化(在升序排序里,尽可能靠左)

	int left = min_ans, right = max_ans;
	//最开始是 [left,right)
	while (left < right) {
		int mid = (left + right) / 2;
		if (ok(mid))	// 这里其实包含了 mid 已经被查找判定过了,那我们后续查找 [left,mid) 就行
			right = mid;
		else			// 进入else,其实包含了mid已经被查找判定过了的意思,后续查找 [mid+1,right) 就行
			left = mid + 1;
	}
	return left;

你可能会觉得上面的查找里right都是开区间,会没覆盖到 right 
但事实上退出条件是 left == right,如果left一直没达标, 结果就是固定是 right(也等于left【注意:返回结果一定会存在,不会出现普通二分的-1情况】),也即答案是 max_ans 的情况也能覆盖

样例题

 

最大值最小化 https://leetcode.com/problems/minimum-limit-of-balls-in-a-bag/

题意(建议直接看英文): 有一堆带球的包,每个包有nums[i] 个球,你可以进行操作:把某个包里的球分成两个新包(即把 nums[i]  变成 两个和为 nums[i]  的新包) ,已知 你的惩罚值(不要在意翻译) 是 最大的 nums[i] ,现在问 在最多操作N次的情况下,这个惩罚值最小是多少?

即: 我们有一个数组 nums[i],每次操作能把 一个 nums[i] 减小成 两个新的数字和,问在最多N次操作的情况下, Max{nums[i]} 最小是多少?

解:「Max{nums[i]} 最小是多少」 即 最大值最小化,直接套二分,二分的值为: Max{nums[i]}  。

         保持右区间永远符合条件(最多N次操作),尽可能让值往小靠。

 

关于验证当前 惩罚值(mid)是否达标:

   - 循环每个包(nums[i]),看这个nums[i] 最小多少次操作能让他 变成 小于等于 惩罚值(mid),如果所有包的操作之和小于 规定次数, 那说明 当前惩罚值(mid)达标。

   - 至于单个 nums[i] 需要多少次操作能让他 变成 小于等于 惩罚值(mid): 贪心一下,要把 nums[i] 变成 ≤ mid 的多个数字, 肯定是把 nums[i] 变成 mid 和 nums[i]-mid ,然后继续对 nums[i]-mid 操作,你要把8分成小于2的,那肯定是8=2+6,然后再6=2+4 以此类推。 可以写循环去减处理,但减着减着你就发现次数就是  (nums[i]  / mid) -1 或者 直接对除,算个小规律

class Solution {
        public boolean isOk(int[] nums, int aim,int op) {
            int count = 0;
            for (int i = 0; i < nums.length; i++) {
                if(nums[i] % aim == 0){
                    //8拆成小于等于2的多个数字之和,需要3次  7 = 2+6 = 2+2+4 = 2+2+2
                    count += (nums[i] / aim) - 1;
                }else{
                    //7拆成小于等于2的多个数字之和,需要3次 7 = 2+5 = 2+2+3 = 2+2+2+1
                    count += (nums[i] / aim) ;
                }
            }
            return count <= op;
        }

        public int minimumSize(int[] nums, int maxOperations) {
            //这个minVal是1,是题目数值范围最小是1
            int maxVal = Integer.MIN_VALUE,minVal = 1;
            for (int i = 0; i < nums.length; i++) {
                maxVal = Math.max(maxVal,nums[i]);
            }
            int left = minVal,right = maxVal;
            while (left < right){
                int mid = (left + right) /2;
                //右边达标,继续缩小区间,尽量让左边靠
                //如果mid可以,那么 mid+1也肯定可以,单调
                if(isOk(nums,mid,maxOperations)){
                    right = mid;
                }else{
                    left = mid + 1;
                }
            }
            return left;
        }



//    public static void main(String[] args) {
//        int []arr = new int[]{9};
//        System.out.println(new Solution().minimumSize(arr,2));;
//
//        arr = new int[]{2,4,8,2};
//        System.out.println(new Solution().minimumSize(arr,4));;
//
//        arr = new int[]{7,17};
//        System.out.println(new Solution().minimumSize(arr,2));;
//
//        arr = new int[]{2};
//        System.out.println(new Solution().minimumSize(arr,2));;
//
//    }
    }

 

 

 

 

三分法

当二分的函数值不是递增/减,而是先增后减或者先减后增时二分就挂了。此时需要三分法,这里直接盗用hihocoder Problem 1142的图(hihocoder需要注册登陆,没登陆进不去)

 
如图这种情况先减后增有极小,若lm比rm低(即lm对应的函数值 < rm函数值)则极小点(图中最低点)肯定在[ left, rm ] ,反之在[ lm, right ],剩下就跟二分一样根据大小关系调整区间就行了。那lm和rm取值多少?一个不错的取值是lm为整个区间的1/3点,rm为2/3点,即
lmid = l + (r - l)/3;
rmid = r - (r - l)/3;
嗯三分就这样完了。
 
然后另外一种情况,先增后减有极大:
如图lm低于rm,则极大在[ lm,right ](为啥不是[ left, rm ]?你试试把rm放在lm右边,极大值左边看看?),否则极大在 [ left, rm ]。写代码上就是极小的处理语句反过来就行了。
 

HDU 2899 Strange fuction

给一函数,该函数在任意Y>0的情况下x在[0,100]内有极小值,求之。
 
按思路套上代码即可AC
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

double y;
double val(double x){
    return 6*x*x*x*x*x*x*x+8*x*x*x*x*x*x+7*x*x*x+5*x*x-y*x;
}

double solve(double l,double r){
    double eps = 1e-7;
    while(l + eps < r){
        double lmid = l + (r-l)/3,rmid = r - (r-l)/3;
        if(val(lmid) < val(rmid)){
            r = rmid;
        }else{
            l = lmid;
        }
    }
    return val(l);
}
int main(){
    int t;
    cin>>t;
    while(t--){
        cin>>y;
        printf("%.4f\n", solve(0,100.0));
    }
    return 0;
}


hihocoder 1142 

有一条抛物线y=ax^2+bx+c和一个点P(x,y),求点P到抛物线的最短距离d。
输入

第1行:5个整数a,b,c,x,y。前三个数构成抛物线的参数,后两个数x,y表示P点坐标。-200≤a,b,c,x,y≤200

输出

第1行:1个实数d,保留3位小数(四舍五入)

样例输入
2 8 2 -2 6
样例输出
2.437
hiho的数据只有一组a>0的情况,所以下面代码直接过。
#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
#define ll long long
#define clr( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("1.in","r",stdin);
#define WE freopen("1.out","w",stdout);
#define SpeedUp std::cout.sync_with_stdio(false);
const int maxn = 1e5+5;
const int inf = 0x3f3f3f3f;
double a,b,c,x,y;

double val(double X){
    return sqrt((X-x)*(X-x)+(a*X*X+b*X+c-y)*(a*X*X+b*X+c-y));
}

double solve(double l,double r){
    double eps = 1e-5;
    while(l+eps<r){
        double lmid = l + (r-l)/3,rmid = r - (r-l)/3;
        if(val(lmid) < val(rmid)){
            r = rmid;
        }else{
            l = lmid;
        }
    }
    return val(l);
}
int main(){
    // RE
    while(cin>>a>>b>>c>>x>>y){
        printf("%.3f\n", solve(-200.0,200.0));
    }
    return 0;
}
 
 
 
  • 32
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值