Swift 数据结构与算法( 十) 数组 + Leetcode1011. 在 D 天内送达包裹的能力(二分)

112 阅读10分钟

难度 M

题目

1011. 在 D 天内送达包裹的能力

传送带上的包裹必须在 days 天内从一个港口运送到另一个港口。

传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量(weights)的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。

返回能在 days 天内将传送带上的所有包裹送达的船的最低运载能力。

 

示例 1:

输入: weights = [1,2,3,4,5,6,7,8,9,10], days = 5
输出: 15
解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 52 天:6, 73 天:84 天:95 天:10

请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。 

示例 2:

输入: weights = [3,2,2,4,1,4], days = 3
输出: 6
解释:
船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:
第 1 天:3, 2
第 2 天:2, 4
第 3 天:1, 4

示例 3:

输入: weights = [1,2,3,1,1], days = 4
输出: 3
解释:
第 1 天:1
第 2 天:2
第 3 天:3
第 4 天:1, 1

解题思路🙋🏻‍ ♀️

思路:

一. 找到范围区间

这个问题的关键是找到可以在规定的天数内将所有货物运输完的船的最低载重。所以,我们需要找到可能的载重范围,然后在这个范围内寻找满足条件的最低载重

  1. 下限为所有货物中的最大值:这是因为船必须能装下最重的货物。无论我们如何分配货物,每一天都必须至少装载一件货物。因此,船的载重至少要大于或等于所有货物中的最大值,否则就无法装载最重的货物。
  2. 上限为所有货物的总重量:这是因为这样的载重可以保证一天内就能运输完所有的货物。如果船的载重等于所有货物的总重量,那么我们就可以在一天内将所有的货物装载到船上,然后一次性将它们运输到目的地。因此,船的载重肯定不会超过所有货物的总重量

[下限为所有货物中的最大值, 上限为所有货物的总重量 ]

我们需要找到的 "最低载重" 就在这个组合中间.

:[下限为所有货物中的最大值....."最低载重"..... 上限为所有货物的总重量 ]

所以要使用二分法去寻找这个 "最低载重"

二. 分析运载能力

根据函数式分析.

class Solution {
    func shipWithinDays(_ weights: [Int], _ days: Int) -> Int {

    }
}

这里给出的已知条件是, weightsdays

假设我们有一组货物,重量分别为 [1,2,3,4,5,6,7,8,9,10],并且我们希望在 5 天内将所有的货物运输完。

在这个例子中,最大值是 10,总重量是 55。所以我们的左边界 left = 10,右边界 right = 55

那么我们这个区间就找到了 [下限为所有货物中的最大值, 上限为所有货物的总重量 ] = [10, 55]

然后,我们进入二分查找的循环。在循环中,我们每次都取左边界右边界中间值作为假设的载重,然后模拟装载货物的过程,看需要多少天可以装完所有的货物

初始状态. left = 10,右边界 right = 55, mid = (10 + 55) / 2 = 32

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
L                                                                 M                                                                       R
  1. 第一次循环,我们取 mid = (10 + 55) / 2 = 32。我们发现在 32 的载重下,我们需要 2 天就可以装完所有的货物。因为 2 小于 5,我们知道当前的载重可能偏大,我们应该尝试减小载重。所以我们更新 right = mid = 32
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
L                                                                 R
m

2.第二次迭代,取中间值 21,需要 3 天,小于 5 天,说明载重偏大,更新右边界 R = M = 21

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
L                                 R
L = 10, R = 32, M = (L + R) / 2 = 21

3.第三次迭代,取中间值 15,需要 5 天,等于 5 天,可能刚好或偏大,更新右边界 R = M = 12

解释: 我们计算的 mid 值为 15,这表示我们假设船只的载重为 15。然后,我们尝试将所有的货物在这个载重下运输。在这个载重下,我们需要 5 天才能装完所有的货物。这个时间等于我们的目标天数 5,这意味着我们可能已经找到了一个满足条件的载重,但我们还不确定这是否是最小的载重。因此,我们应该尝试继续减小载重看是否还能满足条件。为此,我们将右边界 R 更新为 mid,这样我们下一次迭代时计算的 mid 会更小。

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
L              R
L = 10, R = 15, M = (L + R) / 2 = 12

4.第四次迭代,取中间值 12,需要 6 天,大于 5 天,说明载重偏小,更新左边界 L = M + 1 = 13

