我写了一个套路,助你随心所欲运用二分搜索
后台回复进群一起刷力扣
点击卡片可搜索关键词????
读完本文,可以去力扣解决如下题目:
875.爱吃香蕉的珂珂(Medium)
1011.在D天内送达包裹的能力(Medium)
我们前文 我作了首诗,保你闭着眼睛也能写对二分查找 详细介绍了二分搜索的细节问题,探讨了「搜索一个元素」,「搜索左侧边界」,「搜索右侧边界」这三个情况,教你如何写出正确无 bug 的二分搜索算法。
但是前文总结的二分搜索代码框架仅仅局限于「在有序数组中搜索指定元素」这个基本场景,具体的算法问题没有这么直接,可能你都很难看出这个问题能够用到二分搜索。
对于二分搜索算法在具体问题中的运用,前文 二分搜索的运用(一) 和前文 二分搜索的运用(二) 有过介绍,但是还没有抽象出来一个具体的套路框架。
所以本文就来总结一套二分搜索算法运用的框架套路,帮你在遇到二分搜索算法相关的实际问题时,能够有条理地思考分析,步步为营,写出答案。
警告:本文略长略硬核,建议清醒时学习。
原始的二分搜索代码
二分搜索的原型就是在「有序数组」中搜索一个元素target
,返回该元素对应的索引。
如果该元素不存在,那可以返回一个什么特殊值,这种细节问题只要微调算法实现就可实现。
还有一个重要的问题,如果「有序数组」中存在多个target
元素,那么这些元素肯定挨在一起,这里就涉及到算法应该返回最左侧的那个target
元素的索引还是最右侧的那个target
元素的索引,也就是所谓的「搜索左侧边界」和「搜索右侧边界」,这个也可以通过微调算法的代码来实现。
我们前文 二分搜索算法框架详解 详细探讨了上述问题,对这块还不清楚的读者建议复习前文,已经搞清楚基本二分搜索算法的读者可以继续看下去。
在具体的算法问题中,常用到的是「搜索左侧边界」和「搜索右侧边界」这两种场景,很少有让你单独「搜索一个元素」。
因为算法题一般都让你求最值,比如前文 二分搜索的运用(一) 中说的例题让你求吃香蕉的「最小速度」,让你求轮船的「最低运载能力」,前文 二分搜索的运用(二) 讲的题就更魔幻了,让你使每个子数组之和的「最大值最小」。
求最值的过程,必然是搜索一个边界的过程,所以后面我们就详细分析一下这两种搜索边界的二分算法代码。
「搜索左侧边界」的二分搜索算法的具体代码实现如下:
// 搜索左侧边界
int left_bound(int[] nums, int target) {if (nums.length == 0) return -1;int left = 0, right = nums.length;while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] == target) {// 当找到 target 时,收缩右侧边界right = mid;} else if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {right = mid;}}return left;
}
假设输入的数组nums = [1,2,3,3,3,5,7]
,想搜索的元素target = 3
,那么算法就会返回索引 2。
如果画一个图,就是这样:
「搜索右侧边界」的二分搜索算法的具体代码实现如下:
// 搜索右侧边界
int right_bound(int[] nums, int target) {if (nums.length == 0) return -1;int left = 0, right = nums.length;while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] == target) {// 当找到 target 时,收缩左侧边界left = mid + 1;} else if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {right = mid;}}return left - 1;
}
输入同上,那么算法就会返回索引 4,如果画一个图,就是这样:
好,上述内容都属于复习,我想读到这里的读者应该都能理解。记住上述的图像,所有能够抽象出上述图像的问题,都可以使用二分搜索解决。
二分搜索问题的泛化
什么问题可以运用二分搜索算法技巧?
首先,你要从题目中抽象出一个自变量x
,一个关于x
的函数f(x)
,以及一个目标值target
。
同时,x, f(x), target
还要满足以下条件:
1、f(x)
必须是在x
上的单调函数(单调增单调减都可以)。
2、题目是让你计算满足约束条件f(x) == target
时的x
的值。
上述规则听起来有点抽象,来举个具体的例子:
给你一个升序排列的有序数组nums
以及一个目标元素target
,请你计算target
在数组中的索引位置,如果有多个目标元素,返回最小的索引。
这就是「搜索左侧边界」这个基本题型,解法代码之前都写了,但这里面x, f(x), target
分别是什么呢?
我们可以把数组中元素的索引认为是自变量x
,函数关系f(x)
就可以这样设定:
// 函数 f(x) 是关于自变量 x 的单调递增函数
// 入参 nums 是不会改变的,所以可以忽略,不算自变量
int f(int x, int[] nums) {return nums[x];
}
其实这个函数f
就是在访问数组nums
,因为题目给我们的数组nums
是升序排列的,所以函数f(x)
就是在x
上单调递增的函数。
最后,题目让我们求什么来着?是不是让我们计算元素target
的最左侧索引?
是不是就相当于在问我们「满足f(x) == target
的x
的最小值是多少」?
画个图,如下:
如果遇到一个算法问题,能够把它抽象成这幅图,就可以对它运用二分搜索算法。
算法代码如下:
// 函数 f 是关于自变量 x 的单调递增函数
int f(int x, int[] nums) {return nums[x];
}int left_bound(int[] nums, int target) {if (nums.length == 0) return -1;int left = 0, right = nums.length;while (left < right) {int mid = left + (right - left) / 2;if (f(mid, nums) == target) {// 当找到 target 时,收缩右侧边界right = mid;} else if (f(mid, nums) < target) {left = mid + 1;} else if (f(mid, nums) > target) {right = mid;}}return left;
}
这段代码把之前的代码微调了一下,把直接访问nums[mid]
套了一层函数f
,其实就是多此一举,但是,这样能抽象出二分搜索思想在具体算法问题中的框架。
运用二分搜索的套路框架
想要运用二分搜索解决具体的算法问题,可以从以下代码框架着手思考:
// 函数 f 是关于自变量 x 的单调函数
int f(int x) {// ...
}// 主函数,在 f(x) == target 的约束下求 x 的最值
int solution(int[] nums, int target) {if (nums.length == 0) return -1;// 问自己:自变量 x 的最小值是多少?int left = ...;// 问自己:自变量 x 的最大值是多少?int right = ... + 1;while (left < right) {int mid = left + (right - left) / 2;if (f(mid) == target) {// 问自己:题目是求左边界还是右边界?// ...} else if (f(mid) < target) {// 问自己:怎么让 f(x) 大一点?// ...} else if (f(mid) > target) {// 问自己:怎么让 f(x) 小一点?// ...}}return left;
}
具体来说,想要用二分搜索算法解决问题,分为以下几步:
1、确定x, f(x), target
分别是什么,并写出函数f
的代码。
2、找到x
的取值范围作为二分搜索的搜索区间,初始化left
和right
变量。
3、根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码。
下面用几道例题来讲解这个流程。
例题一、珂珂吃香蕉
这是力扣第 875 题「爱吃香蕉的珂珂」:
珂珂每小时最多只能吃一堆香蕉,如果吃不完的话留到下一小时再吃;如果吃完了这一堆还有胃口,也只会等到下一小时才会吃下一堆。
他想在警卫回来之前吃完所有香蕉,让我们确定吃香蕉的最小速度K
。函数签名如下:
int minEatingSpeed(int[] piles, int H);
那么,对于这道题,如何运用刚才总结的套路,写出二分搜索解法代码?
按步骤思考即可:
1、确定x, f(x), target
分别是什么,并写出函数f
的代码。
自变量x
是什么呢?回忆之前的函数图像,二分搜索的本质就是在搜索自变量。
所以,题目让求什么,就把什么设为自变量,珂珂吃香蕉的速度就是自变量x
。
那么,在x
上单调的函数关系f(x)
是什么?
显然,吃香蕉的速度越快,吃完所有香蕉堆所需的时间就越少,速度和时间就是一个单调函数关系。
所以,f(x)
函数就可以这样定义:
若吃香蕉的速度为x
根/小时,则需要f(x)
小时吃完所有香蕉。
代码实现如下:
// 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉
// f(x) 随着 x 的增加单调递减
int f(int[] piles, int x) {int hours = 0;for (int i = 0; i < piles.length; i++) {hours += piles[i] / x;if (piles[i] % x > 0) {hours++;}}return hours;
}
target
就很明显了,吃香蕉的时间限制H
自然就是target
,是对f(x)
返回值的最大约束。
2、找到x
的取值范围作为二分搜索的搜索区间,初始化left
和right
变量。
珂珂吃香蕉的速度最小是多少?多大是多少?
显然,最小速度应该是 1,最大速度是piles
数组中元素的最大值,因为每小时最多吃一堆香蕉,胃口再大也白搭嘛。
这里可以有两种选择,要么你用一个 for 循环去遍历piles
数组,计算最大值,要么你看题目给的约束,piles
中的元素取值范围是多少,然后给right
初始化一个取值范围之外的值。
我选择第二种,题目说了1 <= piles[i] <= 10^9
,那么我就可以确定二分搜索的区间边界:
public int minEatingSpeed(int[] piles, int H) {int left = 1;// 注意,right 是开区间,所以再加一int right = 1000000000 + 1;// ...
}
3、根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码。
现在我们确定了自变量x
是吃香蕉的速度,f(x)
是单调递减的函数,target
就是吃香蕉的时间限制H
,题目要我们计算最小速度,也就是x
要尽可能小:
这就是搜索左侧边界的二分搜索嘛,不过注意f(x)
是单调递减的,不要闭眼睛套框架,需要结合上图进行思考,写出代码:
public int minEatingSpeed(int[] piles, int H) {int left = 1;int right = 1000000000 + 1;while (left < right) {int mid = left + (right - left) / 2;if (f(piles, mid) == H) {// 搜索左侧边界,则需要收缩右侧边界right = mid;} else if (f(piles, mid) < H) {// 需要让 f(x) 的返回值大一些right = mid;} else if (f(piles, mid) > H) {// 需要让 f(x) 的返回值小一些left = mid + 1;}}return left;
}
PS:关于mid
是否需要 + 1 的问题,前文 二分搜索算法详解 进行了详细分析,这里不展开了。
至此,这道题就解决了,现在可以把多余的 if 分支合并一下,最终代码如下:
public int minEatingSpeed(int[] piles, int H) {int left = 1;int right = 1000000000 + 1;while (left < right) {int mid = left + (right - left) / 2;if (f(piles, mid) <= H) {right = mid;} else {left = mid + 1;}}return left;
}// f(x) 随着 x 的增加单调递减
int f(int[] piles, int x) {// 见上文
}
PS:我们代码框架中多余的 if 分支主要是帮助理解的,写出正确解法后建议合并多余的分支,可以提高算法运行的效率。
例题二、运送货物
再看看力扣第 1011 题「在 D 天内送达包裹的能力」:
要在D
天内按顺序运输完所有货物,货物不可分割,如何确定运输的最小载重呢?
函数签名如下:
int shipWithinDays(int[] weights, int days);
和上一道题一样的,我们按照流程来就行:
1、确定x, f(x), target
分别是什么,并写出函数f
的代码。
题目问什么,什么就是自变量,也就是说船的运载能力就是自变量x
。
运输天数和运载能力成反比,所以可以让f(x)
计算x
的运载能力下需要的运输天数,那么f(x)
是单调递减的。
函数f(x)
的实现如下:
// 定义:当运载能力为 x 时,需要 f(x) 天运完所有货物
// f(x) 随着 x 的增加单调递减
int f(int[] weights, int x) {int days = 0;for (int i = 0; i < weights.length; ) {// 尽可能多装货物int cap = x;while (i < weights.length) {if (cap < weights[i]) break;else cap -= weights[i];i++;}days++;}return days;
}
对于这道题,target
显然就是运输天数D
,我们要在f(x) == D
的约束下,算出船的最小载重。
2、找到x
的取值范围作为二分搜索的搜索区间,初始化left
和right
变量。
船的最小载重是多少?最大载重是多少?
显然,船的最小载重应该是weights
数组中元素的最大值,因为每次至少得装一件货物走,不能说装不下嘛。
最大载重显然就是weights
数组所有元素之和,也就是一次把所有货物都装走。
这样就确定了搜索区间[left, right)
:
public int shipWithinDays(int[] weights, int days) {int left = 0;// 注意,right 是开区间,所以额外加一int right = 1;for (int w : weights) {left = Math.max(left, w);right += w;}// ...
}
3、需要根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码。
现在我们确定了自变量x
是船的载重能力,f(x)
是单调递减的函数,target
就是运输总天数限制D
,题目要我们计算船的最小载重,也就是x
要尽可能小:
这就是搜索左侧边界的二分搜索嘛,结合上图就可写出二分搜索代码:
public int shipWithinDays(int[] weights, int days) {int left = 0;// 注意,right 是开区间,所以额外加一int right = 1;for (int w : weights) {left = Math.max(left, w);right += w;}while (left < right) {int mid = left + (right - left) / 2;if (f(weights, mid) == days) {// 搜索左侧边界,则需要收缩右侧边界right = mid;} else if (f(weights, mid) < days) {// 需要让 f(x) 的返回值大一些right = mid;} else if (f(weights, mid) > days) {// 需要让 f(x) 的返回值小一些left = mid + 1;}}return left;
}
到这里,这道题的解法也写出来了,我们合并一下多余的 if 分支,提高代码运行速度,最终代码如下:
public int shipWithinDays(int[] weights, int days) {int left = 0;int right = 1;for (int w : weights) {left = Math.max(left, w);right += w;}while (left < right) {int mid = left + (right - left) / 2;if (f(weights, mid) <= days) {right = mid;} else {left = mid + 1;}}return left;
}int f(int[] weights, int x) {// 见上文
}
本文就到这里,总结来说,如果发现题目中存在单调关系,就可以尝试使用二分搜索的思路来解决。搞清楚单调性和二分搜索的种类,通过分析和画图,就能够写出最终的代码。
_____________
学好算法靠套路,认准 labuladong,知乎、B站账号同名。公众号后台回复「微信」可加我好友。
我写了一个套路,助你随心所欲运用二分搜索相关推荐
- 我写了一个软件,差点被投入监狱!
2014年2月,爱德华·斯诺登与<卫报>记者 Glenn Greenwald 第一次联系时,斯诺登提出了一个额外的要求:为了保证通信安全,记者需要在电脑上安装一个软件. 斯诺登甚至向记者发 ...
- java编程游戏飞机旋转问题_用JAVA写的一个飞机游戏
根据网上视频教程,用JAVA写的一个飞机程序.先看效果先: 改程序主要应用了面向对象的方法构建,使用了AWT技术和一些双缓冲技术 关键有几个点: 一.双缓冲技术,防止重画造成的屏幕闪烁,可以直接用 p ...
- 在纸上写好一个c语言程序后,上机运行的基本步骤为,c基本概念(选择题).docx
PAGE / NUMPAGES 一.单选题 1.下面叙述中正确的是: (A) 在C语言程序中,main()函数必须放在程序的开始位置 (B) 在C语言程序中,要调用的函数必须在main()函数中定义 ...
- 怎样写出一个较好的高速排序程序
写出一个较好的高速排序程序 高速排序是经常使用的排序算法之中的一个,但要想写出一个又快又准的使用程序,就不是那么简单了 须要注意的事项 首先要写正确.通常使用递归实现.其递归相当于二叉树展开,因此假设 ...
- linux mysql 不稳定_linux,mysql:今天写出一个十分弱智的bug!
今天写出一个十分弱智的bug,记录一下,提醒自己以后别这种犯错,不怕丢人哈~ 在写一个分页查询记录的sql时,要根据添加的时间逆序分页输出,之前的写法是酱紫: select record.a, y.c ...
- 分享下自己写的一个微信小程序请求远程数据加载到页面的代码
分享下自己写的一个微信小程序请求远程数据加载到页面的代码 1 思路整理 就是页面加载完毕的时候 请求远程接口,然后把数据赋值给页面的变量 ,然后列表循环 2 js相关代码 我是改的 onload ...
- 用jQuery写的一个翻页,并封装为插件,
用jQuery写的一个翻页,并封装为插件, 1 *{ 2 margin:0; 3 padding: 0; 4 list-style: none; 5 text-decoration: none; 6 ...
- 利用反射自己写的一个ModelHelper类
开发中 很多人都会使用BLL Model这种开发,我也是,虽然现在有很多的自动生成工具,能在几秒内生成cs的模板,但我个人还不是很喜欢,我还是喜欢自己一个一个去写,这样更能了解自己的代码. 不过手动编 ...
- 我写了一个脚本,可在“任意”服务器上执行命令!
冰河之前维护着上千台服务器组成的服务器集群,如果每次需要在服务器上执行命令的时候,都要手动登录每台服务器进行操作的话,那也太麻烦了.你想想,如果在上千台服务器的集群中,每台服务器中只需要简单的执行一个 ...
最新文章
- 某月某日前包括当天吗_创恒国际投资平台介绍股票交易制度:股票是否可以当天买?当天卖?...
- 3000 字推荐一个可视化神器,50 行 Python 代码制作数据大屏
- C++实现平衡二叉树
- 删除二叉搜索树中的节点
- 编写下载服务器。 第一部分:始终流式传输,永远不要完全保留在内存中
- 产品认知:产品经理进入新公司第一件事该做什么?
- freecodecamp_我在1个月内完成了整个freeCodeCamp课程(并记录了所有内容)
- 【现代版】为人处世三十六计详解,真的很受益!
- 操作系统安全 基本概念
- Power Query M语言全部list函数,快速分类掌握
- 全国idc 机房大全
- 用计算机求正有理数算术平方根的步骤,用计算器求算术平方根、用有理数估计算术平方根的大小.doc...
- 2018年个人学习计划总结
- Cartesian coordinate system
- 进入BeOS的花花世界 系列五
- 小程序和APP谁将主导未来?
- 渗透测试类型(白盒测试、黑盒测试)和漏洞扫描器
- Matlab 矩阵的LU分解矩阵(公开代码)
- lisp写标高线_属性块形式的标高标注程序! - AutoLISP/Visual LISP 编程技术 - CAD论坛 - 明经CAD社区 - Powered by Discuz!...
- UILabel attributedText
热门文章
- 基于STM32的12864液晶理解
- php检测硬件代码,查看“硬件设置”的源代码
- linux fat32 乱码,FAT32文件系统乱码的研究和分析
- 台式计算机如何拆硬盘,机械硬盘怎么拆开?台式机3.5英寸机械硬盘拆卸方法图文教程...
- http://nianjian.xiaze.com/tags.php?/%E6%BD%BC%E5%85%B3%E5%B9%B4%E9%89%B4/1/13595315666/
- 三大运营商充话费送手机,里面的套路太深
- 用Python画哆啦A梦
- IC圈的世界杯 | 论芯片设计的胜利十一人
- 转给你身边的工程师!从零开始搭建一个完整AGV控制系统
- 联想E420麦克风没有声音的方案