二分法
经常有这样的问题,求xxx最小值的最大值,即求符合条件的值里的最大值,这种问题有个解法叫二分答案法。一听,什么,不知道的答案也能二分?嗯没错,关键在于这个答案是可以判断是不是符合条件的。
算法思想
以求最小值的最大值(最小值最大化)为例,尝试一个可能的答案,如果这个答案符合题目条件,那么它肯定是“最小”(可行解),但不一定是“最大”(最优解),然后我们换个更大的可能答案,如果也符合条件,那这个新可行解就更优,不断重复即可。怎么找呢?这时就该二分上场了。
二分前提
1.答案区间上下限确定,即最终答案在哪个范围是容易知道的。
2.检验某值是否可行是个简单活,即给你个值,你能很容易的判断是不是符合题目要求。
3.可行解满足区间单调性,即若x是可行解,则在答案区间内x+1(也可能是x-1)也可行。
两种情况
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;
}

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 重合,结束循环了。
注意点
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需要注册登陆,没登陆进不去)

lmid = l + (r - l)/3;
rmid = r - (r - l)/3;

HDU 2899 Strange fuction

#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
#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;
}