概述

单调队列优化 DP 适用于形如 的转移方程,其中决策变量 的取值范围是滑动窗口。核心思想是用单调队列维护候选决策集合,均摊 获取最优值。

基本原理

为前 个元素的最优值,转移需要枚举上一阶段的位置 。若 可分离为 ,则只需维护 的极值:

视为决策值存入单调队列,队首即为当前窗口最优。

多重背包优化

对于完全/多重背包,按模 的余数 分组,每组内用单调队列维护:

参考代码框架:

deque<int> q;
for (int y = 0; y < w[i]; ++y) {
    q.clear();
    for (int x = 0; x * w[i] + y <= W; ++x) {
        int cur = x * w[i] + y;
        while (!q.empty() && q.front() < x - k[i]) q.pop_front();
        if (!q.empty())
            f[cur] = max(f[cur], g[q.front() * w[i] + y] - q.front() * v[i] + x * v[i]);
        while (!q.empty() && g[cur] - x * v[i] >= g[q.back() * w[i] + y] - q.back() * v[i])
            q.pop_back();
        q.push_back(x);
    }
}

问题模板

题目题号说明
滑动窗口P1886纯滑动窗口最值,单调队列入门
多重背包P1776多重背包的单调队列优化
琪露诺P1725
Watching FireworksCF372C二维 DP + 滚动单调队列

问题归约

单调队列优化的核心模式可统一为:

其中 仅依赖于 ,且窗口左右边界 单调递增(滑动窗口性质)。此时用单调队列维护 ,即可 取出窗口最值。

常见可归约类型:

问题窗口
滑动窗口最值
多重背包 (模 组)
跳跃游戏 (P1725)
序列分段 部分 部分决策单调区间

完整例题:P1725 琪露诺

题意: 从 0 出发,每次跳跃 步,到达位置 获得 ,求到 的最大得分,可越过 结束。

状态定义: 表示到达 时的最大得分。

转移方程:

窗口长度 的取值范围随 右移,正好是滑动窗口最大值问题。

#include <bits/stdc++.h>
using namespace std;
const int N = 200010, INF = 0x80808080;
int n, L, R, a[N], f[N];
 
int main() {
    cin >> n >> L >> R;
    for (int i = 0; i <= n; ++i) cin >> a[i];
 
    memset(f, 0x80, sizeof f);  // 负无穷
    f[0] = a[0];
    deque<int> q;
    int ans = INF;
 
    for (int i = L; i <= n; ++i) {
        // 候选决策 j = i - L 进入窗口
        int j = i - L;
        while (!q.empty() && f[q.back()] <= f[j]) q.pop_back();
        q.push_back(j);
        // 弹出窗口左界之外的决策 (j < i - R)
        while (!q.empty() && q.front() < i - R) q.pop_front();
        // 队首为最优决策
        f[i] = f[q.front()] + a[i];
        // 越过 n 可结束,更新答案
        if (i + R > n) ans = max(ans, f[i]);
    }
    cout << ans << endl;
    return 0;
}

复杂度分析: 每个元素入队一次、出队至多一次,均摊 ,总复杂度 。朴素 DP 每次枚举 个决策,总 ,当 时退化为

通用代码模板

// 状态转移: f[i] = min/max_{j in [i-k, i-1]} (f[j] + cost(j, i))
// 前置条件: cost(j,i) = A[j] + B[i] 或可分离为类似形式
deque<int> q;
vector<int> f(n + 1);
 
for (int i = 1; i <= n; ++i) {
    // Step 1: 维护窗口左界 —— 弹出过期的决策
    while (!q.empty() && q.front() < max_left(i)) q.pop_front();
 
    // Step 2: 取队首最优决策进行转移
    if (!q.empty()) {
        int j = q.front();
        f[i] = B[i] + (f[j] + A[j]);  // 具体形式视问题而定
    }
 
    // Step 3: 维护队列单调性 —— 弹出尾部劣于当前决策的元素
    while (!q.empty() && better(i, q.back())) q.pop_back();
 
    // Step 4: 当前决策入队
    q.push_back(i);
}

四个步骤的口诀: 一弹过期,二取最优,三保单调,四插入队。

复杂度对比

维度朴素 DP单调队列优化
单个状态转移 (均摊)
总时间复杂度
空间复杂度 (DP 数组) + (队列)
适用限制 可分离,窗口单调

相关链接

多平台练习

| 洛谷 | 本题单 | 竞赛基础 |
| POJ (北大) | PKU JudgeOnline | 经典题目,适合巩固 |
| HDU (杭电) | HDU OJ | 暑期多校训练 |
| Codeforces | Codeforces | 国际竞赛,适合提升 |