14.单调队列(滑动窗口最大值)、单调队列优化DP【灵神基础精讲】

单调队列(滑动窗口最大值)

从「维护单调性」的角度上来说,单调队列和单调栈是一样的,一个弹出队尾元素,另一个弹出栈顶元素。在单调栈的基础上,单调队列多了一个「移除队首」的操作,这类似滑动窗口移动左指针 left 的过程。所以从某种程度上来说,单调队列 = 单调栈 + 滑动窗口

单调队列套路分为三步:

  1. 入:符合条件的元素进入窗口「队尾」,同时维护队列单调性

  2. 出:不符合条件的元素离开窗口「队首」

  3. 记录\维护答案「队首」(这里单调队列无论是单增还是单减,队首一定是最符合条件的)

总结:及时去掉无用数据,保证双端队列有序

  • 当前元素>=队尾,弹出队尾(单减队列,和单调栈一样)
  • 弹出队尾不在窗口内的元素

适用的问题:

  • 滑动窗口的最大值
  • 满足条件的连续区间长度

单调队列是一种队列,它同样保持单调递增或单调递减。使用单调队列的场景包括:

  • 在一个滑动窗口「区间」中求解最值问题。
  • 求解图中的最短路径问题。

题单:+ 难度分

单调队列优化DP


239. 滑动窗口最大值

困难

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

示例 2:

输入:nums = [1], k = 1
输出:[1]

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104
  • 1 <= k <= nums.length

https://leetcode.cn/problems/sliding-window-maximum/solutions/2499715/shi-pin-yi-ge-shi-pin-miao-dong-dan-diao-ezj6/

在这里插入图片描述

在这里插入图片描述

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        int[] res = new int[n-k+1];
        // 维护一个单调递减队列,队首最大值,队尾最小值
        Deque<Integer> dq = new ArrayDeque<>();
        for(int i = 0; i < n; i++){
            // 入:符合条件的元素进入窗口
            while(!dq.isEmpty() && nums[dq.peekLast()] <= nums[i])
                dq.pollLast();
            dq.addLast(i);
            // 出:不符合条件的元素推出窗口
            if(!dq.isEmpty() && dq.peekFirst() == i - k)
                dq.pollFirst();
            // 统计答案
            if(i+1 >= k){
                res[i-k+1] = nums[dq.peekFirst()];
            }
        }
        return res;
    }
}

单调队列练习

面试题 59-II. 队列的最大值【单调队列模板题】

中等

设计一个自助结账系统,该系统需要通过一个队列来模拟顾客通过购物车的结算过程,需要实现的功能有:

  • get_max():获取结算商品中的最高价格,如果队列为空,则返回 -1
  • add(value):将价格为 value 的商品加入待结算商品队列的尾部
  • remove():移除第一个待结算的商品价格,如果队列为空,则返回 -1

注意,为保证该系统运转高效性,以上函数的均摊时间复杂度均为 O(1)

示例 1:

输入: 
["Checkout","add","add","get_max","remove","get_max"]
[[],[4],[7],[],[],[]]

输出: [null,null,null,7,4,7]

示例 2:

输入: 
["Checkout","remove","get_max"]
[[],[],[]]

输出: [null,-1,-1]

提示:

  • 1 <= get_max, add, remove 的总操作数 <= 10000
  • 1 <= value <= 10^5
class Checkout {
    //其实本质上是一个求滑动窗口最大值的问题。这个队列可以看成是一个滑动窗口,
    //入队就是将窗口的右边界右移,出队就是将窗口的左边界右移。

    Deque<Integer> list;
    Deque<Integer> dq;

    public Checkout() {
        list = new ArrayDeque<>();
        //维护一个单调递增队列,记录结算商品最大值,队首为最大值
        dq = new ArrayDeque<>();
    }
    
    public int get_max() {
        if(list.isEmpty()) 
            return -1;
        return dq.peekFirst();
    }
    
