1. LeetCode 1011题解析:用C语言解决包裹运输问题
今天想和大家分享一道LeetCode上的经典二分查找应用题——1011. Capacity To Ship Packages Within D Days(在D天内送达包裹的能力)。这道题在亚马逊的面试中出现频率很高,考察了对二分查找的灵活应用能力。作为一道中等难度题目,它完美展现了如何将算法思维应用到实际业务场景中。
题目要求我们确定一艘船的最低运载能力,使得所有包裹能在D天内按顺序运输完毕。包裹不能拆分,必须按给定顺序运输。这实际上模拟了现实中的物流调度问题——如何用最经济的运力满足时效要求。用C语言解决这个问题,不仅能锻炼我们的算法思维,还能学习如何用高效的方式处理资源分配问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与算法选择
2.1 题目重述与示例理解
给定一个weights数组表示包裹重量,days表示允许的最大运输天数。我们需要找到最小的运载能力capacity,使得所有包裹能在days天内运输完毕。
例如:
输入:weights = [1,2,3,4,5,6,7,8,9,10], days = 5
输出:15
解释:
第1天:1,2,3,4,5
第2天:6,7
第3天:8
第4天:9
第5天:10
2.2 暴力解法与优化思路
最直观的解法是从最小可能capacity开始尝试,逐步增加直到找到满足条件的最小值。最小capacity至少是最大单个包裹重量(否则无法运输),最大capacity不超过所有包裹重量之和(一天运完)。
但这种线性搜索在最坏情况下时间复杂度为O(n*sum),对于大规模数据效率极低。观察到capacity和所需天数之间存在单调关系:capacity越大,所需天数越少。这种单调性提示我们可以使用二分查找来优化。
3. 二分查找实现详解
3.1 确定搜索范围
二分查找需要明确左右边界:
- 左边界left:最大单个包裹重量(必须能运走最重的包裹)
- 右边界right:所有包裹重量之和(一天运完所有包裹)
c复制int left = 0, right = 0;
for(int i = 0; i < weightsSize; i++) {
if(weights[i] > left) left = weights[i];
right += weights[i];
}
3.2 计算所需天数的辅助函数
我们需要一个辅助函数计算给定capacity时需要的运输天数:
c复制int daysNeeded(int* weights, int weightsSize, int capacity) {
int days = 1, current = 0;
for(int i = 0; i < weightsSize; i++) {
current += weights[i];
if(current > capacity) {
days++;
current = weights[i];
}
}
return days;
}
3.3 二分查找主逻辑
标准的二分查找框架,根据中间值测试结果调整边界:
c复制while(left < right) {
int mid = left + (right - left) / 2;
if(daysNeeded(weights, weightsSize, mid) <= days) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
4. 完整C语言实现与测试
4.1 完整代码
c复制#include <stdio.h>
#include <stdlib.h>
int daysNeeded(int* weights, int weightsSize, int capacity) {
int days = 1, current = 0;
for(int i = 0; i < weightsSize; i++) {
current += weights[i];
if(current > capacity) {
days++;
current = weights[i];
}
}
return days;
}
int shipWithinDays(int* weights, int weightsSize, int days) {
int left = 0, right = 0;
for(int i = 0; i < weightsSize; i++) {
if(weights[i] > left) left = weights[i];
right += weights[i];
}
while(left < right) {
int mid = left + (right - left) / 2;
if(daysNeeded(weights, weightsSize, mid) <= days) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
int main() {
int weights1[] = {1,2,3,4,5,6,7,8,9,10};
printf("Test case 1: %d\n", shipWithinDays(weights1, 10, 5)); // Expected 15
int weights2[] = {3,2,2,4,1,4};
printf("Test case 2: %d\n", shipWithinDays(weights2, 6, 3)); // Expected 6
int weights3[] = {1,2,3,1,1};
printf("Test case 3: %d\n", shipWithinDays(weights3, 5, 4)); // Expected 3
return 0;
}
4.2 复杂度分析
- 时间复杂度:O(n log(sum)),其中n是包裹数量,sum是所有包裹重量之和。二分查找进行了log(sum)次迭代,每次迭代需要O(n)时间计算所需天数。
- 空间复杂度:O(1),只使用了常数级别的额外空间。
5. 常见问题与优化技巧
5.1 边界条件处理
在实际编码中,有几个边界条件需要特别注意:
- 空数组或days为0的情况(题目保证输入有效,但实际工程中需要考虑)
- 所有包裹重量相同的情况
- days等于1或等于包裹数量的情况
5.2 二分查找的变体
这道题展示了二分查找的一种变体应用——在有序解空间中查找满足条件的最小值。类似的模式也出现在其他问题中,如:
-
- Koko Eating Bananas(爱吃香蕉的珂珂)
-
- Split Array Largest Sum(分割数组的最大值)
5.3 性能优化技巧
- 提前终止:在计算daysNeeded时,如果当前天数已经超过目标天数,可以提前返回。
- 初始边界优化:right可以初始化为min(sum, max_weight * (n - days + 1)),缩小搜索范围。
- 并行计算:对于极大数组,可以分段计算部分和。
6. 实际应用与扩展
这道题目虽然抽象,但实际对应着许多现实场景:
- 物流运输中的车辆调度
- 生产线上的任务分配
- 云计算中的资源分配
在更复杂的变体中,可能还需要考虑:
- 多艘运输工具的情况
- 运输顺序的灵活性
- 不同运输工具的不同容量
理解这类问题的解法,不仅能帮助我们在面试中表现出色,更能培养解决实际工程问题的思维模式。二分查找看似简单,但在这类"最小化最大值"或"最大化最小值"的问题中展现出强大的威力。
