Loading docs/dp/knapsack.md +149 −17 Original line number Diff line number Diff line 在学习本章前请确认你已经学习了[动态规划部分简介](/dp/) 在具体讲何为 “背包 dp” 前,先来看如下的例题 在具体讲何为 “背包 dp” 前,先来看如下的例题: ??? note " [「USACO07 DEC」Charm Bracelet](https://www.luogu.org/problemnew/show/P2871)" 题意概要:有 $n$ 个物品和一个容量为 $W$ 的背包,每个物品有重量 $w_{i}$ 和价值 $v_{i}$ 两种属性,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。 Loading @@ -21,7 +21,13 @@ $$ f_{i,j}=\max(f_{i-1,j},f_{i-1,j-w_{i}}+v_{i}) $$ 在程序实现的时候,由于对当前状态有影响的只有 $f_{i-1}$,故可以去掉第一维,直接用 $f_{j}$ 来表示处理到当前物品 $i$ 时背包容量为 $j$ 的最大价值 $f_{i,j}$。 在程序实现的时候,由于对当前状态有影响的只有 $f_{i-1}$,故可以去掉第一维,直接用 $f_{i}$ 来表示处理到当前物品时背包容量为 $i$ 的最大价值,得出以下方程: $$ f_i=\max \left(f_i,f_{i-w_i}+v_i\right) $$ **务必牢记并理解这个转移方程,因为大部分背包问题的转移方程都是在此基础上推导出来的。** 还有一点需要注意的是,很容易写出这样的错误核心代码: Loading @@ -34,7 +40,7 @@ for (int i = 1; i <= W; i++) 这段代码哪里错了呢?枚举顺序错了。 仔细观察代码可以发现:对于当前处理的物品 $i$ 和当前状态 $f_{i,j}$,在 $j\geqslant w_{i}$ 时,$f_{i,j}$ 是会被 $f_{i,j-w_{i}}$ 所影响的。这就相当于物品 $i$ 可以多次被放入背包,与题意不符。(事实上,这正是完全背包问题的解法) 仔细观察代码可以发现:对于当前处理的物品 $i$ 和当前状态 $f_{i,j}$,在 $j\geqslant w_{i}$ 时,$f_{i,j}$ 是会被 $f_{i,j-w_{i}}$ 所影响的。这就相当于物品 $i$ 可以多次被放入背包,与题意不符。(事实上,这正是完全背包问题的解法) 为了避免这种情况发生,我们可以改变枚举的顺序,从 $W$ 枚举到 $w_{i}$,这样就不会出现上述的错误,因为 $f_{i,j}$ 总是在 $f_{i,j-w_{i}}$ 前被更新。 Loading @@ -44,7 +50,6 @@ for (int i = 1; i <= W; i++) for (int i = 1; i <= W; i++) for (int l = W - i; l >= 0; l--) f[l + i] = max(f[l] + w[i], f[l + i]); // 由 f[i][l + w[i]] = max(max(f[i - 1][l + w[i]],f[i - 1][l] + w[i]),f[i][l + w[i]]); 简化而来 ``` ??? 例题代码 Loading Loading @@ -147,7 +152,7 @@ $$ ??? 二进制分组代码 ``` ```cpp index = 0; for(int i = 1; i <= m; i++) { int c = 1, p, h, k; Loading @@ -162,3 +167,130 @@ $$ list[index].v = h * k; } ``` ## 混合背包 混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取 $k$ 次。 这种题目看起来很吓人,可是只要领悟了前面几种背包的中心思想,并将其合并在一起就可以了。下面给出伪代码: ```cpp for (循环物品种类) { if (是 0-1 背包) 套用 0-1 背包代码; else if (是完全背包) 套用完全背包代码; else if (是多重背包) 套用多重背包代码; } ``` ## 二维费用背包 先来一道例题:[「Luogu P1855」榨取kkksc03](https://www.luogu.org/problemnew/show/P1855)。 这道题是很明显的 0-1 背包问题,可是不同的是选一个物品会消耗两种价值(经费、时间)。这种问题其实很简单:方程基本不用变,只需再开一维数组,同时转移两个价值就行了!(完全、多重背包同理) 这时候就要注意,再开一维存放物品编号就不合适了,因为容易 MLE。 例题核心代码: ```cpp for (int k=1;k<=n;k++) { for (int i=m;i>=mi;i--)//对经费进行一层枚举 for (int j=t;j>=ti;j--)//对时间进行一层枚举 dp[i][j]=max(dp[i][j],dp[i-mi][j-ti]+1); } ``` ## 分组背包 再看一道例题:[「Luogu P1757」通天之分组背包](https://www.luogu.org/problemnew/show/P1757)。 所谓分组背包,就是将物品分组,每组的物品相互冲突,最多只能选一个物品放进去。 这种题怎么想呢?其实是从“在所有物品中选择一件”变成了“从当前组中选择一件”,于是就对每一组进行一次 0-1 背包就可以了。 再说一说如何进行存储。我们可以将 $t_{k,i}$ 表示第 $k$ 组的第 $i$ 件物品的编号是多少,再用 $cnt_k$ 表示第 $k$ 组物品有多少个。 例题核心代码: ```cpp for (int k=1;k<=ts;k++)//循环每一组 for (int i=m;i>=0;i--)//循环背包容量 for (int j=1;j<=cnt[k];j++)//循环该组的每一个物品 if (i>=w[t[k][j]]) dp[i]=max(dp[i],dp[i-w[t[k][j]]]+c[t[k][j]]);//像0-1背包一样状态转移 ``` 这里要注意:**一定不能搞错循环顺序**,这样才能保证正确性。 ## 有依赖的背包 一道例题:[「Luogu P1064」金明的预算方案](https://www.luogu.org/problemnew/show/P1064)。 这种背包问题其实就是如果选第$i$件物品,就必须选第 $j$ 件物品,保证不会循环引用,一部分题目甚至会出现多叉树的引用形式。为了方便,就称不依赖于别的物品的物品称为“主件”,依赖于某主件的物品称为“附件”。 对于包含一个主件和若干个附件的集合有以下可能性:仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……需要将以上可能性的容量和价值转换成一件件物品。因为这几种可能性只能选一种,所以可以将这看成分组背包。 如果是多叉树的集合,则要先算子节点的集合,最后算父节点的集合。 ## 泛化物品的背包 这种背包,没有固定的费用和价值,它的价值是随着分配给它的费用而定。在背包容量为 $V$ 的背包问题中,当分配给它的费用为 $v_i$ 时,能得到的价值就是 $h\left(v_i\right)$。这时,将固定的价值换成函数的引用即可。 ## 杂项 ### 小优化 根据贪心原理,当费用相同时,只需保留价值最高的;当价值一定时,只需保留费用最低的;当有两件物品 $i,j$ 且 $i$ 的价值大于 $j$ 的价值并且 $i$ 的费用小于$j$的费用是,只需保留 $j$。 ### 背包问题变种 #### 输出方案 输出方案其实就是记录下来背包中的某一个状态是怎么推出来的。我们可以用 $g_{i,v}$ 表示第 $i$ 件物品占用空间为 $v$ 的时候是否选择了此物品。然后在转移时记录是选用了哪一种策略(选或不选)。输出时的伪代码: ```cpp int v=V;//记录当前的存储空间 for (从最后一件循环至第一件)//因为最后一件物品存储的是最终状态,所以从最后一件物品进行循环 { if (g[i][v]) { 选了第 i 项物品; v -= 第 i 项物品的价值; } else 未选第 i 项物品; } ``` #### 求方案数 对于给定的一个背包容量、物品费用、其他关系等的问题,求装到一定容量的方案总数。 这种问题就是把求最大值换成求和即可。 例如 0-1 背包问题的转移方程就变成了: $$ dp_i=\sum(dp_i,dp_{i-c_i}) $$ 初始条件:$dp_0=1$ 因为当容量为 $0$ 时也有一个方案:什么都不装! #### 求最优方案总数 #### 求第 k 优解 ### 参考资料 dd大牛(崔添翼)的背包九讲,GitHub仓库链接:[tianyicui/pack: 背包问题九讲](https://github.com/tianyicui/pack)。 Loading
docs/dp/knapsack.md +149 −17 Original line number Diff line number Diff line 在学习本章前请确认你已经学习了[动态规划部分简介](/dp/) 在具体讲何为 “背包 dp” 前,先来看如下的例题 在具体讲何为 “背包 dp” 前,先来看如下的例题: ??? note " [「USACO07 DEC」Charm Bracelet](https://www.luogu.org/problemnew/show/P2871)" 题意概要:有 $n$ 个物品和一个容量为 $W$ 的背包,每个物品有重量 $w_{i}$ 和价值 $v_{i}$ 两种属性,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。 Loading @@ -21,7 +21,13 @@ $$ f_{i,j}=\max(f_{i-1,j},f_{i-1,j-w_{i}}+v_{i}) $$ 在程序实现的时候,由于对当前状态有影响的只有 $f_{i-1}$,故可以去掉第一维,直接用 $f_{j}$ 来表示处理到当前物品 $i$ 时背包容量为 $j$ 的最大价值 $f_{i,j}$。 在程序实现的时候,由于对当前状态有影响的只有 $f_{i-1}$,故可以去掉第一维,直接用 $f_{i}$ 来表示处理到当前物品时背包容量为 $i$ 的最大价值,得出以下方程: $$ f_i=\max \left(f_i,f_{i-w_i}+v_i\right) $$ **务必牢记并理解这个转移方程,因为大部分背包问题的转移方程都是在此基础上推导出来的。** 还有一点需要注意的是,很容易写出这样的错误核心代码: Loading @@ -34,7 +40,7 @@ for (int i = 1; i <= W; i++) 这段代码哪里错了呢?枚举顺序错了。 仔细观察代码可以发现:对于当前处理的物品 $i$ 和当前状态 $f_{i,j}$,在 $j\geqslant w_{i}$ 时,$f_{i,j}$ 是会被 $f_{i,j-w_{i}}$ 所影响的。这就相当于物品 $i$ 可以多次被放入背包,与题意不符。(事实上,这正是完全背包问题的解法) 仔细观察代码可以发现:对于当前处理的物品 $i$ 和当前状态 $f_{i,j}$,在 $j\geqslant w_{i}$ 时,$f_{i,j}$ 是会被 $f_{i,j-w_{i}}$ 所影响的。这就相当于物品 $i$ 可以多次被放入背包,与题意不符。(事实上,这正是完全背包问题的解法) 为了避免这种情况发生,我们可以改变枚举的顺序,从 $W$ 枚举到 $w_{i}$,这样就不会出现上述的错误,因为 $f_{i,j}$ 总是在 $f_{i,j-w_{i}}$ 前被更新。 Loading @@ -44,7 +50,6 @@ for (int i = 1; i <= W; i++) for (int i = 1; i <= W; i++) for (int l = W - i; l >= 0; l--) f[l + i] = max(f[l] + w[i], f[l + i]); // 由 f[i][l + w[i]] = max(max(f[i - 1][l + w[i]],f[i - 1][l] + w[i]),f[i][l + w[i]]); 简化而来 ``` ??? 例题代码 Loading Loading @@ -147,7 +152,7 @@ $$ ??? 二进制分组代码 ``` ```cpp index = 0; for(int i = 1; i <= m; i++) { int c = 1, p, h, k; Loading @@ -162,3 +167,130 @@ $$ list[index].v = h * k; } ``` ## 混合背包 混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取 $k$ 次。 这种题目看起来很吓人,可是只要领悟了前面几种背包的中心思想,并将其合并在一起就可以了。下面给出伪代码: ```cpp for (循环物品种类) { if (是 0-1 背包) 套用 0-1 背包代码; else if (是完全背包) 套用完全背包代码; else if (是多重背包) 套用多重背包代码; } ``` ## 二维费用背包 先来一道例题:[「Luogu P1855」榨取kkksc03](https://www.luogu.org/problemnew/show/P1855)。 这道题是很明显的 0-1 背包问题,可是不同的是选一个物品会消耗两种价值(经费、时间)。这种问题其实很简单:方程基本不用变,只需再开一维数组,同时转移两个价值就行了!(完全、多重背包同理) 这时候就要注意,再开一维存放物品编号就不合适了,因为容易 MLE。 例题核心代码: ```cpp for (int k=1;k<=n;k++) { for (int i=m;i>=mi;i--)//对经费进行一层枚举 for (int j=t;j>=ti;j--)//对时间进行一层枚举 dp[i][j]=max(dp[i][j],dp[i-mi][j-ti]+1); } ``` ## 分组背包 再看一道例题:[「Luogu P1757」通天之分组背包](https://www.luogu.org/problemnew/show/P1757)。 所谓分组背包,就是将物品分组,每组的物品相互冲突,最多只能选一个物品放进去。 这种题怎么想呢?其实是从“在所有物品中选择一件”变成了“从当前组中选择一件”,于是就对每一组进行一次 0-1 背包就可以了。 再说一说如何进行存储。我们可以将 $t_{k,i}$ 表示第 $k$ 组的第 $i$ 件物品的编号是多少,再用 $cnt_k$ 表示第 $k$ 组物品有多少个。 例题核心代码: ```cpp for (int k=1;k<=ts;k++)//循环每一组 for (int i=m;i>=0;i--)//循环背包容量 for (int j=1;j<=cnt[k];j++)//循环该组的每一个物品 if (i>=w[t[k][j]]) dp[i]=max(dp[i],dp[i-w[t[k][j]]]+c[t[k][j]]);//像0-1背包一样状态转移 ``` 这里要注意:**一定不能搞错循环顺序**,这样才能保证正确性。 ## 有依赖的背包 一道例题:[「Luogu P1064」金明的预算方案](https://www.luogu.org/problemnew/show/P1064)。 这种背包问题其实就是如果选第$i$件物品,就必须选第 $j$ 件物品,保证不会循环引用,一部分题目甚至会出现多叉树的引用形式。为了方便,就称不依赖于别的物品的物品称为“主件”,依赖于某主件的物品称为“附件”。 对于包含一个主件和若干个附件的集合有以下可能性:仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……需要将以上可能性的容量和价值转换成一件件物品。因为这几种可能性只能选一种,所以可以将这看成分组背包。 如果是多叉树的集合,则要先算子节点的集合,最后算父节点的集合。 ## 泛化物品的背包 这种背包,没有固定的费用和价值,它的价值是随着分配给它的费用而定。在背包容量为 $V$ 的背包问题中,当分配给它的费用为 $v_i$ 时,能得到的价值就是 $h\left(v_i\right)$。这时,将固定的价值换成函数的引用即可。 ## 杂项 ### 小优化 根据贪心原理,当费用相同时,只需保留价值最高的;当价值一定时,只需保留费用最低的;当有两件物品 $i,j$ 且 $i$ 的价值大于 $j$ 的价值并且 $i$ 的费用小于$j$的费用是,只需保留 $j$。 ### 背包问题变种 #### 输出方案 输出方案其实就是记录下来背包中的某一个状态是怎么推出来的。我们可以用 $g_{i,v}$ 表示第 $i$ 件物品占用空间为 $v$ 的时候是否选择了此物品。然后在转移时记录是选用了哪一种策略(选或不选)。输出时的伪代码: ```cpp int v=V;//记录当前的存储空间 for (从最后一件循环至第一件)//因为最后一件物品存储的是最终状态,所以从最后一件物品进行循环 { if (g[i][v]) { 选了第 i 项物品; v -= 第 i 项物品的价值; } else 未选第 i 项物品; } ``` #### 求方案数 对于给定的一个背包容量、物品费用、其他关系等的问题,求装到一定容量的方案总数。 这种问题就是把求最大值换成求和即可。 例如 0-1 背包问题的转移方程就变成了: $$ dp_i=\sum(dp_i,dp_{i-c_i}) $$ 初始条件:$dp_0=1$ 因为当容量为 $0$ 时也有一个方案:什么都不装! #### 求最优方案总数 #### 求第 k 优解 ### 参考资料 dd大牛(崔添翼)的背包九讲,GitHub仓库链接:[tianyicui/pack: 背包问题九讲](https://github.com/tianyicui/pack)。