解释: 我们计算的 mid 值为 12,表示我们现在假设船只的载重为 12。在这个载重下,我们需要 6 天才能装完所有的货物。这个时间大于我们的目标天数 5,这意味着当前的载重过小,我们无法在规定的天数内将所有的货物运输完,所以我们需要增大载重。为此,我们将左边界 L 更新为 mid + 1,这样我们下一次迭代时计算的 mid 会更大。

在二分查找中,右边界 R不会向右移动的。原因在于我们在寻找满足条件的值时,是在 LR 确定的范围内搜索。向右移动 R扩大搜索范围 (同理,向← 移动 L 也是不可以的),而我们的目标逐渐缩小搜索范围,直到找到满足条件的值。

所以, 我们只能向右移动 L

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
                 L  R
L = 13, R = 15, M = (L + R) / 2 = 14

5.第五次迭代,取中间值 14,需要 6 天,大于 5 天,说明载重偏小,更新左边界 L = M + 1 = 15

我们计算的 mid 值为 14,表示我们现在假设船只的载重为 14。在这个载重下,我们需要 6 天才能装完所有的货物。这个时间依然大于我们的目标天数 5,这意味着当前的载重仍然过小,我们仍然需要增大载重。为此,我们再次将左边界 L 更新为 mid + 1,这样我们下一次迭代时计算的 mid更大

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
                    LR
L = 15, R = 15, M = (L + R) / 2 = 15

最后. 此时,左边界右边界重合,L = R = 15 二分查找结束,我们找到的最小载重就是 15,这就是我们的结果,也就是 "最低运载能力"

边界思考🤔

  1. 最小值的边界:对于船只的最低运载能力最小值应该是所有货物中最重的那个。原因在于,无论如何,船只都需要有足够的运载能力运输最重的货物。否则,最重的货物无法被运输。所以,在寻找可能的船只运载能力的范围时,下界应该设为所有货物的最大重量
  2. 最大值的边界:对于船只的最低运载能力,最大值应该是所有货物的总重量。原因在于,如果船只的运载能力等于所有货物的总重量,那么一天内就能运输完所有的货物。所以,在寻找可能的船只运载能力的范围时,上界应该设为所有货物的总重量
  3. 二分查找的终止条件:在二分查找中,当左边界 L 和右边界 R 相遇时,查找过程就结束了。这个时候,LR 就是我们找到的最小载重。
  4. 装载过程中的边界:在模拟装载过程时,我们需要确保装载的货物重量不会超过船只的当前运载能力如果加上新的货物超过船只的运载能,那么我们就需要新开一天,并将新的货物作为新一天的第个货物。

代码

class Solution {
    func shipWithinDays(_ weights: [Int], _ days: Int) -> Int {

        if weights.count <= 0 {
           return 0
        }
        // 获取最大最小 区间  
         var right = weights.reduce(0,+)
         var left = weights.max() ?? 0
         

         // 二分法获取最小载重
         
         while left < right {
             //获取 mid, mid 是暂定的最小载重
             let mid = left + (right - left)/2
             // 我们至少需要一天的时间。即使我们的船只载重非常大,能够在一天内装载完所有的货物,我们也至少需要一天的时间。
             //因此,我们把 need 初始化为 1,表示最初我们至少需要一天。
             //need 是天数
             var need = 1
             //cur 是载重
             var cur = 0
             for item in weights {
                 cur = item + cur
                 if cur > mid {
                    need += 1 
                    cur = item
                 }
             }
             
             // 如果 need >= days  大于 5 天, 所以需要增大 mid 的载重, 那么这样需要扩大, 扩大只能 left 增加.
             // 等于也是一样,需要缩小.
             //当 need > days 时,意味着当前的 mid 值无法满足所有的货物在 days 天内送达,我们需要增大 mid 的值,也就是增大船的运载能力。
             //在二分查找中,我们通过移动左边界 left 来增大 mid 的值,即 left = mid + 1。
             //总的来说,这个 = 号的含义是,当找到一个可能的答案时,我们还要继续尝试寻找一个更小的答案。
             if need > days  {
                left =  mid + 1
             } else {
                right = mid
             }
              
         }
        return left
    }
}

时空复杂度分析

时空复杂度分析. 二分查找的时间复杂度为 O(logn),其中 n 是搜索的范围。但是在每一步的二分查找中,我们都需要对所有的货物进行一次遍历以计算所需的天数,所以每一步的时间复杂度为 O(m),其中 m 是货物的数量。因此,整体的时间复杂度为 O(mlogn)

引用