单调队列优化 dp
1. 概念
单调队列优化的本质是借助单调性,及时排除不可能的决策,保持候选集合的秩序性。
2. 例题
P1714 切蛋糕
题目大意:
给定一个序列,找出长度不超过 \(m\) 的连续子序列,使得子序列中所有数的和最大。
思路:
要求区间和,首先求出前缀和,然后考虑朴素 dp,不难想到用 \(dp[i]\) 表示包含 \(a[i]\) 的连续子序列中的最大和,那么状态转移方程为:
\[dp[i] = \max\limits_{i - m\le j\le i - 1} \{sum[i] - sum[j]\}
\]
所以朴素代码很快就能出炉:
#include <cstring>
#include <iostream>
using namespace std;
const int N = 500010;
int n, m;
int sum[N], dp[N];
int ans;
int main() {
scanf("%d%d", &n, &m);
int x;
for(int i = 1; i <= n; i++) {
scanf("%d", &x);
sum[i] = sum[i - 1] + x;
}
memset(dp, -0x3f, sizeof dp);
for(int i = 1; i <= n; i++) {
for(int j = max(i - m + 1, 0); j <= i; j++)
dp[i] = max(dp[i], sum[i] - sum[j - 1]);
ans = max(ans, dp[i]);
}
printf("%d\n", ans);
return 0;
}
时间复杂度为 \(O(n^2)\),不能够通过此题,考虑优化。
顺便提一句动态规划的优化思路:
一个原则:
在朴素代码上做等价变形。
三个方向:
- 优化状态设计,阶段和状态转移方程;
- 若有状态被多次计算或调用,则考虑加上记忆化搜索;
- 若有多层循环,考虑将与外层循环相关的变量看作定值,及时排除内层循环中的不可能决策或利用数据结构等方法优化找最值的过程,从而优化掉内层循环。
很显然,这道题我们选择方向 \(3\)。
容易发现,在外层循环到 \(i\) 时,\(sum[i]\) 是确定的,改变的只是 \(sum[j]\),所以,我们将状态转移方程整理一下,得:
\[dp[i] = sum[i] - \min\limits_{i - m\le j\le i - 1} \{sum[j]\}
\]
所以,内层循环的作用其实是寻找 \(sum\) 数组中区间 \([i - m,i - 1]\) 的最小值,且当外层 \(i\) 变为 \(i+ 1\) 时,区间变成 \([i - m + 1,i]\),这意味着只需要将 \(j = i\) 加入候选决策集合并将 \(j = i - m\) 从决策集合中移除即可。
是不是和滑动窗口如出一辙?
所以我们可以用单调队列来优化这个找最小值的过程。
\(\texttt{Code:}\)
#include <cstring>
#include <iostream>
using namespace std;
const int N = 500010;
int n, m;
int sum[N];
int q[N], hh, tt = -1;
int ans = -0x3f3f3f3f;
int main() {
scanf("%d%d", &n, &m);
int x;
for(int i = 1; i <= n; i++) {
scanf("%d", &x);
sum[i] = sum[i - 1] + x;
}
for(int i = 1; i <= n; i++) {
if(hh <= tt && i - m - 1 >= q[hh]) hh++;
while(hh <= tt && sum[i - 1] <= sum[q[tt]]) tt--;
q[++tt] = i - 1;
ans = max(ans, sum[i] - sum[q[hh]]);
}
printf("%d\n", ans);
return 0;
}
总结:
在状态转移方程中当前状态的所有值可以从上一个状态的某个连续的段的值得到,要对这个连续的段进行 RMQ 操作,相邻状态的段的左右区间满足非降的关系时,就可以使用单调队列优化 dp。