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

news2024/12/28 2:45:28

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

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

单调队列套路分为三步:

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

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

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

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

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

适用的问题:

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

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

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

题单:+ 难度分

  • 1438. 绝对差不超过限制的最长连续子数组 1672
  • 2398. 预算内的最多机器人数目 1917
  • 862. 和至少为 K 的最短子数组 2307
  • 1499. 满足不等式的最大值 2456

单调队列优化DP

  • 1425. 带限制的子序列和 2032
  • 1687. 从仓库到码头运输箱子 2610

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];
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1321652.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

牛目标检测数据集VOC+YOLO格式4000张

牛是一种古老的哺乳动物&#xff0c;被人类驯化了数千年&#xff0c;成为了人类重要的家畜之一。它们是一种大型草食性动物&#xff0c;主要生活在草原、森林和农村地区。牛的体型较大&#xff0c;体长可达3米&#xff0c;体重可达1500千克。 牛是一种非常有用的动物&#xff0…

【TB作品】STM32 PWM之实现呼吸灯,STM32F103RCT6,晨启

文章目录 完整工程参考资料实验过程 实验任务&#xff1a; 1&#xff1a;实现PWM呼吸灯&#xff0c;定时器产生PWM&#xff0c;控制实验板上的LED灯亮灭&#xff1b; 2&#xff1a;通过任意两个按键切换PWM呼吸灯输出到两个不同的LED灯&#xff0c;实现亮灭效果&#xff1b; 3&…

提升数据中心网络效率:100G QSFP28 LR4光模块的优势分析

数字信息的急剧增长&#xff0c;对高速、高容量网络的需求愈发迫切&#xff0c;数据传输的速度和距离成为了一个关键的挑战。在这个背景下&#xff0c;100G QSFP28 LR4光模块作为一款性能卓越的光模块&#xff0c;为远距离高速传输提供了全新的解决方案。 该产品是专为符合100G…

【网络面试必问(8)】防火墙原理、正向代理、反向代理、缓存服务器、负载均衡和内容分发服务器

接上一篇&#xff1a;【网络面试必问&#xff08;7&#xff09;】聊聊集线器、交换机和路由器 作为一个程序员&#xff0c;其实很少去了解http请求消息在到达服务器之前&#xff0c;所经过的众多组件的&#xff0c;今天借着机会聊一聊标题中提到的正向代理、反向代理、缓存服务…

分段管理及段页管理

一、 分段 程序的逻辑关系被划分为不同的段&#xff0c;每个段有一个段名&#xff0c;并且每个段都从0开始编址。这些段在内存中分配&#xff0c;每个段占据连续的内存空间&#xff0c;但不同段之间可以不相邻。 这种分段管理有一些特点和优势&#xff1a; 逻辑划分&#xff1…

SpringBoot已经禁掉了循环依赖!

还在问循环依赖嘛&#xff1f;SpringBoot已经禁掉了循环依赖&#xff01; 首发2023-12-18 11:26yuan人生 如果现在面试时还有人问你循环依赖&#xff0c;你就这样怼他&#xff1a;循环依赖是一种代码质量低下的表现&#xff0c;springboot2.6之后的版本已经默认禁用了。 Spr…

计算机与自动医疗检查仓:技术革新引领医疗未来

计算机与自动医疗检查仓&#xff1a;技术革新引领医疗未来 一、引言 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;和机器学习&#xff08;ML&#xff09;已经成为现代社会不可或缺的一部分。它们的应用领域日益扩展&#xff0c;从简单的日常任务到复杂…

云原生系列2-GitLab和Jenkins

1、GitLab类似github&#xff0c;是个私有仓库 1、GitLab安装&#xff0c;至少8G内存4核cpu # 查找Gitlab镜像 docker search gitlab/gitlab-ce # gitlab镜像拉取 docker pull gitlab/gitlab-ce # 查看镜像 docker images # 本机先建3个目录&#xff0c;为了gitlab容器通过挂…

【C语言】自定义类型:结构体深入解析(一)

&#x1f308;write in front :&#x1f50d;个人主页 &#xff1a; 啊森要自信的主页 ✏️真正相信奇迹的家伙&#xff0c;本身和奇迹一样了不起啊&#xff01; 欢迎大家关注&#x1f50d;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;>希望看完我的文章对你有小小的帮助&am…

Moonbeam生态项目分析 — — 跨链借贷协议Orbiter One

