二分答案入门(有几道难题)(由浅入深 一步一步讲)LeetCode 073->410->牛客机器人跳跃->719
总结了“二分答案”这一重要算法思想,并通过四道经典题目由浅入深进行讲解:LeetCode 073(爱吃香蕉的狒狒)、LeetCode 410(分割数组的最大值)、牛客机器人跳跃问题以及 LeetCode 719(第 K 小的数对距离)。核心思想在于:当问题的答案具有范围,并且满足“答案越大(或越小),条件越容易(或越难)成立”的单调性时,就可以对“答案本身”进行二分搜索。文章详细分析了如何确定左右
零 前置知识(二分搜索)
二分搜索的关键(我前面文章有讲)
可以先看一下目录 带题目链接 内容有点多哈 但是真的很详细 写了我的思考过程代码都是AC了的请放心
(由浅入深 一步一步讲)
LeetCode 073->410(难)->牛客机器人跳跃->719(难)
**后续还会更新二分答案算法 **
一 LeetCode LCR 073 爱吃香蕉的狒狒[
狒狒喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。
狒狒可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K
根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉,下一个小时才会开始吃另一堆的香蕉。狒狒喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。
示例 1:
输入: piles = [3,6,7,11], H = 8 输出: 4 示例 2:
输入: piles = [30,11,23,4,20], H = 5 输出: 30 示例 3:
输入: piles = [30,11,23,4,20], H = 6 输出: 23
提示:
1 <= piles.length <= 10^4 piles.length <= H <= 10^9 1 <= piles[i] <=
10^9
1我的思路(下面还有解释 不着急)
每一个速度都会对应一个吃掉所有香蕉所需要的时间
速度越大 吃掉香蕉的时间会不变或者变小
也就是说 速度越大越好
我们假设给定一个速度speed 计算以这个 速度吃完所有香蕉所需要的时间
如果这堆香蕉少于 K
"她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉,下一个小时才会开始吃另一堆的香蕉。 "
这句话的意思时
如果speed 为4
香蕉有3个 需要2个小时
香蕉有5个 需要2个小时
香蕉有4个 需要1个小时
香蕉有2个 需要1 个小时
不难发现 吃完一组香蕉所需要的时间为 (这一组香蕉的个数/speed)向上取整
我们假如speed是固定的 那可以根据piles的值来算出吃完香蕉所需要的时间
3)
这个speed 是有范围的 最小一定是1 最大一定为piles数组中的最大值
(
因为吃掉每一组香蕉至少需要一个小时 是向上取整了
假如 数组最大值为max 以数组最大值为速度speed 吃完所有香蕉所需要的时间一定就是数组长度piles.size()
假如 以比数组最大值还要大的值为speed 那么在吃每一组香蕉时 仍然都是需要一小时 总共需要的时间仍然时数组的长度piles.size() 这一题要求最小值 比数组最大值还要大的speed不用再考虑了 一定时可以吃完的
)
4)
我们可以写一个函数fun() 参数时数组piles 和 速度speed 返回以这个speed为速度吃完所有香蕉需要的时间 具体实现如下
int fun(int speed,vector<int>&piles){
int time=0;
for(int it:piles){
time+=(int)ceil(it*1.0/speed);//*1.0是为了转换为double类型 方便向上取整
}
return time;
}
现在我们可以明确一下思路了
speed的范围是 [1,数组最大值max]
速度越大 吃完所需要的时间越小(也可能不变 反正一定不会变小 哈哈)
所以我们可以把speed的取值看作一个数轴 从1开始到max结束
1-----------------max (数轴)
越靠近1越不达标(不达标的意思是不能在指定时间内吃完所有香蕉)
越靠近max越达标(达标的意思是能够在指定时间内吃完所有香蕉)
现在我们已经基本明确了 范围 和 单调性
二分答案
left=1;
right=max;
int mid=(left+right)/2;
//为了避免溢出先减后加 改为int mid=left+(right-left)/2;
如果以mid为速度speed 达标了 那么以更大的速度一定也达标 所以让right=mid-1;
如果以mid为速度speed 不达标 那么以更小的速度一定也不达标 所以让left=mid+1;
最后一定是
[不达标--------------------------------------达标 ]
[------------------right-left-------------------------]
即right右边的数值(不包含right)一定是达标的speed
left左边的数值(不包含left)一定是不达标的speed
这样的话left 一定就是最小的达标的speed了
直接返回left就行了
2 代码实现
我觉得已经说清楚了 谢谢
希望可以帮到你
//AC了
class Solution {
private:
int fun(int speed,vector<int>&piles){
int time=0;
for(int it:piles){
//必须向上取整 取整之前要转换为double型 所以我*1.0
time+=(int)ceil(it*1.0/speed);
//也可以time+=(int)ceil((double)it/speed);
}
return time;
}
public:
int minEatingSpeed(vector<int>& piles, int h) {
int left=1;//左边界
//直接函数找最大值
//因为max_element(piles.begin(),piles.end() )返回的是指向最大值的迭代器
//所以要加* 解引用
int right=*max_element(piles.begin(),piles.end() );//右边界
while(left<=right){//开始二分答案
int mid=left+(right-left)/2;//先减后加 避免数据溢出
int time=fun(mid,piles);
if(time<=h){//如果达标 就是<=要求的时间h
right=mid-1;//右边界移动
}else{
left=mid+1;//左边界移动
}
}
return left;//返回left或者right+1都可以 是一样是数值 哈哈
}
};
这道题一定要看懂了之后再看下面的题目
3 如果你还是不懂思路(懂了直接跳过)
1)先要明白计算一个speed下 吃完所需要的时间time
“狒狒可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K
根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉,下一个小时才会开始吃另一堆的香蕉。”
这句话的意思是每一堆香蕉吃完它需要的时间一定是大于等于(double)piles[i]/speed个小时 因为如果有剩余时间一定会停下来休息
我们先不管这个h(题目中的条件)所以对于一个给定的速度speed 一定会有一个对应的时间time 表示吃完这个数组的所有香蕉所需要的时间time
函数应该怎么写呢 先说参数 涉及到一个数组piles 一个给定的速度speed
int fun(vector<int>&piles,int speed){
int time=0;//计算吃完所有香蕉所需要的时间
for(int it:piles){//遍历整个数组
time+=(int)ceil((double)it/speed);//向上取整
}
return time;//返回吃掉所有香蕉所需要的时间
}
- 题目要求在h小时内吃完所有香蕉 我们在函数fun()中传入参数speed 以这个速度speed吃完所有香蕉的时间不一定是小于等于h的
接下来我们在考虑一个问题 是不是速度越大 这个吃完所有香蕉所需要的时间time越小(或者不变 反正是不会变小的)
-----假设以一个速度speed吃完所有香蕉所需要的时间是time 如果这个time<=h说明以这个速度speed吃香蕉是达标的(达标就是指满足了题目说的在h小时之内吃完所有香蕉) 那么我们想一想如果以更大的速度吃 香蕉呢 一定也是能在规定时间内吃完的 ok 但是这个题目求的是最小值 所以我们应该探寻一下以比这个speed更小的速度吃香蕉 会不会达标
-----假设以一个速度speed吃完所有香蕉所需要的时间 是time 如果中国time>h 说明以这个speed速度吃不能在规定时间内吃完所有香蕉 那么我们想一想如果以更小的速度吃呢 那一定也是不能达标的(不达标的意思是不能在规定时间内吃完所有香蕉) 我们就必须要在比speed大的数值上寻找达标的速度
3)怎么确定speed的范围呢
speed最小为1
speed最大是多少呢 ? 因为吃完所有香蕉的时间最大是piles.size() 就是数组的长度 最起码吃一堆香蕉需要一个小时吧 吃完piles.size()堆香蕉最起码需要piles.size()个小时 那么speed最大的就是piles数组中的最大值maxx 因为以更大的值吃香蕉需要的时间也一定是piles.size() 没有讨论的必要了 我们要求的是最小值
整理一下
left=1;//左边界
right=*max_element(piles.begin(),piles.end() );//右边界
在这个边界上二分 就是** 2 代码实现**上的答案了
二 LeetCode 410 分割数组的最大值(有点难)
给定一个非负整数数组 nums 和一个整数 k ,你需要将这个数组分成 k 个非空的连续子数组,使得这 k 个子数组各自和的最大值 最小
返回分割后最小的和的最大值。
子数组 是数组中连续的部份。
示例 1:
输入:nums = [7,2,5,10,8], k = 2 输出:18 解释: 一共有四种方法将 nums 分割为 2 个子数组。
其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。 因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。 示例 2:输入:nums = [1,2,3,4,5], k = 2 输出:9 示例 3:
输入:nums = [1,4,4], k = 3 输出:4
提示:
1 <= nums.length <= 1000 0 <= nums[i] <= 106 1 <= k <= min(50,
nums.length)
1 我的思路(有点难 建议自己模拟一下)
1)
我的先明白一个点
如果给定一个子数组的累加和最大值max 用这个max去划分数组 假如最小划分了m个数组 那么一定也能划分m+1 m+2 m+3… 个数组使每一个子数组的累加和小于等于max 这个就是单调性(重要的解题关键)
这个max 越大相当于范围越宽泛 划分的子数组数量就越小
这个max 越大 就越能满足划分的数组数<=k(k 就是题目上的k)
2)
接着
这个子数组的累加和max 是有范围的,最小为0 最大为整个数组的累加和(
因为当累加和最大为整个数组的累加和Sum时
直接可以划分为子数组的数量为1
可以划分为1个子数组满足累加和不大于max
那一定能划分为2个子数组满足累加和不大于max
那一定能划分为3个子数组满足累加和不大于max
那一定能划分为k个子数组满足累加和不大于max…)
3)
所以
只要给定一个字子数组累加和的最大值 我们可以找到最小要把数组划分为几个子数组
这个f()函数 参数为子数组的最大累加和maxx 数组本身nums
返回 最少要划分为多少个子数组part
int f(vector<int>&nums,int maxx){
int part=0;//要分割多少刀 就是间隔
int sum=0;//目前中国数组的累加和
for(int i=0;i<nums.size();++i){
//先不要加进去 先判断加进去之前满不满足 每一部分的累加和<=maxx
if(sum+nums[i]>maxx){//如果不满足 就要part++ sum从新开始
sum=0;
part++;
}
sum+=nums[i];
}
part++;//part时切了多少刀 求部分数=part+1
return part;
}
如果这个求出来的部分数part<=k 那么达标(达标的意思是以这个maxx为子数组最大值 分割的数组数一定是小于等于k的) 那么以比这个maxx大的值为子数组最大值 一定也是达标的 所以范围right=mid-1 移动右端点
2 代码实现
//AC了
class Solution {
private:
//以子数组最大和为max_sum 返回满打满算可以最少分为多少part个数组
int fun(vector<int>&nums,int max_sum){
int sum=0;
int part=1;
for(int i=0;i<nums.size();++i){
if(sum+nums[i]>max_sum){//先预处理 看看能不能加上去
sum=nums[i];//不能加上去就直接更新sum=0 并加上nums[i]
part++;
}else{
sum+=nums[i];//能加上去就直接加上去
}
}
return part;//返回最小被分割为多少部分
}
public:
int splitArray(vector<int>& nums, int k) {
int left=*max_element(nums.begin(),nums.end() );
int right=0;
for(int it:nums)right+=it;
while(left<=right){
int midd=left+(right-left)/2;
int part=fun(nums,midd);
if(part<=k){//part<=k 意思是以这个midd为子数组的最大和 分割一定是可以分为K个部分的 满足条件
right=midd-1;//那以更大的标准分割一定也满足条件 我们要找最小值 就缩小右边界
}else{
left=midd+1;//缩小左边界
}
}
return left;//right+1也行
}
};
三 牛客 机器人跳跃问题
机器人正在玩一个古老的基于DOS的游戏。游戏中有N+1座建筑——从0到N编号,从左到右排列。编号为0的建筑高度为0个单位,编号为i的建筑的高度为H(i)个单位。
起初, 机器人在编号为0的建筑处。每一步,它跳到下一个(右边)建筑。假设机器人在第k个建筑,且它现在的能量值是E,
下一步它将跳到第个k+1建筑。它将会得到或者失去正比于与H(k+1)与E之差的能量。如果 H(k+1) > E 那么机器人就失去
H(k+1) - E 的能量值,否则它将得到 E - H(k+1) 的能量值。游戏目标是到达第个N建筑,在这个过程中,能量值不能为负数个单位。现在的问题是机器人以多少能量值开始游戏,才可以保证成功完成游戏?
输入描述:
第一行输入,表示一共有 N 组数据.第二个是 N 个空格分隔的整数,H1, H2, H3, …, Hn 代表建筑物的高度 输出描述:
输出一个单独的数表示完成游戏所需的最少单位的初始能量 示例1 输入: 5 3 4 3 2 4 复制 输出: 4 复制 示例2 输入: 3 4
4 4 复制 输出: 4 复制 示例3 输入: 3 1 6 4 复制 输出: 3 复制 备注: 数据约束: 1 <= N <= 10^5 1
<= H(i) <= 10^5
1 我的思路(二分答案法)
相当于重力势能的问题
遇到比它小的就加上差值 遇到比它大的就减去差值
首先要明白一点 起始值val越大越容易跳跃到最后结尾位置
------当起始值为数组的最大值时 一定能通关(通关就是指能跳到结尾) 因为在遍历的时候遇到的每一个值都会比现在的值小 会加上差值 这个val只会变得越来越大 一定会走到最后通关的
起始值的最小值为0 val为0时不一定能通关
所以就确定了左右边界
int left=0;//左边界
//为什么是arr.begin()+1呢
//因为看题意 初始值是我们要求的值 也是arr[0]
//所以接下来要跳跃的值是从arr[1]开始的
//(注意arr.size()=N+1 设置数组长度的时候要注意)
int right=*max_element(arr.begin()+1,arr.end() );//右边界
- 一步一步来思考
对于每一个起始值 都会产生能不能通关 就是返回true(表示能通关) 还是false(表示不能通关)
bool fun(vector<int>&arr,int val){
//遍历arr的从下标为1开始
for(int i=1;i<arr.size();++i){
val=val+(val-arr[i]);
if(val<0)return false;//如果出现有负数 直接不通关
}
//最后走到这一步(因为<0的时候早就返回false了)一定是>=0 通关
return true;
}
- 不对我们需要考虑一个问题 val=val+(val-arr[i])会不会溢出
假如val=5 arr数组={ _ ,1 , 1, 1 ,1 , 1,…4,3,2,9,3,4… };
在前面加的时候+4 =9
+8 =17
+16=33
+32=65
+64=129
+128=…
这样的指数型增加情况一定会溢出(改为long long也会溢出 基本数据类型一定承受不了这样大的数据 )
因为当val=maxx(maxx就是arr数组的最大值)时 一定会通关(我们之前讲过了) 所以在fun()函数中可以提前返回 保证在溢出前就返回了
所以fun()函数改为
bool fun(vector<int>&arr,int val){
int maxx=*max_element(arr.begin()+1,arr.end() );
//遍历arr的从下标为1开始
for(int i=1;i<arr.size();++i){
val=val+(val-arr[i]);
if(val<0)return false;//如果出现有负数 直接不通关
if(val>=maxx)return true;//如果>=maxx了 不用管后面的数值了一定会通关的
}
//最后走到这一步(因为<0的时候早就返回false了)一定是>=0 通关
return true;
}
你如果不想每一个调用函数都执行一次*max_element(arr.begin()+1,arr.end() );
就改为传参数
bool fun(vector<int>&arr,int val,int maxx){
//遍历arr的从下标为1开始
for(int i=1;i<arr.size();++i){
val=val+(val-arr[i]);
if(val<0)return false;//如果出现有负数 直接不通关
if(val>=maxx)return true;//如果>=maxx了 不用管后面的数值了一定会通关的
}
//最后走到这一步(因为<0的时候早就返回false了)一定是>=0 通关
return true;
}
我更偏向于传参数 哈哈
4 ) 本质上就是确定范围和单调性
2 我的代码
思路很清晰 可读性强
ACM模式输入输出
//AC了
#include <iostream>
#include<vector>
#include<algorithm>
using namespace std;
bool fun(vector<int>&arr,int val,int maxx){
for(int i=1;i<arr.size();++i){
val=val+(val-arr[i]);//加上差值 如果val-arr[i]<0就相当于减去了差值的绝对值 是一样的
if(val<0)return false;//出现负数 直接不通关
if(val>=maxx)return true;//出现>=maxx 不用再管后面的了一定会通关的
}
return true;//都走到这一步了 一定通关
}
int main() {
int N;
cin >> N;
vector<int>arr(N + 1); //注意要设置为N+1 不懂的话看看题目要求
for (int i = 1; i <= N; ++i)cin >> arr[i];
int left = 0;//左边界
int maxx = *max_element(arr.begin() + 1, arr.end() );//数组舍弃arr[0]后的最大值
int right = maxx;//右边界
while (left <= right) {//二分答案
int midd = left + (right - left) / 2;
if (fun(arr, midd, maxx) == true ) {//如果以midd为起始值通关 那么以大于它的也一定通关
right=midd-1;//我们要找刚好通关的最小值 所以要缩小右边界
}else{
left=midd+1;//缩小左边界
}
}
cout<<left<<endl;//cout<<right+1<<endl;也行 一样的
}
四 LeetCode 719 找出第k 小的数对的距离(难题)
数对 (a,b) 由整数 a 和 b 组成,其数对距离定义为 a 和 b 的绝对差值。
给你一个整数数组 nums 和一个整数 k ,数对由 nums[i] 和 nums[j] 组成且满足 0 <= i < j <
nums.length 。返回 所有数对距离中 第 k 小的数对距离。

