二分法的经典情况
题目介绍
传送带上的包裹必须在 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, 5
第 2 天:6, 7
第 3 天:8
第 4 天:9
第 5 天: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], D = 4
输出:3
解释:
第 1 天:1
第 2 天:2
第 3 天:3
第 4 天:1, 1
提示:
- 1 <= days <= weights.length <= 5 * 104
- 1 <= weights[i] <= 500
题目解析
容易想偏的dp思路
小提醒
如果仅想学习二分法的应用和了解经典情景的话,可以跳过这一部分
为什么要插入这样的dp思路分析呢,因为我动态规划往往是解题的利器,能用动态规划考虑的一般都很不错,但是本题给了我一个提醒,那就是重要的是时间复杂度分析,判读一个算法的好坏与可用性不是用了多么酷的方法,而是时间复杂度是否合适
另一方面,既然不是用dp思路的题反而用dp思路想出来,可见过程也是比较复杂的,也可以作为一种锻炼
dp思路解析
受到背包问题采用动态规划思路的影响,该题很容易也想成动态规划
而且我想到的状态转移方程为
创建dp数组,来存放运送下标个数货物要付出的代价,然后从1天,2天考虑到D天,不断更新dp数组
那么状态转移方程就是,假如我们第n+1天运送m个货物,那么n+1会把m个货物分割为两部分,m1,m2
m1是又是经过n天的划分,其最好的结果已经保留在我们dp数组中,而m2仅需要把货物累加起来就是第n+1天付出的代价了
上述只是m个货物,第n+1天的一种情况,我们要把m分割成如下
货物m1 | 货物m2 | n+1天m货物最小运输代价 |
---|---|---|
0 | 1-m | Math.max(dp[0],sum(cost[1]-cost[m])) |
0-1 | 2-m | Math.max(dp[1],sum(cost[2]-cost[m])) |
0-2 | 3-m | Math.max(dp[2],sum(cost[3]-cost[m])) |
0-(m-2) | (m-1)-m | Math.max(dp[m-2],sum(cost[m-1]-cost[m])) |
最后n+1天m货物最小运输代价 要取第三列的最小值。
dp code
class Solution {
public int shipWithinDays(int[] weights, int days) {
//如果天数太多,就取决于货物最重的那一个
if(days>=weights.length){
Arrays.sort(weights);
return weights[weights.length-1];
}
int[] array=weights;
//利用差分,避免每次求和 array[j]-array[i]=x从i到j array[x]的和
for (int i = 1; i < array.length; i++) {
array[i]+=array[i-1];
}
int[] dp=array;
for (int i = 1; i < days; i++) {
System.out.println(Arrays.toString(dp));
int[] dp_new=new int[weights.length];
for (int j = 0; j < dp_new.length; j++) {
for (int k = 0; k < j; k++) {
int temp=Math.max(dp[k],array[j]-array[k]);
if(dp_new[j]!=0){
dp_new[j]=Math.min(temp,dp_new[j]);
}else{
dp_new[j]=temp;
}
}
}
dp=dp_new;
}
System.out.println(Arrays.toString(dp));
return dp[dp.length-1];
}
}
测试用例以及分析
[1,2,3,4,5,6,7,8,9,10]
5
[1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
[0, 2, 3, 6, 9, 11, 15, 21, 24, 28]
[0, 2, 3, 4, 6, 9, 11, 15, 17, 21]
[0, 2, 3, 4, 5, 6, 9, 11, 15, 17]
[0, 2, 3, 4, 5, 6, 7, 9, 11, 15]
15
通过上述代码,可以输出打赢这样的一个二维数组,可以很骄傲的说第i行和第j列代表i+1天运输j个货物花费的最小代价。
存在的缺点
通过好几个for循环也可以看出,这是一个时间复杂度爆炸的算法,幸亏还是用了点dp思想,不然就好像是暴力遍历一般 (甚至有时候能想出一个暴力遍历就很难)
设为n天,货物数组长度为m,简单分析一下时间复杂度为
(n * m * m)
二分法
怎么使用二分法呢
其实思路很简单,就是最小运送货物能力也要大于等于最重的一个货物,设此为left
最大运送货物的能力肯定小于货物重量的总和,设为right
因此我们二分收缩找个区域即可,如果mid满足,就让right降低,反过来提高
如果满足我们
code
class Solution {
public int shipWithinDays(int[] weights, int days) {
int sum=0;
int max=0;
for (int i = 0; i < weights.length; i++) {
sum+=weights[i];
max=Math.max(weights[i],max);
}
if(days>=weights.length)return max;
int left=Math.max(sum/days,max);
int right=sum;
while (right>=left){
int mid=(right+left)/2;
if(canFinish(mid,weights,days)){
right=mid-1;
}else {
left=mid+1;
}
}
return left;
}
public boolean canFinish(int val, int[]weights, int days){
int temp=0;
for (int i = 0; i < weights.length; i++) {
if(temp+weights[i]<=val){
temp+=weights[i];
}else {
temp=weights[i];
days--;
if(days<=0)return false;
}
}
return true;
}
}
时间复杂度为 nlongΣw
Σw 是数组weights 中元素的和
n为weights数组的长度
leecode传送门
以下是本题以及两道相似思路的题目:
https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days/
https://leetcode-cn.com/problems/xiao-zhang-shua-ti-ji-hua/
https://leetcode-cn.com/problems/koko-eating-bananas/