动态规划入门——多重背包与单调优化

本文深入探讨动态规划中的多重背包问题,尤其是如何利用单调优化提高算法效率。文章介绍了单调队列的概念,解释了如何通过单调队列在不遍历所有状态的情况下找到最优解,从而降低复杂度。内容包括基础分析、单调队列的原理及其在动态规划算法中的应用,以及算法流程的详细解析。文章适合对动态规划感兴趣的读者,帮助理解动态规划的复杂度分析和优化技巧。

本文始发于个人公众号:TechFlow,原创不易,求个关注


今天是算法与数据结构的第14篇文章,也是动态规划专题的第三篇。

在之前的文章当中,我们介绍了多重背包的二进制拆分的解法。在大多数情况下,这种解法已经足够了,但是如果碰到极端的出题人可能还是会被卡时间。这个时候只能用更加快速的方法,也就是今天我们要一起来看的单调优化。

单调优化是单调队列优化的简称,单调栈我们在之前的LeetCode专题已经介绍过了。它的本质只是一个简单的栈,通过在插入元素时候对栈顶的部分元素进行弹出,从而保证了栈内元素的有序。而单调队列也类似,只是插入元素的位置不同而已。栈是只能从栈顶插入,队列则是从队尾插入。

比如我们看下下图,下图就是一个典型的单调队列,队列中的元素是[10, 6, 3]。队首是10,队列当中的元素从队首开始往队尾递减。

如果这个时候我们从队尾插入9,由于9大于队尾的3,所以3出队列,我们继续判断,发现9依然大于6,所以6再次出队列。最后得到的结果是[10, 9]。准确的说,由于我们进出队列的操作可以同时在队首或者队尾进行,所以严格说起来这并不是普通的队列,而是一个双端队列

单调队列或者说单调栈的最大用处是由于容器内元素递增或者递减,所以栈顶或者是队首的元素就是最值。我们通过使用单调栈可以在常数时间内获得某一个区间内的若干个最值,在一些问题当中只获得一次最值还是不够的。因为种种条件的限制,所以可能使得最值不一定能够成立,这个时候需要求第二最值或者是第三最值,在这种问题下, 使用单调栈或者是单调队列就是非常有必要的了。

比如今天要讨论的多重背包问题,就是这样的情况。


基础分析

我们先把单调队列的事情先放在一边,先来仔细分析一下题目。

在之前的文章当中,我们曾经讨论过动态规划算法的复杂度问题。对于动态规划算法而言,我们要做的是遍历所有的决策,以及决策可以应用的状态,找到每个状态最佳的转移,记录这些最好的转移结果。在所有的结果当中的最值,就是我们要找的整个问题的答案。那么,我们可以很方便地推导出动态规划的复杂度,等于状态数乘上决策数。

我们记住这个简单的结论,它可以帮助我们很方便地分析动态规划算法的复杂度,尤其在一些通过传统方法不方便分析的时候。

我们把复杂度结论带入多重背包问题,对于多重背包问题来说,我们的决策是由两个条件决定的。其中一个是物品,另一个是这个物品的数量。所以决策的数量等于物品数乘上物品的个数,状态是背包的容量。我们假设物品的数量是N,物品的个数为M,背包的容量是V,那么它的复杂度就是O(NMV)O(NMV)O(NMV)。显然,在绝大多数情况下,这个复杂度是我们不能接受的,也是我们需要引入种种优化的原因。

在之前的文章当中,我们通过二进制表示法,将物品的数量拆分成了若干个2次幂的和。所以对于二进制表示法而言,它的复杂度是O(NlogMV)O(NlogMV)O(NlogMV)。我们通过二进制表示将M这一维降到了logMlogMlogM,那么有没有办法将它继续简化呢?

当然是有的,我们继续来分析。在N

