Y22M12D05_1687_从仓库到码头运输箱子

1687. 从仓库到码头运输箱子

题目及示例

分析

运输的过程中只能按boxes中的顺序进行搬运,而且每次搬运的不能操作规定的箱子数量k,且也不能超过规定的重量m。那么每次最多也就会遍历k个箱子。会产出如下的情况

  1. 如果n个码头在boxes中是连续的,那么卡车不用进行运送,即重量和路程在这个码头中不参与计算
  2. 如果k个箱子中的重量和小于等于k,那么可以直接运送这一次
  3. 如果k个箱子的重量大于k,那么需要对其进行切割,进行多次运送
  4. 每一次从仓库出发到码头再回来,都是一去一回,所以具有两个行程,而在各个码头直接,只存在一次行程

我们需要计算的是运送前 i个箱子需要的最少行程次数,如果前i个箱子的最少行程次数是最优解,那么前i-1个箱子的最少行程次数也一定是最优解。那么公式可以简写为:f(i) = f(i-1) + M,其中的M是连接两个函数之间的关系函数。

对于M的分析:

  1. 如果i和i-1是在同一个码头,那么次数不会增加,则m = 0
  2. 如果i和i-1不在同一个码头,但是第i次的货物可以装下,那么则只需要第i-1个码头到第i个码头的行程次数;即 m= 1
  3. 如果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次只需一次行程即可

1223445

公式变为: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)也永远不会成立。

image-20221206203643503

设函数: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];
        }
    }

后言

该题的解题分析有些难度,到发表为止,仍然有些地方感觉理解的不够好。而且官方的解题方法中也没有详细的解说,若之后找到能够理解的解释,会会进行补充。也希望各位大佬对不足之处进行评论

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值