示例 1:
输入:nums = [1,3,1], k = 1 输出:0 解释:数对和对应的距离如下: (1,3) -> 2 (1,1) -> 0
(3,1) -> 2 距离第 1 小的数对是 (1,1) ,距离为 0 。 示例 2:输入:nums = [1,1,1], k = 2 输出:0 示例 3:
输入:nums = [1,6,1], k = 3 输出:5
提示:
n == nums.length 2 <= n <= 104 0 <= nums[i] <= 106 1 <= k <= n * (n -1) / 2
1 我的思路
这个距离是由范围的最小是0 最大是maxx-minn 距离不会超出数组最大值于最小值的差值
[0 , maxx-minn]
int left=0;
int maxx=*max_element(nums.begin(),nums.end() );
int minn=*min_element(nums.begin(),nums.end() );
int right=maxx-minn;
fun()函数返回小于等于limit 的数对数量cnt
如果这个数对数量cnt<=k 那么以更大的limit传入fun()函数 返回的一定比cnt要大 就一定不是第k小的差值了
如果这个数对数量cnt>k 那么这个limit一定不是第k小的差值 比它小的limit也一定不是第k小的差值 一定是大于k的
2 代码实现
//AC了
class Solution {
public:
//返回数组中小于等于limit的数对数量cnt
//用滑动窗口法(时间复杂度为O(n) 只遍历一遍数组)
int fun(vector<int>&nums,int limit){
int left=0;
int cnt=0;
for(int right=left+1;right<nums.size();++right){
while(nums[right]-nums[left]>limit){
left++;
}
cnt+=(right-left);//以right为结束的数对数量正好就是right-left
}
return cnt;
}
int smallestDistancePair(vector<int>& nums, int k) {
sort(nums.begin(),nums.end() );
int left=0;//左边界
int maxx=*max_element(nums.begin(),nums.end() );
int minn=*min_element(nums.begin(),nums.end() );
int right=maxx-minn;//右边界
while(left<=right){
int midd=left+(right-left)/2;
int cnt=fun(nums,midd);
if(cnt>=k){//cnt就是以差值小于等于midd的数对数量
//cnt就是以midd为答案的排序后序号
right=midd-1;
}else{
left=midd+1;
}
}
return left;
}
};
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)