• Leetcode 1687. 从仓库到码头运输箱子 [四种解法] 动态规划 & 从朴素出发详细剖析优化步骤


    你有一辆货运卡车,你需要用这一辆车把一些箱子从仓库运送到码头。这辆卡车每次运输有 箱子数目的限制 和 总重量的限制 。
    
    给你一个箱子数组 boxes 和三个整数 portsCount, maxBoxes 和 maxWeight ,其中 boxes[i] = [ports​​i​, weighti] 。
    
        ports​​i 表示第 i 个箱子需要送达的码头, weightsi 是第 i 个箱子的重量。
        portsCount 是码头的数目。
        maxBoxes 和 maxWeight 分别是卡车每趟运输箱子数目和重量的限制。
    
    箱子需要按照 数组顺序 运输,同时每次运输需要遵循以下步骤:
    
        卡车从 boxes 队列中按顺序取出若干个箱子,但不能违反 maxBoxes 和 maxWeight 限制。
        对于在卡车上的箱子,我们需要 按顺序 处理它们,卡车会通过 一趟行程 将最前面的箱子送到目的地码头并卸货。如果卡车已经在对应的码头,那么不需要 额外行程 ,箱子也会立马被卸货。
        卡车上所有箱子都被卸货后,卡车需要 一趟行程 回到仓库,从箱子队列里再取出一些箱子。
    
    卡车在将所有箱子运输并卸货后,最后必须回到仓库。
    
    请你返回将所有箱子送到相应码头的 最少行程 次数。
    
     
    
    示例 1:
    
    输入:boxes = [[1,1],[2,1],[1,1]], portsCount = 2, maxBoxes = 3, maxWeight = 3
    输出:4
    解释:最优策略如下:
    - 卡车将所有箱子装上车,到达码头 1 ,然后去码头 2 ,然后再回到码头 1 ,最后回到仓库,总共需要 4 趟行程。
    所以总行程数为 4 。
    注意到第一个和第三个箱子不能同时被卸货,因为箱子需要按顺序处理(也就是第二个箱子需要先被送到码头 2 ,然后才能处理第三个箱子)。
    
    示例 2:
    
    输入:boxes = [[1,2],[3,3],[3,1],[3,1],[2,4]], portsCount = 3, maxBoxes = 3, maxWeight = 6
    输出:6
    解释:最优策略如下:
    - 卡车首先运输第一个箱子,到达码头 1 ,然后回到仓库,总共 2 趟行程。
    - 卡车运输第二、第三、第四个箱子,到达码头 3 ,然后回到仓库,总共 2 趟行程。
    - 卡车运输第五个箱子,到达码头 3 ,回到仓库,总共 2 趟行程。
    总行程数为 2 + 2 + 2 = 6 。
    
    示例 3:
    
    输入:boxes = [[1,4],[1,2],[2,1],[2,1],[3,2],[3,4]], portsCount = 3, maxBoxes = 6, maxWeight = 7
    输出:6
    解释:最优策略如下:
    - 卡车运输第一和第二个箱子,到达码头 1 ,然后回到仓库,总共 2 趟行程。
    - 卡车运输第三和第四个箱子,到达码头 2 ,然后回到仓库,总共 2 趟行程。
    - 卡车运输第五和第六个箱子,到达码头 3 ,然后回到仓库,总共 2 趟行程。
    总行程数为 2 + 2 + 2 = 6 。
    
    示例 4:
    
    输入:boxes = [[2,4],[2,5],[3,1],[3,2],[3,7],[3,1],[4,4],[1,3],[5,2]], portsCount = 5, maxBoxes = 5, maxWeight = 7
    输出:14
    解释:最优策略如下:
    - 卡车运输第一个箱子,到达码头 2 ,然后回到仓库,总共 2 趟行程。
    - 卡车运输第二个箱子,到达码头 2 ,然后回到仓库,总共 2 趟行程。
    - 卡车运输第三和第四个箱子,到达码头 3 ,然后回到仓库,总共 2 趟行程。
    - 卡车运输第五个箱子,到达码头 3 ,然后回到仓库,总共 2 趟行程。
    - 卡车运输第六和第七个箱子,到达码头 3 ,然后去码头 4 ,然后回到仓库,总共 3 趟行程。
    - 卡车运输第八和第九个箱子,到达码头 1 ,然后去码头 5 ,然后回到仓库,总共 3 趟行程。
    总行程数为 2 + 2 + 2 + 2 + 3 + 3 = 14 。
    
     
    
    提示:
    
        1 <= boxes.length <= 105
        1 <= portsCount, maxBoxes, maxWeight <= 105
        1 <= ports​​i <= portsCount
        1 <= weightsi <= maxWeight
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    解法一: 朴素版本

    通过题目发现,我们可以很简单的抽象出一个集合状态, d p [ i ] dp[i] dp[i]即运送前i个箱子需要的最小行程次数,那么怎么进行状态计算呢?我们可以枚举最后一次运送的状态,包括[1,2,3,…maxBoxeds]个箱子,那么枚举运送这些箱子能够产生的最小次数即可。
    在这里插入图片描述

    状态集合:
        dp[i]:运送前i个箱子需要的最少行程次数
    状态计算:
        dp[i] = dp[j - 1] + cost[j, i],  (i - maxB + 1 <= j <= i)
        cost[j, i]代表第k~第i个箱子的行程次数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 时间复杂度: O ( n 3 ) O(n^3) O(n3)
    • 空间复杂度: O ( n ) O(n) O(n)
    class Solution {
        public int boxDelivering(int[][] boxes, int portsCount, int maxBoxes, int maxWeight) {
            int n = boxes.length;
            int[] dp = new int[n + 5];
            Arrays.fill(dp, 0x3f3f3f3f);
            dp[0] = 0; //初始状态为0
            for (int i = 1; i <= n; i++) {
                int sum = 0;
                for (int j = i; j >= 1 && j >= i - maxBoxes + 1; j--) {
                    sum += boxes[j - 1][1]; //累加箱子的种类之和
                    if (sum > maxWeight) break; //超过了最大重量
                    dp[i] = Math.min(dp[i], dp[j - 1] + cost(boxes, j, i));
                }
            }  
            return dp[n];
        } 
        int cost(int[][] boxes, int l, int r) {
            int ans = 2, port = boxes[l - 1][0]; //初始话为2,因为返回仓库算一次行程
            while (++l <= r) {
                if (boxes[l - 1][0] == port) continue; //只要相同,那么次数不会增加
                ans++;  //码头不相同运输次数增加1
                port = boxes[l - 1][0];
            }
            return ans;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    解法二:时间优化

    我们首先从状态计算的角度去优化:
    在这里插入图片描述

    d p [ ] dp[] dp[]数组右边的所有式子可以看作在一个窗口内,窗口的大小为maxBoxes,而我们现在要求的是窗口中的最小值,并且随着 i i i的增加窗口会向右移动。那么即转化为求滑动窗口的最小值,使用单调队列求解


    但是我们发现两段蓝色部分其实是有些地方不一样的,不一样的地方在于 c o s t cost cost的右端点是不相同的。相比于前一层来说,当前层多了一个 i i i端点。那么如何弥补这个差异呢,我们可以使用 d i f dif dif来表示 c o s t cost cost的差异值,若前一个箱子 i − 1 i-1 i1于当前箱子 i i i的码头相同,那么并不会增加运输次数,那么这次的dif为0,否则就会增加1。由于我们无法直接在队列中进行修改,那么可以考虑增加一个累加值dif,具体看代码实现。


    例如:若之前的窗口里面保存的次数为 [ 1 , 2 , 3 ] [1, 2, 3] [1,2,3],那么相对于当前进来值 d p [ i − 1 ] + c o s t [ i , i ] dp[i - 1] + cost[i, i] dp[i1]+cost[i,i]来说要加上以前的差异dif进行比较后,继续构造一个单调递增的队列求解窗口的最小值。最后,将当前的次数 d p [ i − 1 ] + c o s t [ i , i ] − d i f dp[i - 1] + cost[i, i] - dif dp[i1]+cost[i,i]dif放入队列中,减去一个dif是因为队列中保存的是一个相对的运输次数。


    同理,我们还要判断重量是否超过了 m a x W e i g h t maxWeight maxWeight, 一样的道理,我们创建一个变量 w e i wei wei来代表重量的偏差值,每次比较时,队列里面的重量要加上偏差值。


    那么最后我们的队列里面就存放3个元素值, a , b , c {a,b,c} a,b,c, 其中 a a a为该点的编号用来判断是否在窗口外, b b b为当前值的行程数, c c c为当前的重量之和。


    s u r p r i s e surprise surprise,至此可以发现我们不仅优化了第二个循环,顺带将cost函数也进行了优化。

    • 时间复杂度: O ( n ) O(n) O(n)
    • 空间复杂度: O ( n ) O(n) O(n)
    class Solution {
        public int boxDelivering(int[][] boxes, int portsCount, int maxBoxes, int maxWeight) {
            int n = boxes.length;
            int[] dp = new int[n + 5];
            Arrays.fill(dp, 0x3f3f3f3f);
            dp[0] = 0;
            Deque<int[]> q = new ArrayDeque<int[]>(); //双端队列
            int dif = 0, wei = 0;
            for (int i = 1; i <= n; i++) {
                int cur = dp[i - 1] + 2;//cur为每次滑动窗口增加的值即dp[i-1]+cost[i,i]
                dif += i >= 2 && boxes[i - 1][0] != boxes[i - 2][0] ? 1 : 0;//dif为运输累加值,由于我们无法直接在队列中进行修改,那么可以考虑增加一个累加值
                wei += boxes[i - 1][1]; //重量要加上当前箱子的重量
                while (!q.isEmpty() && q.peekLast()[1] + dif >= cur) q.pollLast(); //构造一个单调递增的队列
                q.add(new int[]{i, cur - dif, boxes[i - 1][1] - wei}); 
                //判断左端队头是否在窗口外 并且重量不能超过最大重量
                while (q.peekFirst()[0] <= i - maxBoxes || q.peekFirst()[2] + wei > maxWeight) q.pollFirst(); 
                dp[i] = q.peekFirst()[1] + dif; 
            }  
            return dp[n];
        } 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    解法三:空间优化

    利用变量优化 d p dp dp数组

    • 时间复杂度: O ( n ) O(n) O(n)
    • 空间复杂度: O ( k ) , k 为 滑 动 窗 口 大 小 O(k), k为滑动窗口大小 O(k),k
    class Solution {
        public int boxDelivering(int[][] boxes, int portsCount, int maxBoxes, int maxWeight) {
            int n = boxes.length, dp = 0;
            Deque<int[]> q = new ArrayDeque<int[]>();
            int dif = 0, wei = 0;
            for (int i = 1; i <= n; i++) {
                int cur = dp + 2;
                dif += i >= 2 && boxes[i - 1][0] != boxes[i - 2][0] ? 1 : 0;//cost[i, i] = 2
                wei += boxes[i - 1][1];
                while (!q.isEmpty() && q.peekLast()[1] + dif >= cur) q.pollLast();
                q.add(new int[]{i, cur - dif, boxes[i - 1][1] - wei}); 
                while (q.peekFirst()[0] <= i - maxBoxes || q.peekFirst()[2] + wei > maxWeight) q.pollFirst();
                dp = q.peekFirst()[1] + dif; 
            }  
            return dp;
        } 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    解法四:优先队列

    除了使用单调队列求解滑动窗口,那么还可以直接使用单调队列求解其中的最小值。

    • 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
    • 空间复杂度: O ( n ) O(n) O(n)
    class Solution {
        public int boxDelivering(int[][] boxes, int portsCount, int maxBoxes, int maxWeight) {
            int n = boxes.length, dp = 0;
            PriorityQueue<int[]> q = new PriorityQueue<int[]>((a, b)->a[1] - b[1]);
            int dif = 0, wei = 0;
            for (int i = 1; i <= n; i++) {
                int cur = dp + 2;
                dif += i >= 2 && boxes[i - 1][0] != boxes[i - 2][0] ? 1 : 0;//cost[i, i] = 2
                wei += boxes[i - 1][1]; 
                q.add(new int[]{i, cur - dif, boxes[i - 1][1] - wei}); 
                while (q.peek()[0] <= i - maxBoxes || q.peek()[2] + wei > maxWeight) q.poll();
                dp = q.peek()[1] + dif; 
            }  
            return dp;
        } 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Note: 另一种思路优化:

    我们首先去观察如何优化cost数组,发现可以使用前缀和进行优化。

    例如: [ 1 , 2 , 2 , 3 , 4 , 3 , 3 ] [1,2,2,3,4,3,3] [1,2,2,3,4,3,3],这分别是不同码头的箱子,那么怎么快速计算 [ l , r ] [l,r] [l,r]的运输次数。

    那么我们可以首先初始化第一个箱子的运输次数 s u m [ 1 ] = 0 sum[1] = 0 sum[1]=0, 若当前箱子与前一个箱子相同,那么次数不会增加 s u m [ i ] = s u m [ i − 1 ] sum[i] = sum[i -1] sum[i]=sum[i1],否则 s u m [ i ] = s u m [ i − 1 ] + 1 。 sum[i] = sum[i - 1] + 1。 sum[i]=sum[i1]+1

    最后, s u m = [ 0 , 1 , 1 , 2 , 3 , 4 , 4 ] sum=[0, 1, 1, 2, 3, 4, 4] sum=[0,1,1,2,3,4,4], 那么 c o s t [ l , r ] = s u m [ r ] − s u m [ l ] + 2 cost[l, r] = sum[r] - sum[l] + 2 cost[l,r]=sum[r]sum[l]+2。我们更新我们的状态计算如下:
    在这里插入图片描述那么利用前缀和数组计算的话,我们队列里面就只需要存储一下每个点的下标即可,例如 i − m a x B + 1 , . . . , i − 1 i-maxB+1,...,i-1 imaxB+1,...i1,每次通过下标来计算运输次数和重量即可。而解法二是直接优化前缀和数组,通过遍历答案时继续计算


    如果有问题,欢迎评论区交流, 如果有帮助到你,请给题解点个赞和收藏哈~~~

  • 相关阅读:
    8.softmax回归
    一个极简的Http请求client推荐,一行搞玩外部请求
    JavaScript系列之switch语句
    ATT&CK框架现有的14个战术进行了详细介绍
    分治算法——快排 | 归并思想
    1985-2020年全国各省一二三产业就业人数/各省分产业就业人数数据(无缺失)
    基于CNTK实现迁移学习-图像分类【附部分源码】
    Keras深度学习框架实战(5):KerasNLP使用GPT2进行文本生成
    蓝图通信、类的封装和继承——拾取物品01
    java计算机毕业设计海东市乐都区沙果线上线下销售管理平台MyBatis+系统+LW文档+源码+调试部署
  • 原文地址:https://blog.csdn.net/qq_41280600/article/details/128177023