    public void add(int value) {
        list.addLast(value);
        while(!dq.isEmpty() && dq.peekLast() <= value){ // 队尾不如新来的强
            dq.pollLast();
        }
        dq.addLast(value);
    }
    
    public int remove() {
        if(list.isEmpty())
            return -1;
        int x = list.pollFirst();
        if(!dq.isEmpty() && dq.peekFirst() == x) 
            dq.pollFirst();
        return x;
    }
}

1438. 绝对差不超过限制的最长连续子数组

中等

给你一个整数数组 nums ,和一个表示限制的整数 limit,请你返回最长连续子数组的长度,该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit

如果不存在满足条件的子数组,则返回 0

示例 1:

输入:nums = [8,2,4,7], limit = 4
输出:2 
解释:所有子数组如下:
[8] 最大绝对差 |8-8| = 0 <= 4.
[8,2] 最大绝对差 |8-2| = 6 > 4. 
[8,2,4] 最大绝对差 |8-2| = 6 > 4.
[8,2,4,7] 最大绝对差 |8-2| = 6 > 4.
[2] 最大绝对差 |2-2| = 0 <= 4.
[2,4] 最大绝对差 |2-4| = 2 <= 4.
[2,4,7] 最大绝对差 |2-7| = 5 > 4.
[4] 最大绝对差 |4-4| = 0 <= 4.
[4,7] 最大绝对差 |4-7| = 3 <= 4.
[7] 最大绝对差 |7-7| = 0 <= 4. 
因此,满足题意的最长子数组的长度为 2 。

示例 2:

输入:nums = [10,1,2,4,7,2], limit = 5
输出:4 
解释:满足题意的最长子数组是 [2,4,7,2],其最大绝对差 |2-7| = 5 <= 5 。

示例 3:

输入:nums = [4,2,2,2,4,4,2,2], limit = 0
输出:3

提示:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^9
  • 0 <= limit <= 10^9

方法一:单调队列

class Solution {
    /**
    使用滑动窗口,维护当前窗口的的最大值和最小值是经典的问题
    单调队列只能维护一种单调性,而题目又要求我们同时考虑最大值和最小值的差,所以就用两个呗,后边的解决方法也就顺其自然了。
     */
    public int longestSubarray(int[] nums, int limit) {
        Deque<Integer> dqmx = new ArrayDeque<>();
        Deque<Integer> dqmn = new ArrayDeque<>();
        int res = 0;
        int left = 0;
        for(int i = 0; i < nums.length; i++){
            // 滑动窗口最小值,单增队列
            while(!dqmn.isEmpty() && nums[dqmn.peekLast()] > nums[i])
                dqmn.pollLast();
            dqmn.addLast(i);
            // 滑动窗口最大值,单减队列
            while(!dqmx.isEmpty() && nums[dqmx.peekLast()] < nums[i])
                dqmx.pollLast();
            dqmx.addLast(i);
            // 出:根据窗口的最小值和最大值的差更新窗口
            while(nums[dqmx.peekFirst()] - nums[dqmn.peekFirst()] > limit){
                if(nums[left] == nums[dqmx.peekFirst()])
                    dqmx.pollFirst();
                if(nums[left] == nums[dqmn.peekFirst()])
                    dqmn.pollFirst();
                left++;
            }
            res = Math.max(res, i - left + 1);
        }
        return res;
    }
}

方法二:利用有序哈希表

class Solution {
    public int longestSubarray(int[] nums, int limit) {
        TreeMap<Integer, Integer> map = new TreeMap<>();
        int res = 0;
        int left = 0;
        for(int right = 0; right < nums.length; right++){
            map.merge(nums[right], 1, Integer::sum);
            while(left <= right && Math.abs(map.lastKey() - map.firstKey()) > limit){
                map.merge(nums[left], -1, Integer::sum);
                if(map.get(nums[left]) == 0) map.remove(nums[left]);
                left++;
            }
            res = Math.max(res, right - left + 1);
        }
        return res;
    }
}

