TYS的博客

算法小白努力学习中

0%

动态规划优化-单调队列

动态规划优化-单调队列

当状态转移的复杂度为$O(n)$时, 状态个数为n个,动态规划的算法复杂度为$O(n^2)$,可能会超时,因此对状态转移的过程进行优化,将状态转移的复杂度降低到$O(logn)$或$O(1)$

例1 多重背包的单调队列优化

N 种物品和一个容量是 V的背包。第 i种物品最多有 $s_i$ 件,每件体积是 $v_i$,价值是$w_i$。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。0<N≤1000
0<V≤20000
0<$s_i$,$v_i$,$w_i$≤20000

状态转移方程,可以按照体积进行分组,本质上是求一个滑动窗口的最大值。
$$
dp[j+mv] = max(dp[j] + mw, dp[j+v] + (m - 1)w, \dots, dp[j+mv])
$$
对上面的状态转移方程进行变形,采用单调队列进行优化,代码如下:
$$
dp[j+mv] = max(dp[j], dp[j+v] - w, \dots, dp[j+mv]) + mw
$$

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
#include<iostream>
#include<queue>
#include<stdio.h>
#include<string.h>

using namespace std;

int dp[20010], pre[20010], dq[20010];
int main() {
int n, m, v, w, s;
cin >> n >> m;

memset(dp, 0, sizeof(dp));
for (int i = 0; i < n; ++i) {
cin >> v >> w >> s;
memcpy(pre, dp, sizeof(dp));
memset(dp, 0, sizeof(dp));
for (int j = 0; j < v; ++j) {
int head = 0, tail = -1;
for (int k = j; k <= m; k += v) {
while (head <= tail && pre[k] - pre[dq[tail]] > (k - dq[tail]) / v * w) {
tail--;
}
dq[++tail] = k;
if ((k - dq[head]) / v > s) head++;
dp[k] = pre[dq[head]] + (k - dq[head]) / v * w;
}
}
}
cout << dp[m] << endl;
return 0;
}

例2 跳跃游戏VI

给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。一开始你在下标 0 处。每一步,你最多可以往前跳 k 步,但你不能跳出数组的边界, 达数组最后一个位置(下标为 n - 1 ),你的 得分 为经过的所有数字之和。请你返回你能得到的 最大得分

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

状态转移方程

$$
f[i]=\max_{max(0,i−k)≤j<i}​{f[j]}+nums[i]
$$

该状态转移方程为$O(n^2)$,但是我们仅需要前面这个滑动窗中的最大值,因此采用单调队列的方式进行优化,使得队列头部为该滑动窗的最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int maxResult(vector<int>& nums, int k) {
int n = nums.size();
deque<int> dq{0};
vector<int> dp(n, 0);
dp[0] = nums[0];
for (int i = 1; i < n; ++i) {
if (i - dq.front() > k) dq.pop_front();
int val = dp[dq.front()] + nums[i];
while (!dq.empty() && val > dp[dq.back()]) {
dq.pop_back();
}
dq.push_back(i);
dp[i] = val;
}
return dp[n - 1];
}
};