<think> 首先,用户查询是关于多背包问题使用单调队列优化的信息。用户引用了几个来源,但指出这些引用仅供参考,我需要专注于真正的需求。用户上一次的问题是查找多背包问题的单调队列优化方法。 关键点是:多背包问题(Multiple Knapsack Problem)通常涉及多个物品,每个物品有重量、价值和数量限制(或多个实例)。单调队列优化是一种高效方法,用于处理多重背包问题(Multiple Knapsack Problem with Quantities),其中每个物品有多个副本。 从用户的引用中: - 引用[1]讨论了多重背包单调队列优化,并提供了C++代码。 - 引用[2]强调了队列内部元素的单调性维护。 - 引用[3]提供了另一个C++代码实现。 我需要综合这些信息,提供一个清晰、结构化的回答。回答必须: - 使用$...$格式行内数学表达式。 - 使用$$...$$格式独立公式单独成段。 - 使用中文。 - 生成相关问题。 - 末尾添加引用标识。 回答结构: 1. **解释问题**:简要描述多重背包问题。 2. **介绍优化方法**:解释单调队列优化的核心思想,为什么有效。 3. **算法步骤**:详细说明优化算法的逻辑。 4. **代码示例**:提供或描述代码实现。 5. **时间复杂度和优势**:讨论性能。 6. **生成相关问题**:基于内容提出相关问题。 使用引用标识:在回答中引用关键点时,末尾添加[^n]。 现在,构建内容: - **多重背包问题定义**:有一个背包容量$V$,$N$种物品,每种物品有体积$v_i$、价值$w_i$和可用数量$s_i$。目标是选择物品放入背包,使得总价值最大,且总容量不超过$V$。 - **标准解法的问题**:朴素多重背包解法(如化为0-1背包)时间复杂度为$O(N \times V \times S)$,当$S$大时效率低。 - **单调队列优化思想**:优化动态规划的状态移。对于每种物品,移方程是: $$ dp[j] = \max_{k=0}^{s_i} \{ dp[j - k \cdot v_i] + k \cdot w_i \} \quad \text{for } j = 0 \text{ to } V $$ 这可以改写为: $$ dp[j] = \max_{k=0}^{s_i} \{ dp[j - k \cdot v_i] + k \cdot w_i \} $$ 单调队列用于高效计算这个最大值。 - **为什么用单调队列**:队列维护一个滑动窗口的最大值,确保在$O(1)$均摊时间获取最大值。 - **算法步骤**: - 对于每种物品$i$,复制当前dp数组到辅助数组$g$。 - 对于每个余数$j$ (mod $v_i$),维护一个单调递减队列。 - 遍历容量$k$从$j$到$V$,步长为$v_i$。 - 更新队列:移除过期元素(超出$s_i$限制),添加新元素,保持队列单调。 - 用队列头更新$dp[k]$。 - **时间复杂度**:优化后为$O(N \times V)$,显著优于朴素方法[^1]。 - **代码参考**:基于引用,提供简化代码描述或伪代码。 生成相关问题: 1. 多重背包问题单调队列优化的时间复杂度分析? 2. 如何实现单调队列优化中的队列操作? 3. 其他优化方法相比的优势? 确保语言中文。 </think> ### 多重背包问题的单调队列优化方法 多重背包问题是背包问题的一个变种:给定$N$种物品,每种物品有体积$v_i$、价值$w_i$和数量上限$s_i$,以及一个背包容量$V$。目标是在总容量不超过$V$的前提下,最大化所选物品的总价值。朴素解法(如化为0-1背包)的时间复杂度为$O(N \times V \times \max s_i)$,效率较低;而单调队列优化能将复杂度降至$O(N \times V)$,显著提升性能[^1][^2][^3]。 #### 优化核心思想 单调队列优化的核心在于高效处理动态规划的状态移。多重背包的状态移方程可表述为: $$ dp[j] = \max_{k=0}^{\min(s_i, \lfloor j / v_i \rfloor)} \{ g[j - k \cdot v_i] + k \cdot w_i \} \quad \text{对所有 } j = 0, 1, \dots, V $$ 其中: - $dp[j]$ 表示背包容量为$j$时的最大价值。 - $g$ 是辅助数组,存储放入当前物品前的状态(即上一种物品的dp值)。 - $k$ 表示当前物品的选取数量。 单调队列维护一个滑动窗口的最大值: 1. **窗口定义**:对于每个余数$r$($r = j \mod v_i$),遍历容量$j$(从$r$到$V$,步长为$v_i$)。 2. **单调性维护**:队列存储容量索引,保证队列头(head)对应的价值最大,且队列单调递减(即$g[q[\text{head}]] - \text{offset}$ 递减)。移除过期元素(超出物品数量$s_i$限制)和尾部较小值。 3. **高效更新**:在$O(1)$均摊时间内获取窗口最大值,用于更新$dp[j]$[^1][^2]。 #### 算法步骤 以下是基于引用代码的简化步骤(以C++为例): 1. **初始化**:读入$N$(物品种数)和$V$(背包容量),初始化dp数组为0。 2. **遍历每种物品**:对于每种物品$i$: - 复制当前dp数组到辅助数组$g$(保留放入$i$前的状态)。 - 遍历余数$r$($r = 0$ 到 $v_i - 1$),处理容量$j$:$r, r+v_i, r+2v_i, \dots, V$。 3. **维护单调队列**: - 队列$q$存储容量索引,$hh$(队头)和$tt$(队尾)初始化为0和-1。 - 对于每个$k$(从$r$到$V$,步长为$v_i$): - **移除过期元素**:如果队列头$q[hh]$超出$s_i$限制(即$k - q[hh] > s_i \cdot v_i$),则$hh++$。 - **更新dp[k]**:如果队列非空,$dp[k] = \max(dp[k], g[q[hh]] + (k - q[hh]) / v_i \cdot w_i)$。 - **入队新元素**:移除尾部小于当前值的元素,保持单调递减: ```c while (hh <= tt && g[q[tt]] - (q[tt] - r) / v_i * w_i <= g[k] - (k - r) / v_i * w_i) tt--; q[++tt] = k; ``` 4. **输出结果**:遍历所有物品后,$dp[V]$即为最大价值。 #### 代码示例(简化C++实现) 参考引用[1]和[3],以下是关键部分伪代码: ```cpp #include <iostream> #include <cstring> using namespace std; const int maxv = 20005; int dp[maxv], g[maxv], q[maxv]; int main() { int N, V; cin >> N >> V; memset(dp, 0, sizeof dp); // 初始化dp for (int i = 0; i < N; i++) { int v, w, s; cin >> v >> w >> s; // 物品体积、价值、数量 memcpy(g, dp, sizeof dp); // 保存当前状态 for (int r = 0; r < v; r++) { // 遍历余数r int hh = 0, tt = -1; // 队列初始化 for (int k = r; k <= V; k += v) { // 步长为v // 移除过期元素 (k - q[hh] > s * v) if (hh <= tt && (k - q[hh]) > s * v) hh++; // 更新dp[k]使用队列头最大值 if (hh <= tt) dp[k] = max(dp[k], g[q[hh]] + (k - q[hh]) / v * w); // 维护单调递减队列 while (hh <= tt && g[q[tt]] - (q[tt] - r) / v * w <= g[k] - (k - r) / v * w) tt--; q[++tt] = k; // 入队 } } } cout << dp[V] << endl; return 0; } ``` #### 时间复杂度优势 - **时间复杂度**:$O(N \times V)$,因为每个余数类独立处理,且每个容量$j$在队列操作中均摊$O(1)$时间[^1][^3]。 - **优势**:相比二进制优化($O(N \times V \times \log s_i)$),单调队列优化避免了额外拆分,适合大$s_i$场景(如AcWing等竞赛问题)[^1][^2]。 该优化利用了动态规划的状态移特性,通过单调队列高效管理滑动窗口,确保性能最优[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值