From 832086d29d610d8ea8cdac8405daff3361c436a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B5=B7?= <10402852@qq.com> Date: Tue, 12 Mar 2024 13:48:35 +0800 Subject: [PATCH] 'commit' --- TangDou/AcWing/BeiBao/6.md | 91 ++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/TangDou/AcWing/BeiBao/6.md b/TangDou/AcWing/BeiBao/6.md index 68f3a0c..d1b08ea 100644 --- a/TangDou/AcWing/BeiBao/6.md +++ b/TangDou/AcWing/BeiBao/6.md @@ -40,7 +40,7 @@ $0 @@ -60,8 +60,7 @@ $0 - - +> **温馨提示**:~~其实啊,三种都需要熟练背下来,谁知道考试时出题人会从哪个版本出发搞你~~ ### 三、单调队列优化 @@ -99,7 +98,25 @@ int main() { ```
+#### 原理解析 + +多重背包:物品个数是复数个,但又不是无限个。 +- 当作$01$背包来理解,$s$个物品当成$s$次$01$背包操作。然后优化的话通过 **二进制** 来优化。 +- 当作完全背包来理解,就是有数量限制的完全背包,而这个数量限制就可以理解成 **滑动窗口的长度**,然后优化通过 **单调队列** 来优化。 +队列的单调性就是基于 +$$f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w,.....,f[i - 1][j - k * v] + k * w$$ + +要将前$i - 1$个物品的方案基础上不停尝试放入第$i$个物品,遍历取最大值。 +$f[i - 1][j - k*v] + k *w$表示,总空间是$j$,有且仅有$k$个物品$i$,其余空间通过前$i - 1$个物品填充的最大价值。 + +多重背包因为有数量限制,向前遍历的个数$k$是受到数量$s$限制的。 + +所以要将$max$中的每个元素$f[i - 1][j], f[i - 1][j - v] + w,.....,f[i - 1][j - k * v] + k * w$,通过维护单调队列,来获得当前窗口宽度$s$范围内的最大值。 + +并且在$j = j + v$后,队列中所有元素对应状态与当前背包空间差增加了$v$,可以多放一个物品$i$,每个元素对应的价值增加$w$,全部都加一个$w$,所以单调性不发生任何变化。 + +两种优化可以理解成两种思路的进化路线。 #### 二维版本 ```cpp {.line-numbers} @@ -112,34 +129,37 @@ const int M = 20010; // 背包容量上限 int n, m; int f[N][M]; // 前i个物品,在容量为j的限定下,最大的价值总和 -int q[M]; // 单调优化的队列 +int q[M]; // 单调优化的队列,M是背包容量上限,说明q[]里面保存的是体积 -// 二维朴素版+队列[k-s*v,k],队列长s+1 +// 二维+队列[k-s*v,k],队列长s+1 int main() { cin >> n >> m; - for (int i = 1; i <= n; i++) { // 枚举每个物品 + for (int i = 1; i <= n; i++) { // 考虑前i种物品 int v, w, s; // 体积、价值、个数 cin >> v >> w >> s; - for (int j = 0; j < v; j++) { // 按余数分组,组内向前依赖 - // 查找指定范围内的最大值,标准的单调队列 - int hh = 0, tt = -1; - for (int k = j; k <= m; k += v) { // 分组内枚举每个可能的体积 - // 1、超出窗口范围的队头出队列,左侧只保留到k-s*v + // 下面的j,k是一起用来描述剩余体积的,之所以划分成两层循环,是因为依赖的前序是按v为间隔的依赖,并且,是有个数限制的依赖 + // j:按对体积取模分组:0表示剩余空间除以当前物品的体积余数是0 + // k:分组内的每一个体积,注意:这里的体积不一定都是合法的,因为数量是有限制的 + // 单调队列的意义:查找前面k-s*v范围内的价值的最大值,是一个单调递减的队列,队头保存的是获取到最大值的最近体积 + for (int j = 0; j < v; j++) { // 按余数分组讨论 + int hh = 0, tt = -1; // 全新的单调下降队列 + for (int k = j; k <= m; k += v) { // 与j一起构成了有效体积 + // 1、讨论到第i个物品时,由于它最多只有s个,所以有效的转移体积最小是k-s*v,更小的体积将被去除 if (hh <= tt && q[hh] < k - s * v) hh++; // 2、处理队尾,下一个需要进入队列的是f[i-1][k],它是后来的,生命周期长,可以干死前面能力不如它的所有老头子,以保证一个单调递减的队列 while (hh <= tt && f[i - 1][q[tt]] + (k - q[tt]) / v * w <= f[i - 1][k]) tt--; // 3、k入队列 q[++tt] = k; - // 4、上面操作完,f[i-1][k]已经进入队列,f[i][k]需要的所有人员到齐,可以直接从队头取出区间最大值更新自己了 + // 4、队列维护完毕,f[i-1][k]已经进入队列,f[i][k]可以直接从队头取出区间最大值更新自己 f[i][k] = f[i - 1][q[hh]] + (k - q[hh]) / v * w; } } } - printf("%d\n", f[n][m]); return 0; } + ``` @@ -177,28 +197,9 @@ int main() { } ``` -### 七、疑问解答  - -#### $Q_1$:原理解析 -多重背包:物品个数是复数个,但又不是无限个。 -- 当作$01$背包来理解,$s$个物品当成$s$次$01$背包操作。然后优化的话通过 **二进制** 来优化。 -- 当作完全背包来理解,就是有数量限制的完全背包,而这个数量限制就可以理解成 **滑动窗口的长度**,然后优化通过 **单调队列** 来优化。 - -队列的单调性就是基于 -$$f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w,.....,f[i - 1][j - k * v] + k * w$$ - -要将前$i - 1$个物品的方案基础上不停尝试放入第$i$个物品,遍历取最大值。 -$f[i - 1][j - k*v] + k *w$表示,总空间是$j$,有且仅有$k$个物品$i$,其余空间通过前$i - 1$个物品填充的最大价值。 - -多重背包因为有数量限制,向前遍历的个数$k$是受到数量$s$限制的。 - -所以要将$max$中的每个元素$f[i - 1][j], f[i - 1][j - v] + w,.....,f[i - 1][j - k * v] + k * w$,通过维护单调队列,来获得当前窗口宽度$s$范围内的最大值。 - -并且在$j = j + v$后,队列中所有元素对应状态与当前背包空间差增加了$v$,可以多放一个物品$i$,每个元素对应的价值增加$w$,全部都加一个$w$,所以单调性不发生任何变化。 +### 七、疑问与解答  -两种优化可以理解成两种思路的进化路线。 - -#### $Q_2$:单调队列中装的是什么? +#### $Q_1$:单调队列中装的是什么? $A$:是体积,是$f[i][j]$可以从哪些 **体积** 转移而来。比如当前$i$物品的体积是$v_i=2$,个数是$3$,那么$f[i][j]$可以从 $$ \large \left\{\begin{array}{l} @@ -208,19 +209,21 @@ $$ f[i-1][j-v_i*3]+3*w_i& 选择3个 \end{array}\right. $$ -转移而来,当然,还需要判断一下是不是你的背包能装下那么多,一旦装不下了就别硬装了。 +转移而来,也就是$(j-v_i*0,j-v_i*1,j-v_i*2,j-v_i*3)$这四个中的一个。 + +当然,还需要判断一下是不是你的背包能装下那么多,一旦装不下了就别硬装了。 -#### $Q_3$:只记录体积怎么计算最大价值? -$A$:只记录了所关联的体积,最大价值是现用现算的,办法是 +#### $Q_2$:只记录体积怎么计算最大价值? +$A$:只记录了所关联的体积,最大价值是现算: $$\large f[i][k] = f[i - 1][q[hh]] + (k - q[hh]) / v * w$$ -即,自己的最优解,可以通过前序当中**最大值**所在的体积`q[hh]`转移而来,产生的增量价值就是 $\large \displaystyle (k - q[hh]) / v * w$ +即,自己的最优解,可以通过前序当中 **最大值** 所在的体积`q[hh]`转移而来,增量价值是 $\large \displaystyle (k - q[hh]) / v * w$ -#### $Q4$:单调队列的使用场景在哪里? -$A$:使用单调队列的唯一场景就是 **离我在$X$的范围内,最大或最小值是多少**? +#### $Q_3$:单调队列的使用场景在哪里? +$A$:使用单调队列的 **唯一场景** 就是 **离我在$S$的范围内,最大或最小值是多少**? 它的任务是做到$O(1)$的时间复杂度进行快速查询结果,所以,只能是放在队首,不能再进行遍历或者二分,那样就不是$O(1)$了。 -#### $Q5$:单调队列是怎么样做到将最优解放到队首的呢? +#### $Q_4$:单调队列是怎么样做到将最优解放到队首的呢? $A:$单调队列优化有三步曲,按套路做就可以完成这样的任务: * 将已经超过 **窗口范围** 的旧数据从单调队列中去掉,保证窗口中只有最近的、最多$s$个(或$s+1$,这和具体的题意有关,后续会继续说明~)有效数据。 @@ -229,13 +232,13 @@ $A:$单调队列优化有三步曲,按套路做就可以完成这样的任务 * 滑动窗口是建立在前序数组$f[i-1]$上的,范围只能是前面一行$f[i-1][j],f[i-1][j-v],f[i-1][j-2v],...,f[i-1][j-kv]$ -#### $Q6:$此处的单调队列,是递增还是递减的? +#### $Q_5:$此处的单调队列,是递增还是递减的? $A:$是一个单调递减的队列,队列头存储的是窗口中的最大值所对应的体积。 -#### $Q7$:为什么要先进队列,再更新答案呢?我看有些同学是先更新答案,再进队列啊? +#### $Q_6$:为什么要先进队列,再更新答案呢?我看有些同学是先更新答案,再进队列啊? $A$:这个主要看$f[i-1][k]$是不是可以成为答案的备选项,如果是,那么就先进队列,再更新;如果不是,则先更新再进队列。以本题为例,$f[i][k]$可不可以从$f[i-1][k]$迁移而来呢?从实际含义出发,是可以的,这表示:第$i$个物品一个也不要,在空间最大是$k$的情况下,最大值如何表示?此时,当然最大值表示为$f[i-1][k]$了,即可以成为答案的备选项,需要先进队列再更新答案。 -#### $Q8$:`if (hh <= tt && q[hh] < k - s * v) hh++;` +#### $Q_7$:`if (hh <= tt && q[hh] < k - s * v) hh++;` 不是应该是$0$~$s$个物品$i$吗,不应该是$(k-q[hh])/v>s+1$个项吗? **答**:好问题!确实是$0$~$s$共$s+1$个,按理说单调队列长度最长应该是$s+1$,这里为什么只有$s$个长度呢?