2398. 预算内的最多机器人数目

困难

你有 n 个机器人,给你两个下标从 0 开始的整数数组 chargeTimesrunningCosts ,两者长度都为 n 。第 i 个机器人充电时间为 chargeTimes[i] 单位时间,花费 runningCosts[i] 单位时间运行。再给你一个整数 budget

运行 k 个机器人 总开销max(chargeTimes) + k * sum(runningCosts) ,其中 max(chargeTimes) 是这 k 个机器人中最大充电时间,sum(runningCosts) 是这 k 个机器人的运行时间之和。

请你返回在 不超过 budget 的前提下,你 最多 可以 连续 运行的机器人数目为多少。

示例 1:

输入:chargeTimes = [3,6,1,3,4], runningCosts = [2,1,3,4,5], budget = 25
输出:3
解释:
可以在 budget 以内运行所有单个机器人或者连续运行 2 个机器人。
选择前 3 个机器人,可以得到答案最大值 3 。总开销是 max(3,6,1) + 3 * sum(2,1,3) = 6 + 3 * 6 = 24 ,小于 25 。
可以看出无法在 budget 以内连续运行超过 3 个机器人,所以我们返回 3 。

示例 2:

输入:chargeTimes = [11,12,19], runningCosts = [10,8,7], budget = 19
输出:0
解释:即使运行任何一个单个机器人,还是会超出 budget,所以我们返回 0 。

提示:

  • chargeTimes.length == runningCosts.length == n
  • 1 <= n <= 5 * 104
  • 1 <= chargeTimes[i], runningCosts[i] <= 105
  • 1 <= budget <= 1015
class Solution {
    // 在 不超过 budget 的前提下,你 最多 可以 【连续】 运行的机器人数目为多少
    // 维护一个单调队列 + 双指针,每次入队right,然后检查 不符合条件则退出left至符合条件
    public int maximumRobots(int[] chargeTimes, int[] runningCosts, long budget) {
        // 维护一个单调递增队列,队首元素 > 队尾元素值
        Deque<Integer> q = new ArrayDeque<>();
        int res = 0;
        long sum = 0l;
        // 枚举区间右端点 right,计算区间左端点 left 的最小值
        for(int left = 0, right = 0; right < chargeTimes.length; right++){
            // 及时清除队列中的无用数据,保证队列的单调性
            while(!q.isEmpty() && chargeTimes[right] >= chargeTimes[q.peekLast()]){
                q.pollLast();
            }
            q.addLast(right);
            sum += runningCosts[right];
            // 如果左端点(队首) left 不满足要求,就不断右移 left
            while(!q.isEmpty() && 
                        chargeTimes[q.peekFirst()] + (right - left + 1) * sum > budget){
                // 及时清除队列中无用的数据,保证队列单调性
                if(q.peekFirst() == left) q.pollFirst();
                sum -= runningCosts[left++];
            }
            res = Math.max(res, right - left + 1);
        }
        return res;
    }
}

1499. 满足不等式的最大值

困难

给你一个数组 points 和一个整数 k 。数组中每个元素都表示二维平面上的点的坐标,并按照横坐标 x 的值从小到大排序。也就是说 points[i] = [xi, yi] ,并且在 1 <= i < j <= points.length 的前提下, xi < xj 总成立。

请你找出 yi + yj + |xi - xj|最大值,其中 |xi - xj| <= k1 <= i < j <= points.length

题目测试数据保证至少存在一对能够满足 |xi - xj| <= k 的点。

示例 1:

输入:points = [[1,3],[2,0],[5,10],[6,-10]], k = 1
输出:4
解释:前两个点满足 |xi - xj| <= 1 ,代入方程计算,则得到值 3 + 0 + |1 - 2| = 4 。第三个和第四个点也满足条件,得到值 10 + -10 + |5 - 6| = 1 。
没有其他满足条件的点,所以返回 4 和 1 中最大的那个。

