二分答案法(利用二分法查找区间的左右端点)
(1)估计 最终答案可能得范围 是什么
(2)分析 问题的答案 和 给定条件 之间的单调性,大部分时候只需要用到 自然智慧
(3)建立一个f函数, 当答案固定的情况下 ,判断 给定的条件是否达标
(4)在 最终答案可能的范围上不断二分搜索, 每次用f函数判断,直到二分结束,找到最合适的答案
(5)当题目要查找一个区间的左右端点时(符合的区间有多个值找最大或者最小),可以考虑一下这个方法能不能用
综上:
核心点:分析单调性,建立f函数
光说不练,假把式!开练!!!!!
下面有一些相关题目
题目一:爱吃香蕉的珂珂
测试链接:875. 爱吃香蕉的珂珂 - 力扣(LeetCode)
题解:
1. 先看问题答案与对应条件的单调性,这道题中当吃香蕉的速度越大时,吃完所需要的时间就会越少,但这道题中要注意如果一小时内吃完一堆香蕉就会休息,等到下一个小时才会再次开始吃,如果一小时内没有吃完,那么下一个小时就会继续吃,吃完之后还是不会继续吃其他堆的香蕉,还是会休息。所以这个题会用到向上取整算法( 例如a/b向上取整,(a+b-1)/b )
2. 分析要二分什么,问题要求返回h小时内吃掉所有香蕉的最小速度k(k为整数),那这道题就是二分速度,并且速度与时间也符合单调性,速度也快,时间越短。考虑二分速度的左右区间,左区间 速度最小为1 最大速度为数组中最大数据的大小,因为如果速度大于最大数据时,吃完之后也不会继续吃下一堆还是会休息,又有单调性所以最大数据的速度一定可以,但不会是答案。
3. check函数,参数传速度,返回吃完所需要的时间,然后与给定的k作对比,不断的缩小范围直到二分结束,返回答案。
class Solution {
public:int minEatingSpeed(vector<int>& piles, int h) {//定区间int r = 0;//找到数组中最大的数据作为区间的右端点for(int i=0; i<piles.size();i++){r = max(piles[i],r);}int ans = 0;//速度最小为1,左端点设置为1int l = 1;int mid =0;while(l<= r){// 等价于mid = (l+r)/2 但有时会越界防止越界mid = l+((r-l)>>1); // 二分点靠左if(check(piles,mid) <= h){//达标//记录答案ans = mid;r = mid-1;}else{//不达标//不记录答案直接去右面二分l = mid+1;}}return ans;}//check函数检查是否符合要求long long check(vector<int>& piles, int mid){//时间是向上取整 ,初始化long long类型数据,防止数据过大越界long long ans = 0; // ans为时间int i = 0;int size = piles.size();while(size--){// 时间是向上取整的ans += (piles[i++]-1)/mid + 1;}//返回mid速度,吃完所需时间return ans;}
};
题目二:分割数组的最大值
题目链接:410. 分割数组的最大值 - 力扣(LeetCode) (画匠问题)
题解:
1. 总结题意就是将数组分成规定的份数,然后返回分好的这些组中各自和的最大值的最小(就是组内的和最大的组的和使这个数即可能的小返回最小)
2 先看看这道题中最终答案,与给定条件之间是否存在单调性,最答案是各自总和的最大值,给定条件为划分成几份,当划分的分数越多,各自的总和就会越小,存在一定的单调性。
3. 思路就是写一个函数,传递的参数就是各自总和的最大值,然后返回满足在这个限制下,会将数组分成几份,然后与给定的k作对比,不断地缩小区间,直至二分结束
方法一:
在后面check函数中处理特殊情况(容易忘记处理)
class Solution {
public:int splitArray(vector<int>& nums, int k) {//二分答案 将最大部分的累加和一直二分,写一个函数(每组数据都限制在这个累加和的范围内时是否符合要求小于等于k个)判断//非负整数可以为0//在前面可以将区间缩小到最小然后可以避免后面细节的判断防止出错//改法将左端点设置为最小0int l = 0;//二分区间的右端点,就是累计和的最大也就是将数组分成一份就是数组中所有元素的累加和 int r = 0;for(int i = 0; i<nums.size();i++){r += nums[i];}//起始将mid设为0 int mid = 0;//ans来记录二分的答案,当答案满足记录下来继续二分 int ans = 0;//确定了二分的大致范围while(l<=r){//mid = (r+l)/2 下面这样写可以防止越界超过int的最大数值 mid = l+((r-l)>>1);//下面可以这样理解,返回值小于<=k,就是符合限制条件并且分的分数还比规定的小,那么一定有mid if( check(nums,mid) <= k){//mid都符合要求,右面的limit一定符合要求,所以去左面进行二分查找//先记录答案,然后去做左面看有没有最优解ans = mid;r = mid-1;}else{//不符合要求,去右面找 l = mid +1;}}return ans;}//可以说用到了贪心算法,就是没个部分都是最优解,这样分的份数最小 int check(vector<int>& nums, int limit){int sum = 0;int ret= 1; //代表最少分几份for(int i = 0 ;i<nums.size();i++){//如果数组中有值超过了限制,这个限制不成立if(nums[i] > limit) return INT_MAX;if(sum + nums[i] <= limit){//没有满 将数据吸纳进来sum += nums[i];}else{//当前sum结束了,现在sum的值为当前num[i]的值ret++;sum = nums[i];}}return ret;}
};
方法二:
在前面划分左右区间时就将区间范围划分到最小,这样可以避免处理后面,数组中有大于限制情况。
class Solution {
public:int splitArray(vector<int>& nums, int k) {//二分答案 将最大部分的累加和一直二分,写一个函数(每组数据都限制在这个累加和的范围内时是否符合要求小于等于k个)判断//非负整数可以为0//在前面可以将区间缩小到最小然后可以避免后面细节的判断防止出错int l = 0;int r = 0;for(int i = 0; i<nums.size();i++){r += nums[i];//区间的左值因为是数组划分完后的最大的最小值,所以一定大于等于数组中最大元素的值,将左端点设置为数组中最大的元素l = max(l,nums[i]);}int mid = 0;int ans = 0;//确定了二分的大致范围while(l<=r){mid = l+((r-l)>>1);if( check(nums,mid) <= k){//mid都符合要求,右面的limit一定符合要求,所以去左面进行二分查找//先记录答案,然后去做左面看有没有最优解ans = mid;r = mid-1;}else{l = mid +1;}}return ans;}int check(vector<int>& nums, int limit){int sum = 0;int ret= 1; //代表最少分几份for(int i = 0 ;i<nums.size();i++){if(sum + nums[i] <= limit){//没有满 将数据吸纳进来sum += nums[i];}else{//当前sum结束了,现在sum的值为当前num[i]的值ret++;sum = nums[i];}}return ret;}
};
题目三:找出第K小的数对距离
题目链接:719. 找出第 K 小的数对距离 - 力扣(LeetCode)
题解:
1 题意数对距离就是两数之间的绝对值差值,题目要返回第k小的数对距离。
2 先看答案与给定条件之间的单调性关系,当两数的绝对值差值越大,那么这个K值也会越大,所以存在一定的单调性关系。
3 那么我们可以二分数对距离,根据题意找到最大最小的数对距离。
4 check函数我们可以传递mid位置的数对距离,然后返回mid位置为第几小的位置,与给定的k进行比较,直到二分结束。
class Solution {
public:int smallestDistancePair(vector<int>& nums, int k) {//因为要找数据之间差值最大的两个数,并且该题排序与不排序不会影响最终结果 sort(nums.begin(),nums.end());//由于上面进行了排序所以右边界就是最大值与最小值的差值 int r = nums[nums.size() -1] - nums[0];//左边界就是数组中最小的差值,也就是数组之间相邻的两个数据最小的差值int l =r;for(int i = 1;i<nums.size()-1;i++){l = min(r,nums[i]-nums[i-1]);} int ans = 0;int mid = 0;while(l<=r){mid = l+((r-l)>>1);//num为返回的第几小的元素,与k进行比较int num = check(nums,mid);if(num >= k){//符合要求,将mid记录,这是mid可能已经为最终答案,记录以备候选,继续二分看是否还有最优解 ans = mid;r = mid -1;}else{//mid不符合要求,缩小区间到mid的右面 l = mid +1;}}return ans;}//用来判断是不是与第k小的关系,也就是返回<=mid的数对数量int check(vector<int>& nums,int mid){//这道题应用到了滑动窗口的方法 int sum = 0;int r = -1, l = 0;for(l=0;l< nums.size();l++){//从0开始存不能等于nums.size(),如果r+1 - l 的差值小于等于传进来的差值,就r++向后走,否则就停止 while(r < l || ((r+1)<nums.size() && nums[r+1] - nums[l] <= mid)){r++;}//因为l要加了,先将区间内符合条件的数据记录个数,L++,也就是l走之前记录l位置处的符合条件的个数 sum += r-l;}return sum;}
};