本文记录最近与朋友聊过的算法实例:给定一堆带重量的货物,计算最少需要的货车数量。

业务算法系列:收录业务系统开发中真实遇到的算法

背景

题目描述:

给定一个int[],weights [100,300]范围,每个元素表示货物重量的数组

求装完给定货物最少的车辆数

限制:每个货物最多能装300的货物

货物重量数组长度为n,货物重量区间为m

思路

过了两天已经想不起我最开始的错误思路了,所以本文就聚焦来看正确思路吧。

关于这道题跟湧哥聊了一轮,他给了我一个解题的窍门:针对这种题目,对于入参有常数级的限制,因此可以考虑一些特定的做法,比如对数据排序可以考虑桶排序,对于数据查找可以粗暴遍历(因为复杂度是常数级)。

正确的解题思路:

  1. 对货物按照重量排序;
  2. 针对每辆车,尽可能找当前剩余最重且当前车能装的下的货物(贪心);
  3. 重复这个过程,一直每辆车都装满或者装不下剩余货物中最轻的;

第一步:排序

对于排序,最粗暴的方式就是使用快排,针对本题,由于货物重量区间为[100,300],是一个常量区间,因此可以使用桶排序,使用一个201长度的数组即可统计货物重量以及数量,同时一次桶排序仅需遍历一次入参数组,时间复杂度为O(n)

在排序这一步,桶排序优于快排。

第二步:找当前装的下最重的货物

本题中一辆车最多能装三个100,因此查找当前装得下最重的货物可以粗暴遍历,从当前车辆剩余容量开始逐个遍历即可,最多遍历201次。

拓展:如果入参可以很大,这里的查找可以考虑二分法,这样可以保证较大数据集的时候,查找效率最高。

第三步:重复装车

这一步只要维护好边界,能够及时跳出循环即可。

代码实现

思路描述完了,下面展示我两种思路下的代码实现:

桶排序+遍历查找

 1
 2
 3
 4
 5
 6
 7
 8
 9
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
/**
 * 观察题目给定的条件,发现货物重量区间为 [100, 300],是一个常数级别的区间,因此我们可以尝试两个操作:
 * 1. 给定的重量排序,可以使用桶排序,排序的复杂度能达到O(n),要优于快排这种方案;
 * 2. 每辆车去找当前能装得下的最大货物时,可直接遍历查找(因为逐个遍历也就是O(201)的复杂度,是一个常数级复杂度);
 */
public static class BucketSortSolution {

   public int findMinimumQuantityOfCars(int[] weights) {
      // 排序后的桶,维护货物重量->货物数量
      // 复杂度:O(n)
      // 一共n个货物
      int bucketN = 300 - 100 + 1;
      int[] weight2Cnt = new int[bucketN];
      for (int weight : weights) {
            weight2Cnt[weight - 100]++;
      }

      int ans = 0;
      while (true) {
            // 标记当前车辆有无货物可装,一辆车最多装3个货
            int carWeightCnt = 3;
            // 当前车辆剩余可装容量
            int remainWeight = 300;
            // 每辆车尽可能装满
            // 从大的货物开始装
            for (int i = remainWeight - 100; i >= 0; i--) {
               int currWeight = i + 100;
               if (remainWeight < currWeight || weight2Cnt[i] == 0) {
                  // 当前货物太重了,看下一个比较轻的
                  // 当前重量没有货物
                  continue;
               }
               while (remainWeight >= currWeight && weight2Cnt[i] > 0) {
                  remainWeight -= currWeight;
                  weight2Cnt[i]--;
                  carWeightCnt--;
               }
            }
            if (carWeightCnt == 3) {
               // 已经没有货物了
               break;
            }
            ans++;
      }
      return ans;
   }
}

复杂度

空间复杂度,使用了一个额外的数组维护重量对应数量,复杂度O(m)

时间复杂度,排序遍历一次入参数组,每辆车查找遍历x次排序数组(x为一个常量),时间复杂度最大为O(m*n)

红黑树(或者可以考虑java中的跳表结构)

 1
 2
 3
 4
 5
 6
 7
 8
 9
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
56
57
58
59
60
61
/**
 * 使用有序集合(树、跳表)的方案思路:
 * 1. 使用红黑树记录每个重量对应的货物数量;
 * 2. 每辆车从当前剩余
 */
public static class BinarySearchCollectionSolution {
   /**
   * 整体复杂度:O(n*logn)
   * 限制:每辆车可以装载最大300的货物
   *
   * @param weights [100,300]范围,每个元素表示货物重量的数组
   * @return 求装完给定货物最少的车辆数
   */
   public int findMinimumQuantityOfCars(int[] weights) {
      int ans = 0;
      // 每个重量的货 对应 数量
      TreeMap<Integer, Integer> weight2Count = new TreeMap<>(Comparator.naturalOrder());
      // 树形成复杂度 O(n*logn)
      for (int weight : weights) {
            weight2Count.put(weight, weight2Count.getOrDefault(weight, 0) + 1);
      }
      // 每辆车先选中一个当前最大的货,重量x,然后去找小于且最接近(当前剩余容量-x)的货,如果此时(当前剩余容量-x)小于100则装下一辆
      // 每一轮选中某个货物后,从treemap有序结构中移除货物计数

      while (true) {
            if (weight2Count.size() == 0) {
               break;
            }
            // 从最重的货物开始装
            // 获取最大值复杂度 O(1)
            Integer maxWeight = weight2Count.lastKey();
            // 更新最重货物量
            updateTargetWeightCount(weight2Count, maxWeight);

            if (weight2Count.size() == 0) {
               break;
            }
            int remainWeight = 300 - maxWeight;
            while (weight2Count.size() > 0 && weight2Count.firstKey() <= remainWeight) {
               // 这里使用树形结构查询,二分法复杂度 O(logn)
               Integer maxLowerWeight = weight2Count.floorKey(remainWeight);
               // 更新 小于且最接近(当前剩余容量-x)的货量
               updateTargetWeightCount(weight2Count, maxLowerWeight);
               remainWeight -= maxLowerWeight;
            }
            ans++;
      }
      return ans;
   }

   // 更新某个key O(1)
   private void updateTargetWeightCount(TreeMap<Integer, Integer> weight2Count, Integer maxLowerWeight) {
      Integer maxLowerWeightCount = weight2Count.get(maxLowerWeight);
      if (maxLowerWeightCount - 1 > 0) {
            weight2Count.put(maxLowerWeight, maxLowerWeightCount - 1);
      } else {
            weight2Count.remove(maxLowerWeight);
      }
   }
}

复杂度

空间复杂度,使用了一个额外的树形map维护货物重量,复杂度O(n)

时间复杂度,每次查找通过二分法找到小于等于特定重量的元素,时间复杂度O(logn),这个过程最多查找n次,因此整体时间复杂度O(n*logn)

小结

对于题目给定的较小数据集,桶排序+遍历查找是更加直观的解法,同时湧哥几分钟内就实现了一版,他能够快速找到这道题给定条件的突破点并且实现,这点给了我启发:对于特定条件要有条件反射,小数据集有对应的套路。

而对于更大或者更通用的数据集,则可以按照我实现的树结构、跳表结构来解题。

两种算法各有优劣,对于本题货车装货的实际场景,桶排序的方式更高效,只有当m n变量变的更大时,二分的解法才更高效。