示例 2:

输入:points = [[0,0],[3,0],[9,2]], k = 3
输出:3
解释:只有前两个点满足 |xi - xj| <= 3 ,代入方程后得到值 0 + 0 + |0 - 3| = 3 。

提示:

  • 2 <= points.length <= 10^5
  • points[i].length == 2
  • -10^8 <= points[i][0], points[i][1] <= 10^8
  • 0 <= k <= 2 * 10^8
  • 对于所有的1 <= i < j <= points.lengthpoints[i][0] < points[j][0] 都成立。也就是说,xi 是严格递增的。
class Solution {
    //  yi + yj + |xi - xj| =>  yi + yj + xj - xi => (yj + xj) + (yi - xi)
    // 枚举j,问题变成计算 yi - xi 的最大值(老员工的能力必须比新来的强。否则就淘汰)
    public int findMaxValueOfEquation(int[][] points, int k) {
        int ans = Integer.MIN_VALUE;
        // 单调队列存储二元组(xi, yi - xi)
        // 队列中 yi-xi 从队首到队尾时单调递减的,即队首时最大值
        Deque<int[]> q = new ArrayDeque<>();
        for(int[] p : points){
            int x = p[0], y = p[1];
            while(!q.isEmpty() && q.peekFirst()[0] < x - k) // 队首超出范围,即xi < xj-k
                q.pollFirst();
            if(!q.isEmpty())
                ans = Math.max(ans, x + y + q.peekFirst()[1]); // 加上最大的 yi-xi
            while(!q.isEmpty() && q.peekLast()[1] <= y - x)  // 队尾不如新来的强
                q.pollLast();
            q.addLast(new int[]{x, y-x});
        }
        return ans;
    }
}

🚀862. 和至少为 K 的最短子数组

困难

给你一个整数数组 nums 和一个整数 k ,找出 nums 中和至少为 k最短非空子数组 ,并返回该子数组的长度。如果不存在这样的 子数组 ,返回 -1

子数组 是数组中 连续 的一部分。

示例 1:

输入:nums = [1], k = 1
输出:1

示例 2:

输入:nums = [1,2], k = 4
输出:-1

示例 3:

输入:nums = [2,-1,2], k = 3
输出:3

提示:

  • 1 <= nums.length <= 105
  • -105 <= nums[i] <= 105
  • 1 <= k <= 109

https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/solutions/1925036/liang-zhang-tu-miao-dong-dan-diao-dui-li-9fvh/

class Solution {
    /**
    1. 求子数组的和 问题可以化成两个前缀和的差
    2. 求出前缀和s后,可以枚举所有子数组的开始结束位置,但是复杂度是O(n^2)的,怎么优化?
        当遍历到s[i]时,考虑左边的某个s[j],
        1)如果s[i]-s[j]>=k,那么无论s[i]右边的数字是大是小
                    都不可能「把j当左端点」得到一个比i-j更短的子数组,因此s[j]没有任何作用,弹出s[j]
        2)如果s[i] <= s[j],假如后续有数字x能和s[j]组成满足要求的子数组,即 x-s[j]>=k
                    那么必然有x-s[i]>=k,由于从s[i]到x的这段子数组更短,因此s[j]没有任何作用,弹出s[j]
        做完这两个优化后,再把s[i]加到数据结构中
    优化二保证数据结构中的s[i]会形成一个递增的序列,因此
    优化一移除的是序列最左侧的若干元素,优化二移除的是序列最右侧的若干元素。
    我们需要一个数据结构,它支持移除最左端的元素和最右端的元素,以及在最右端添加元素,故选用双端队列。
    拓展:
    如果nums的元素均为非负数,可以用双指针做,即 209. 长度最小的子数组
     */
    public int shortestSubarray(int[] nums, int k) {
        int n = nums.length, ans = n + 1;
        long[] s = new long[n+1];
        for(int i = 0; i < n; i++)
            s[i+1] = s[i] + nums[i];
        // 维护一个双端队列
        Deque<Integer> dq = new ArrayDeque<>();
        for(int i = 0; i <= n; i++){
            long curS = s[i];
            while(!dq.isEmpty() && curS - s[dq.peekFirst()] >= k)
                ans = Math.min(ans, i - dq.pollFirst()); // 优化一
            while(!dq.isEmpty() && s[dq.peekLast()] >= curS) 
                dq.pollLast(); // 优化二
            dq.addLast(i);
        }
        return ans > n ? -1 : ans;
    }
}

