算法(六):二分查找的实际应用

59 阅读3分钟

上一章节讲了二分查找和左右边界的查找,其实一些题目是可以使用二分查找的思想来解答的,但是它不会把所有的条件准备好让我们来使用二分查找,这就需要我们自己去创造条件来使用二分查找,下面通过一道题目来具体了解:

二分查找应用初体验

875. 爱吃香蕉的珂珂:珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。
珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。
思路:速度k和吃完的时间h是成反比的,所以是满足二分查找的条件的。那么吃的速度最小是每小时1根,最大是每小时吃掉最大那堆的根数,所以我们的查找范围也已经找到了。我们的target自然是h小时,越接近h小时越好,所以我们只需要根据速度中位数算出时间,然后与target做对比,移动left和right即可,注意求的是最小速度,即左边界:

// 时间与速度的关系函数:
// 速度是speed时 吃完所有苹果需要的时间
function getTime(speed: number, piles: number[]): number {
  let hours: number = 0;
  for (let i = 0; i < piles.length; i++) {
    hours += Math.floor((piles[i] + speed - 1) / speed)
  }
  return hours;
}
function minEatingSpeed(piles: number[], h: number): number {
  let left = 1;
  let right = Math.max(...piles);
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (h >= getTime(mid, piles)) {
      // 寻找左边界所以相等是 right = mid - 1
      // 由于speed与h是负相关 所以小于target时也是收缩右边界
      right = mid - 1;
    } else {
      left = mid + 1;
    }
  }
  return left;
}

上述代码还需要解释的是hours的计算,吃掉一堆香蕉的时间其实是pile/speed,但是由于吃掉一堆只能是整数时间,所以为了取整将pile/speed转换成:(pile + speed - 1) / speed配合向下取整。当然如果你还是不明白这种转换思路,你大可以使用if...else来判断是否整除然后未被整除的向上取整。

总结

当题目符合二分查找的条件时:为了方便与target进行对比,我们会抽象出所求x的f(x)函数;然后找到x的取值范围,初始化left和right;最后根据题目判断出是求解左边界还是右边界,套用对应的模板即可;

练习

1011. 在 D 天内送达包裹的能力:传送带上的包裹必须在 days 天内从一个港口运送到另一个港口。
传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量(weights)的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。
返回能在 days 天内将传送带上的所有包裹送达的船的最低运载能力。
思路:我们要求的是船的最低运载能力,所以我们要写出关于运载能力与天数的关系函数:

function getDays(carring: number, weights: number[]): number {
  let day: number = 0;
  let curCarring = 0;
  for(let i = 0; i < weights.length; i++) {
    curCarring += weights[i]
    if (curCarring > carring) {
      day++
      curCarring = weights[i]
    } else if (curCarring == carring) {
      day++
      curCarring = 0
    }
  }
  if (curCarring) day++
  return day;
}

然后我们找出运载能力的范围,最低运载left肯定是weights中的最大值,最大运载是一船运完所有的货物,我们可以取left * weights.length,当然你也可以遍历数组求出总和。由于运载能力与所需天数是负相关,所以我们要找的是左边界,所以:

function shipWithinDays(weights: number[], days: number): number {
  let left = Math.max(...weights);
  let right = left * weights.length;
  while(left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (getDays(mid, weights) <= days) {
      right = mid - 1
    } else {
      left = mid + 1
    }
  }
  return left;
};