1. 问题背景与题目解析
今天我们来解决LeetCode上的1710题"卡车上的最大单元数"。这是一道典型的贪心算法应用题,题目描述如下:
假设你有一辆卡车,需要装载一些箱子。每个箱子有两种属性:
- 箱子数量(numberOfBoxes)
- 每个箱子包含的单元数(numberOfUnitsPerBox)
给定一个二维数组boxTypes,其中boxTypes[i] = [numberOfBoxesi, numberOfUnitsPerBoxi],以及一个整数truckSize表示卡车可以装载的最大箱子数。我们需要计算卡车可以装载的最大单元总数。
举个例子:
输入:boxTypes = [[1,3],[2,2],[3,1]], truckSize = 4
输出:8
解释:
- 1个第一类箱子,共3个单元
- 2个第二类箱子,共4个单元
- 1个第三类箱子,共1个单元
总计单元数 = 3 + 4 + 1 = 8
2. 贪心算法思路分析
2.1 贪心策略选择
这道题的核心在于如何选择箱子组合使得总单元数最大化。贪心算法的基本思想是:在每一步选择中都采取当前状态下最优的选择,从而希望导致全局最优的结果。
对于本题,最优策略是:
- 优先选择单位箱子单元数最多的箱子类型
- 尽可能多地装载这类箱子
- 当这类箱子用完或卡车容量不足时,转向次优的箱子类型
2.2 为什么贪心算法适用
贪心算法适用于本题的原因在于:
- 问题具有最优子结构:全局最优解可以通过局部最优解组合得到
- 贪心选择性质:局部最优选择能导致全局最优解
- 箱子类型之间相互独立,选择一种箱子不会影响其他箱子的选择
注意:不是所有问题都适合贪心算法。只有当问题满足贪心选择性质和最优子结构时,贪心算法才能得到全局最优解。
3. Java实现详解
3.1 排序预处理
首先需要对箱子类型进行排序,按照每个箱子的单元数从大到小排列:
java复制Arrays.sort(boxTypes, (a, b) -> b[1] - a[1]);
这里使用了Java 8的Lambda表达式作为Comparator,相当于:
java复制Arrays.sort(boxTypes, new Comparator<int[]>() {
@Override
public int compare(int[] a, int[] b) {
return b[1] - a[1];
}
});
3.2 贪心装载过程
排序后,我们可以按顺序处理每种箱子类型:
java复制int totalUnits = 0;
int remainingSize = truckSize;
for (int[] box : boxTypes) {
if (remainingSize <= 0) break;
int boxesTaken = Math.min(box[0], remainingSize);
totalUnits += boxesTaken * box[1];
remainingSize -= boxesTaken;
}
这段代码的关键点:
- 维护remainingSize变量跟踪剩余卡车容量
- 每次尽可能多地取当前最优箱子类型
- 当卡车装满时立即终止循环
3.3 完整解决方案
结合上述两部分,完整的解决方案如下:
java复制import java.util.Arrays;
class Solution {
public int maximumUnits(int[][] boxTypes, int truckSize) {
// 按照单元数降序排序
Arrays.sort(boxTypes, (a, b) -> b[1] - a[1]);
int totalUnits = 0;
int remainingSize = truckSize;
for (int[] box : boxTypes) {
if (remainingSize <= 0) break;
int boxesTaken = Math.min(box[0], remainingSize);
totalUnits += boxesTaken * box[1];
remainingSize -= boxesTaken;
}
return totalUnits;
}
}
4. 复杂度分析与优化
4.1 时间复杂度
算法的时间复杂度主要由排序决定:
- 排序时间复杂度:O(n log n),其中n是boxTypes的长度
- 贪心选择过程:O(n)
因此总时间复杂度为O(n log n)
4.2 空间复杂度
- 排序使用O(log n)的栈空间(Java的Arrays.sort实现)
- 其他变量使用常数空间
因此空间复杂度为O(log n)
4.3 可能的优化
对于本题而言,当truckSize远小于箱子类型数量n时,可以考虑在排序后提前终止:
java复制Arrays.sort(boxTypes, (a, b) -> b[1] - a[1]);
int totalUnits = 0;
for (int[] box : boxTypes) {
if (truckSize <= 0) break;
int boxesTaken = Math.min(box[0], truckSize);
totalUnits += boxesTaken * box[1];
truckSize -= boxesTaken;
}
这种写法更简洁,但原理相同。
5. 边界条件与测试用例
5.1 常见边界情况
-
卡车容量为0:
- 输入:boxTypes = [[1,3],[2,2],[3,1]], truckSize = 0
- 输出:0
-
箱子数量刚好用完卡车容量:
- 输入:boxTypes = [[2,3],[3,2],[4,1]], truckSize = 5
- 输出:32 + 22 = 10
-
卡车容量大于所有箱子数量总和:
- 输入:boxTypes = [[1,3],[2,2]], truckSize = 10
- 输出:3 + 4 = 7
5.2 测试用例设计
好的测试用例应该覆盖:
- 正常情况
- 边界条件
- 极端情况
建议的测试用例:
java复制@Test
public void testMaximumUnits() {
Solution solution = new Solution();
// 正常情况
assertEquals(8, solution.maximumUnits(new int[][]{{1,3},{2,2},{3,1}}, 4));
// 卡车容量为0
assertEquals(0, solution.maximumUnits(new int[][]{{1,3},{2,2},{3,1}}, 0));
// 卡车容量大于所有箱子
assertEquals(7, solution.maximumUnits(new int[][]{{1,3},{2,2}}, 10));
// 只有一种箱子
assertEquals(10, solution.maximumUnits(new int[][]{{5,2}}, 5));
// 箱子单元数相同
assertEquals(15, solution.maximumUnits(new int[][]{{3,5},{4,5}}, 3));
}
6. 贪心算法的应用场景
贪心算法在解决最优化问题时非常高效,常见的应用场景包括:
- 背包问题(如本题)
- 任务调度问题
- 最小生成树(Prim和Kruskal算法)
- 最短路径问题(Dijkstra算法)
- 哈夫曼编码
- 区间调度问题
在实际应用中,贪心算法往往能提供简单高效的解决方案,但需要注意:
- 并非所有问题都适合贪心算法
- 需要证明问题具有贪心选择性质
- 有时贪心算法只能得到近似解而非最优解
7. 类似题目推荐
为了巩固贪心算法的理解,推荐以下LeetCode题目:
-
- 分发饼干
-
- 无重叠区间
-
- 用最少数量的箭引爆气球
-
- 买卖股票的最佳时机 II
-
- 种花问题
-
- 划分字母区间
-
- 根据身高重建队列
这些题目都体现了贪心算法的核心思想:通过局部最优选择达到全局最优解。每道题目都有其独特的贪心策略,值得一一研究。
8. 实际工程中的应用思考
虽然本题是一个算法题目,但类似的场景在实际工程中经常出现:
- 资源分配问题:如何将有限的资源分配给不同的任务以获得最大收益
- 缓存淘汰策略:当缓存空间不足时,应该优先淘汰哪些数据
- 任务调度:如何安排任务顺序以最大化吞吐量或最小化等待时间
理解贪心算法的核心思想,能够帮助我们在面对这类问题时快速找到合理的解决方案。在实际编码中,我通常会:
- 先明确问题的优化目标
- 分析是否可以通过局部最优达到全局最优
- 设计合适的排序或选择策略
- 考虑边界条件和异常情况
这种系统化的思考方式,往往比直接编码更能产生高质量的解决方案。