单调队列优化DP

单调队列就是一种队列内的元素有单调性(单调递增或者单调递减)的队列,最优解存在队首,而队尾则是最后进队的元素。

单调队列用来维护区间最值或者降低DP的维数减少空间及时间

利用单调队列对dp方程进行优化,可将O(n)复杂度降至O(1)

  • N 维的DP,可以优化为 N-1 维 !!!

单调队列适合优化决策取值范围的上、下界均单调变化的问题

并不是所有DP都可以由单调队列优化,像最大化、最小化决策的结果,即决策具有单调性的题目可以优化。


🚀1425. 带限制的子序列和

困难

给你一个整数数组 nums 和一个整数 k ,请你返回 非空 子序列元素和的最大值,子序列需要满足:子序列中每两个 相邻 的整数 nums[i]nums[j] ,它们在原数组中的下标 ij 满足 i < jj - i <= k

数组的子序列定义为:将数组中的若干个数字删除(可以删除 0 个数字),剩下的数字按照原本的顺序排布。

示例 1:

输入:nums = [10,2,-10,5,20], k = 2
输出:37
解释:子序列为 [10, 2, 5, 20] 。

示例 2:

输入:nums = [-1,-2,-3], k = 1
输出:-1
解释:子序列必须是非空的,所以我们选择最大的数字。

示例 3:

输入:nums = [10,-2,-10,-5,20], k = 2
输出:23
解释:子序列为 [10, -2, -5, 20] 。

提示:

  • 1 <= k <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

https://leetcode.cn/problems/constrained-subsequence-sum/solutions/220273/dpdan-diao-zhan-you-hua-xiang-jie-by-wangdh15/

class Solution {
    /**
    定义f[i]表示以i结尾的最大子序列和,考虑第 i 个元素选还是不选
        接在后面 f[i] = max(dp[i-j] + nums[i]) , 其中 j < i 且 i-j<=k
        不接,重新做起点 f[i] = nums[i]
        取最大值
    如果枚举所有元素作为结尾的话,时间复杂度O(nk),会超时,如何优化?
    
    由于当前时刻只依赖于前k个时刻的状态,所以只要快速找到前k个状态中的最大子序列和即可
        ==> 滑动窗口求最大值
     */
    public int constrainedSubsetSum(int[] nums, int k) {
        int n = nums.length;
        int[] f = new int[n]; // 定义f[i]表示以i结尾的最大子序列和
        // 维护单增队列,队首为最大值,保存 (以下标为结尾的子序列和,下标)
        Deque<int[]> dq = new ArrayDeque<>();
        int res = nums[0];
        for(int i = 0; i < n; i++){
            f[i] = nums[i]; // 不接
            if(!dq.isEmpty()){ // 接
                f[i] = Math.max(f[i], dq.peekFirst()[0] + nums[i]);
            }
            res = Math.max(res, f[i]);
            // 清除队列中无用的元素
            while(!dq.isEmpty() && dq.peekLast()[0] <= f[i]){
                dq.pollLast();
            }
            dq.addLast(new int[]{f[i], i});
            if(!dq.isEmpty() && dq.peekFirst()[1] == i - k)
                dq.pollFirst();
        }
        return res;
    }
}

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

