左右指针法:二分查找-其它应用
二分查找最常见的就是在有序数组中搜索给定的某个目标值的索引。再推广一点,如果目标值存在重复,修改版的二分查找可以返回目标值的左侧边界索引或者右侧边界索引。
抛开有序数组这个枯燥的数据结构,二分查找如何运用到实际的算法问题中呢?当搜索空间有序的时候,就可以通过二分搜索「剪枝」,大幅提升效率。
以实例为说明:
爱吃香蕉的珂珂(leetcode875)
Koko 每小时最多吃一堆香蕉,如果吃不下的话留到下一小时再吃;如果吃完了这一堆还有胃口,也只会等到下一小时才会吃下一堆。在这个条件下,让我们确定 Koko 吃香蕉的最小速度(根/小时)。
那么我们先抛开二分查找技巧,想想如何暴力解决这个问题呢?
首先,算法要求的是「H
小时内吃完香蕉的最小速度」,我们不妨称为speed
,请问speed
最大可能为多少,最少可能为多少呢?
显然最少为 1,最大为max(piles)
,因为一小时最多只能吃一堆香蕉。那么暴力解法就很简单了,只要从 1 开始穷举到max(piles)
,一旦发现发现某个值可以在H
小时内吃完所有香蕉,这个值就是最小速度,也就是在连续的空间线性搜索,这就是二分查找可以发挥作用的标志。
由于我们要求的是最小速度,所以可以用一个搜索左侧边界的二分查找来代替线性搜索
class Solution {
public int minEatingSpeed(int[] piles, int H) {
int speedsmall=1;
int speedbig=getMax(piles);
while(speedsmall<=speedbig){
int speedmid=speedsmall+(speedbig-speedsmall)/2;
int hour=hourNeed(piles,speedmid);
if(hour<=H){
speedbig=speedmid-1;
}else if(hour>H){
speedsmall=speedmid+1;
}
}
return speedsmall;
}
public int getMax(int[] piles){
int max=0;
for(int i=0;i<piles.length;i++){
max=Math.max(piles[i],max);
}
return max;
}
public int hourNeed(int[] piles,int speedNow){
int need=0;
for(int i=0;i<piles.length;i++){
need=need+hourPer(piles[i],speedNow);
}
return need;
}
public int hourPer(int pileNum,int speedNow){
if(pileNum<=speedNow){return 1;}
else if(pileNum%speedNow==0){return (int)(pileNum/speedNow);}
else{return (int)(pileNum/speedNow)+1;}
}
}
本道题的关键在于:
首先要大致确定speed的范围,即使采用穷举法也要找到这个范围。
在这种连续范围的穷举法时,就可以使用二分查找。
在二分查找的查找数的应用之中,可以认为是符合要求的即返回,不符合要求的继续查找。而本题是在符合要求的时候,不直接返回,而是继续找最小值,即“搜索左侧边界的二分查找”的类型。
实例2:
在D天内传送包裹的能力(leetcode1011)
class Solution {
public int shipWithinDays(int[] weights, int D) {
int min = getMax(weights);
int max = getSum(weights);
while (min < max) {
int mid = (min + max) / 2;
if (canDeliverFinish(weights, D, mid)) {//如果能运完,说明还可以运少一点
max = mid;
} else {//如果运不完,说明要运多一点
min = mid + 1;
}
}
return min;
}
private int getMax(int[] weights) {
int temp = 0;
for (int v : weights) {
temp = Math.max(v, temp);
}
return temp;
}
private int getSum(int[] weights) {
int sum = 0;
for (int v : weights) {
sum += v;
}
return sum;
}
/**
* 按照传送带上货物的顺序,依次且尽可能多地往船上装载货物,当该艘船无法装下更多货物时,
* 我们换一搜船,同时将天数加1。当运输完所有货物后,我们判断所用的天数是否小于等于D。
*
* @param weights 货物
* @param D 天数
* @param capacity 最小值为货物中的最大值,最大值为几个货物相加的和
* @return 是否能运完
*/
private boolean canDeliverFinish(int[] weights, int D, int capacity){
int totalW = 0;
int totalD = 0;
for (int w : weights) {
if (totalW+w > capacity) {
totalD += 1;
//注意这里要重新计算
totalW = 0;
}
totalW += w;
}
//注意第一天没有算入到totalD中,所以要加1
return totalD+1 <= D;
}
}