单调队列(滑动窗口最大值)
从「维护单调性」的角度上来说,单调队列和单调栈是一样的,一个弹出队尾元素,另一个弹出栈顶元素。在单调栈的基础上,单调队列多了一个「移除队首」的操作,这类似滑动窗口移动左指针
left
的过程。所以从某种程度上来说,单调队列 = 单调栈 + 滑动窗口。
单调队列套路分为三步:
-
入:符合条件的元素进入窗口「队尾」,同时维护队列单调性
-
出:不符合条件的元素离开窗口「队首」
-
记录\维护答案「队首」(这里单调队列无论是单增还是单减,队首一定是最符合条件的)
总结:及时去掉无用数据,保证双端队列有序
- 当前元素>=队尾,弹出队尾(单减队列,和单调栈一样)
- 弹出队尾不在窗口内的元素
适用的问题:
- 滑动窗口的最大值
- 满足条件的连续区间长度
单调队列是一种队列,它同样保持单调递增或单调递减。使用单调队列的场景包括:
- 在一个滑动窗口「区间」中求解最值问题。
- 求解图中的最短路径问题。
题单:+ 难度分
- 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()
:获取结算商品中的最高价格,如果队列为空,则返回 -1add(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 开始的整数数组 chargeTimes
和 runningCosts
,两者长度都为 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| <= k
且 1 <= 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.length
,points[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]
,它们在原数组中的下标 i
和 j
满足 i < j
且 j - 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
, maxBoxes
和 maxWeight
,其中 boxes[i] = [portsi, weighti]
。
portsi
表示第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 趟行程。
- 卡车运输第五个箱子,到达码头 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];
}
}