困难

你有一辆货运卡车,你需要用这一辆车把一些箱子从仓库运送到码头。这辆卡车每次运输有 箱子数目的限制总重量的限制

给你一个箱子数组 boxes 和三个整数 portsCount, maxBoxesmaxWeight ,其中 boxes[i] = [portsi, weighti]

  • portsi 表示第 i 个箱子需要送达的码头, weightsi 是第 i 个箱子的重量。
  • portsCount 是码头的数目。
  • maxBoxesmaxWeight 分别是卡车每趟运输箱子数目和重量的限制。

箱子需要按照 数组顺序 运输,同时每次运输需要遵循以下步骤:

  • 卡车从 boxes 队列中按顺序取出若干个箱子,但不能违反 maxBoxesmaxWeight 限制。
  • 对于在卡车上的箱子,我们需要 按顺序 处理它们,卡车会通过 一趟行程 将最前面的箱子送到目的地码头并卸货。如果卡车已经在对应的码头,那么不需要 额外行程 ,箱子也会立马被卸货。
  • 卡车上所有箱子都被卸货后,卡车需要 一趟行程 回到仓库,从箱子队列里再取出一些箱子。

卡车在将所有箱子运输并卸货后,最后必须回到仓库。

请你返回将所有箱子送到相应码头的 最少行程 次数。

示例 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 趟行程。
- 卡车运输第五个箱子,到达码头 2 ,回到仓库,总共 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 <= portsi <= portsCount
  • 1 <= weightsi <= maxWeight

https://leetcode.cn/problems/delivering-boxes-from-storage-to-ports/solutions/2006470/by-lcbin-xwzl/

class Solution {
    /**
    定义f[i]表示运送前i个箱子需要的最小行程次数
    转移,枚举上一次运送的状态,包括[1,2,3...maxBoxeds]个箱子,i可以从j转移过来
    f[i] = f[j-1] + cost[j, i] (i - maxB+1 <= j <= i)
            cost[j, i] 表示第k~第i个箱子的行程次数
    枚举所有以i结尾的行程会超时 O(n^2),如何优化?

    实际上我们是要在[i-maxBoxeds,..i-1]这个窗口内找到一个j,
                                使得f[j] - cost[j]的值最小,
    问题变为求滑动窗口的最小值
    
    如何优化码头i到j的行程数?取决于相邻两个码头是否相等
    我们可以通过前缀和,计算出码头之间的行程数,再加上首尾两趟行程,就能O(1)计算
    重量同理
     */
    public int boxDelivering(int[][] boxes, int portsCount, int maxBoxes, int maxWeight) {
        int n = boxes.length;
        long[] ws = new long[n+1];
        int[] cs = new int[n];
        for(int i = 0; i < n; i++){
            int p = boxes[i][0], w = boxes[i][1];
            ws[i+1] = ws[i] + w;
            if(i < n-1){
                cs[i+1] = cs[i] + (p != boxes[i+1][0] ? 1 : 0);
            }
        }
        int[] f = new int[n+5];
        Deque<Integer> dq = new ArrayDeque<>();
        dq.add(0);
        for(int i = 1; i <= n; i++){
            while(!dq.isEmpty() && (i - dq.peekFirst() > maxBoxes || 
                                    ws[i] - ws[dq.peekFirst()] > maxWeight)){
                dq.pollFirst();                        
            }
            if(!dq.isEmpty()){
                f[i] = cs[i-1] + f[dq.peekFirst()] - cs[dq.peekFirst()] + 2;
            }
            if(i < n){
                while(!dq.isEmpty() && f[dq.peekLast()] - cs[dq.peekLast()] >= f[i] - cs[i]){
                    dq.pollLast();
                }
                dq.addLast(i);
            }
        }
        return f[n];
    }
}
  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值