单调队列优化 DPCSP-S

目录

💡 核心思想

单调队列优化 DP 用于解决"滑动窗口最值型"DP 问题。典型场景:dp[i] = max{ dp[j] + val(i,j) },其中 j 的范围是一个滑动窗口 [i-k, i-1]。直接用单调队列维护窗口内的 dp 值,将 O(n·k) 优化到 O(n)。经典应用:多重背包优化、单调子序列、任务调度。

单调队列优化 DP 是 CSP-S 的高级技巧。很多同学看到多重背包就写二进制优化,其实单调队列优化更通用。核心思路:把 DP 转移方程按模数分组,每组独立用单调队列维护。记住公式:dp[i] = max{ dp[j] + w(i,j) },其中 j ∈ [i-k, i-1],用单调队列维护窗口内的最值。

🎯 直觉理解

想象你在排队买票,窗口只能看到前面 k 个人。你想找前面 k 个人里谁最划算(dp 值最大)。单调队列就像一个"智能排队机":新人来了,如果比前面所有人都强,前面的人永远没机会了,直接踢走;如果窗口滑走了,就把队头的人请出去。

📝 算法流程

  1. 写出原始 DP 方程,确定 j 的取值范围
  2. 将 j 按某个模数分组(通常是物品重量)
  3. 每组内用单调队列维护 dp 值
  4. 队列中存下标,保持 dp 值单调递减(求最大)或递增(求最小)
  5. 队头超出窗口范围时出队
  6. 当前 dp 值入队前,从队尾弹出所有更差(更不可能成为最优)的值

$$dp[i] = \max_{j \in [i-k, i-1]} \{ dp[j] + w(i, j) \}$$

📊 复杂度分析

指标复杂度
时间$O(n)$(每个状态最多入队出队一次)
空间$O(n)$

💻 参考实现(C++)

C++ (C++17)
#include 
using namespace std;

// 单调队列优化多重背包模板
// dp[i] = max(dp[i], dp[i - j*w] + j*v) for j in [0, cnt]
const int N = 200005;
int dp[N];

void monoQueueKnapsack(int W, int w, int v, int cnt) {
    // 按模 w 分组
    for (int r = 0; r < w; r++) {
        deque> dq; // (下标, dp值)
        for (int k = 0; r + k * w <= W; k++) {
            int i = r + k * w;
            int val = dp[i] - k * v;
            
            // 队头超出窗口
            while (!dq.empty() && dq.front().first < k - cnt) dq.pop_front();
            
            // 队尾维护单调性(求最大)
            while (!dq.empty() && dq.back().second <= val) dq.pop_back();
            
            dq.push_back({k, val});
            dp[i] = dq.front().second + k * v;
        }
    }
}

int main() {
    int n = 3, W = 10;
    // 物品:(w, v, cnt)
    memset(dp, -0x3f, sizeof(dp));
    dp[0] = 0;
    monoQueueKnapsack(W, 3, 4, 2); // 重量3,价值4,最多2个
    cout << dp[W] << endl;
    return 0;
}

⚠️ 常见坑点

模数分组写错——通常是按重量 w 分组

窗口大小计算错误——要考虑 cnt 的限制

队头出队条件——是 < 还是 <=

初始化——dp 数组要根据题意设置初始值

📚 相关题目

题目来源难度备注
P1776 宝物筛选洛谷CSP-S单调队列优化多重背包
P2627 修剪草坪洛谷CSP-S单调队列优化 DP
P3957 跳房子洛谷CSP-S单调队列 + 二分答案