概览 Orbiter One是一个非托管的借贷协议和DeFi中心&#xff0c;专注于跨链互操作性。通过使用从借贷中赚取的ORB Token铸造的Intergactic Whiskers Brigade NFT&#xff0c;用户可以质押并获得额外奖励&#xff0c;借贷和跨链存款则可以在不离开Moonbeam的情况下无缝参与其他…

听说蚂蚁的职级调整了

上周三听说蚂蚁的职级调整了&#xff0c;让我们来看一下具体的改革方案&#xff1a; 简单地说&#xff0c;就是把原来的 PN 级一拆二&#xff0c;拆成 2N 和 2N1 级。 从本质上来看&#xff0c;就是把原来扁平化的宽职级变多了&#xff0c;相当于 double 了。 那职级变多有什…

初探 Reactor、Proactor 线程模型与 BIO、AIO、NIO

1 前言 工作中或者是技术上经常会遇到 I/O 、线程模型相关的问题&#xff0c;以及同步、异步、阻塞、非阻塞等各种基础问题&#xff0c;之前上学时候的概念认知总是模糊的&#xff0c;一知半解。趁这次了解希望能够更加深入的去了解这方面的知识&#xff0c;于是有了接下来这篇…

AWS 知识一:如何在AWS上启动云AD服务器(详细到极致)

前言&#xff1a; 首先这里指的云AD服务器&#xff0c;只是为了让读友更好理解。云AD服务器在AWS中称为目录。AWS一共提供了4种目录类别&#xff0c;下面我将全程使用AWS托管微软AD这种目录类别进行示例。他完全提供了和Microsoft AD的功能&#xff0c;包括NTLM&#xff0c;Ker…

Android-Binder基本原理

一、进程角度看IPC机制 在Android系统中&#xff0c;每个进程只能运行在自己所拥有的虚拟地址空间。例如&#xff0c;一个4GB的虚拟地址空间&#xff0c;包含3GB的用户空间和1GB的内核空间&#xff0c;内核空间的大小可以通过参数配置进行调整。两个进程之间的用户空间是彼此独…

如何开发一个免费的App

开发一个免费App意味着能够在项目启动初期&#xff0c;以更低成本的方式进行业务的迭代和市场化验证。 互联网发展到2023年&#xff0c;尤其在生成式AI及大模型技术“跃进式”增长的背景下&#xff0c;一个创新式商业模式的起步变得异常艰难。但如果用好工具&#xff0c;那么不…

【网络安全】—计算机网络基础

文章目录 网络必备基础物理层数据链路层与交换机网络模型OSI/TCP对等传输虚拟局域网VLAN静态路由与配置网络地址转换NAT访问控制列表ACLIP协议与IP地址分类子网掩码网关子网划分总结 计算机网络是指将地理位置不同的、功能独立的多台计算机通过通信线路连接起来&#xff0c;以功…

前端基础Vue项目中的插槽使用

概念 简单理解就是组件内部留一个或多个的插槽位置&#xff0c;可供组件传对应的模板代码进去。插槽的出现&#xff0c;让组件变的更加灵活。 1. 匿名插槽 父组件 <son><p>我是父组件通过匿名插槽传输的内容</p></son> 子组件 <template><di…

透过清澈的眼眸:新生儿视力检测的重要性与留意事项

引言&#xff1a; 新生儿的视力发展是其整体感知和认知能力的基础。因此&#xff0c;进行新生儿视力检测是保障他们健康成长的关键一步。本文将深入探讨新生儿视力检测的重要性&#xff0c;并提供父母在这一过程中需要留意的关键事项&#xff0c;以确保宝宝在视觉方面的正常发…

50ms时延工业相机

华睿工业相机A3504CG000 参数配置&#xff1a; 相机端到端理论时延&#xff1a;80ms 厂家同步信息&#xff0c;此款设备帧率上线23fps&#xff0c;单帧时延&#xff1a;43.48ms&#xff0c;按照一图缓存加上传输显示的话&#xff0c;厂家预估时延在&#xff1a;80ms 厂家还有…

2018年第七届数学建模国际赛小美赛C题共享单车对城市交通的影响解题全过程文档及程序

2018年第七届数学建模国际赛小美赛 C题 共享单车对城市交通的影响 原题再现&#xff1a; 共享自行车改变了许多城市的交通状况&#xff0c;许多大城市引入共享自行车来解决交通问题。我们需要定量评估共享自行车对城市交通的影响&#xff0c;以及相关的经济、社会和环境影响。…