1687. 从仓库到码头运输箱子
分析
运输的过程中只能按boxes中的顺序进行搬运,而且每次搬运的不能操作规定的箱子数量k,且也不能超过规定的重量m。那么每次最多也就会遍历k个箱子。会产出如下的情况
- 如果n个码头在boxes中是连续的,那么卡车不用进行运送,即重量和路程在这个码头中不参与计算
- 如果k个箱子中的重量和小于等于k,那么可以直接运送这一次
- 如果k个箱子的重量大于k,那么需要对其进行切割,进行多次运送
- 每一次从仓库出发到码头再回来,都是一去一回,所以具有两个行程,而在各个码头直接,只存在一次行程
我们需要计算的是运送前 i个箱子需要的最少行程次数,如果前i个箱子的最少行程次数是最优解,那么前i-1个箱子的最少行程次数也一定是最优解。那么公式可以简写为:f(i) = f(i-1) + M
,其中的M是连接两个函数之间的关系函数。
对于M的分析:
- 如果i和i-1是在同一个码头,那么次数不会增加,则m = 0
- 如果i和i-1不在同一个码头,但是第i次的货物可以装下,那么则只需要第i-1个码头到第i个码头的行程次数;即 m= 1
- 如果i和i-1不在同一个码头,且第i次的货物装不下了,那么久需要第i次单独配送,则需要的是一个来回,即m= 2
到这里的分析似乎结束了,因为好像对于M的分析分析不下去了。
但是我们可以换 一个角度来看:
如果f(i)是最优解,那么在i之前,肯定存在于一个数x,这个数和一个数y,在boxes中的x到y可以一次性的运输到各个码头再回来,而y+1到i的时候也可以在一次运输来解决(y+1 <= i),那么我们可以通过码头运算次数来列出关系:f(1_i) = f(1_y) + M(y+1 _ i) + 2
,解释这个关系式:在boxes中的第1个数到最后一个数的最少行程次数,等于第1个数到第y个数的最少行程次数 + 最后一次行程中经过的非连续相同码头的个数+一次 往返次数2
对于M(y+1 _ i),我们可以用一次遍历来得到从第1个数到第i个数的非连续码头个数,然后M(1_i) - M(1_y+1) 即为这两个数的差值。
如下:M(1_7) - M(1_4+1) = 5 - 4 = 1,即第5次到第7次只需一次行程即可
1 | 2 | 2 | 3 | 4 | 4 | 5 |
---|---|---|---|---|---|---|
公式变为:f(1_i) = f(1_y) - M(1 _ y+1) + M(1 _ i) + 2
当求最后一次的时候,i是确认的数,所以M(1 _ i)+2就是一个常数,所以需要的是f(1_y) - M(1 _ y+1)
的最优解,当递归到边界条件的时候,即第一次的行程f(1 _ z)- M(1 _ z+1)
,这个f(1 _ z) = 0
,
我们继续分析每次运送的时候的一些条件:
定义子母含义:
- w(k): 前k个箱子的总重量(不含k)
- j: 每次运送的开始点
- i:下一次运送的开始点
- maxBoxes: 每次运送的最多箱子
- maxWeight: 每次运送的最大重量
以上含有两个关系:
// 每次运送的重量不能超过总重量
w(i) - w(j) <= maxWeight
// 每次运送的箱子个数不能操作总个数
i - j <= maxBoxes
将不等式进行变化:
w(j) >= w(i) - maxWeight // 也是在确实批次的开始
j >= i - maxBoxes // 确定运送批次的开始
到目前为止,每个y都需要对之前的j进行判断,判断哪些j的为止是符合条件的,复杂的为n的平方,所以我们需要降低复杂度
j到i是成递增的方式正在的,w(j)到w(i)也是成递增方式增长的,如果某一个数i = j(0) ,它使得两个等式中的某一个关系不成立,那么任何i > j(0)的数也不会成立,且j(0)也永远不会成立。
设函数:g(1_y) = f(1_y) - M(1 _ y+1)
,如果现在有两个数j(0)和j(1),且j(0)<j(1),那么对于函数g来说,有两种情况
- 如果g(j(0)) < g(j(1)),那么当j(0)满足限制的时候,g(0)较小,是最优解,保留;当j(0)不满足条件时,j(1)比j(0)大,所以根据上述关系时,j(1)可能满足条件,且g(1)可能也是最优解
- 如果g(j(0)) >= g(j(1)),只需要将j(1)保留下来即可,因为当j(0)满足限制的是,j(1)不会更差,但是j(1)的值比j(0)更大,能存放更多的东西
以上两种情况是leetcode的官方解题的解答,但是我们还可以将这两种情况进行具体的分析:以下的1和0是j(1)和j(0)的简写,那么根据公式可以进行如下推导
g(1-0) = [f(1) - f(0)] - [neg(1+1) - neg(0+1)]
f(1) - f(0) >= 0 ======>> 相同的码头,等于,不同的码头+1 || +3
neg(1+1) - neg(0+1) >= 0 相同的码头,等于,不同的码头 + 1
所以:有三种情况:
f(1) - f(0) = 0 ,neg(1+1) - neg(0+1) = 1 ===> g(1) < g(0)
f(1) - f(0) = 1 ,neg(1+1) - neg(0+1) = 1 ===> g(1) = g(0)
f(1) - f(0) = 0 ,neg(1+1) - neg(0+1) = 0 ===> g(1) = g(0)
f(1) - f(0) = 1 ,neg(1+1) - neg(0+1) = 0 ===> g(1) > g(0)
f(1) - f(0) = 3 ====> g(1) > g(0)
让我们再将g(1) >= g(0)和上面官方的两种情况
- g(1) = g(0):1和0是相同的码头,或者是加上1之后,仍然可以一次的运送完货物,即送完0的码头再送1的码头。此时的g(1)-g(0) = 0 - 0 || 1 - 1 = 0
- g(1) > g(0):1和0不是在相同的码头,且送完0之后不能送1,所以1需要从新开一次运送。而此时的g(1)-g(0) = 3 - 1 = 2。或者是1和0是在不同的码头,但是1和2又是在相同的码头,对于后面又同一个码头的,优先放在一起配送
- g(1) < g(0):1和0是在同一个码头,但是1和2不是在同一个码头,这个时候可以优先将1和0放在一起配送
当g(0) >= g(1),后者的j值更大,满足条件的值越多,所以保留后者
当g(0) < g(1),前者的值小,保留前者,但是对于后者来说,它的j值更大,所以当j(0)不满足条件的时候,j(1)更符合条件
图解
f = g + meg[i] + 2
所以求f的次数需要g的次数最小,g最小的值就是队列种g的最前面的值
如果之前的g大于等于此次的g,则说明之前的g不是最优解,所以弹出之前的g,加入当前的g:当前的g值最小
注意:f(i)才是运送此时的最优解,g(i)只是一个代表f(i)的一个中间函数的过程
官方解题的步骤解析
class Solution {
public int boxDelivering(int[][] boxes, int portsCount, int maxBoxes, int maxWeight) {
// 初始化数据
int n = boxes.length;
int[] p = new int[n + 1];
int[] w = new int[n + 1];
int[] neg = new int[n + 1];
long[] W = new long[n + 1];
// boxes[i] = [第i个码头, 货物的重量]
for (int i = 1; i <= n; ++i) {
// 将boxes中的数据转换为两个单独数组中的数据
p[i] = boxes[i - 1][0];
w[i] = boxes[i - 1][1];
// 从第二个开始,如果前一个码头和后一个的码头相同,则的neg+1
if (i > 1) {
neg[i] = neg[i - 1] + (p[i - 1] != p[i] ? 1 : 0);
}
// 到第i个位置的重量
W[i] = W[i - 1] + w[i];
}
// 以上初始化完成
// 创建一个队列,这是一个双端队列
Deque<Integer> opt = new ArrayDeque<Integer>();
// 在末尾放一个初始值:0
opt.offerLast(0);
int[] f = new int[n + 1];
int[] g = new int[n + 1];
for (int i = 1; i <= n; ++i) {
// j >= i - maxBoxes ==> i - j <= maxBoxes
// 以下是j0 不满足条件,则将队列中的j移除,
while (i - opt.peekFirst() > maxBoxes || W[i] - W[opt.peekFirst()] > maxWeight) {
opt.pollFirst();
}
// 算出fi,从队列的头部取出g的值,该值一定是g的最小值
f[i] = g[opt.peekFirst()] + neg[i] + 2;
// 没到最后一位
if (i != n) {
// 这是记:g[i] = f[i] - neg[i + 1],得到gi
g[i] = f[i] - neg[i + 1];
// 如果队列不为空,并且得到队列中最后的一个数,这个数和gi比较,如果最后一个数比gi大,证明最后一个数不是最优解
// 需要移除最后一个数,重新加入
while (!opt.isEmpty() && g[i] <= g[opt.peekLast()]) {
opt.pollLast();
}
// 把当前数加入到队列的末尾中
opt.offerLast(i);
}
}
return f[n];
}
}
后言
该题的解题分析有些难度,到发表为止,仍然有些地方感觉理解的不够好。而且官方的解题方法中也没有详细的解说,若之后找到能够理解的解释,会会进行补充。也希望各位大佬对不足之处进行评论