diff --git a/Assets/Origins/Categories-List.md b/Assets/Origins/Categories-List.md index 9d0d7f69..c48edb07 100644 --- a/Assets/Origins/Categories-List.md +++ b/Assets/Origins/Categories-List.md @@ -285,7 +285,7 @@ ###### 1137. 第 N 个泰波那契数、0650. 只有两个键的键盘、0264. 丑数 II、0279. 完全平方数、0343. 整数拆分 -### [背包问题题目](../../Contents/10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-List.md) +### [背包问题题目](../../Contents/10.Dynamic-Programming/04.Knapsack-Problem/06.Knapsack-Problem-List.md) #### 0-1 背包问题 diff --git a/Assets/Origins/README-Catalogue-List.md b/Assets/Origins/README-Catalogue-List.md index 857a8cbc..fa5fa5fd 100644 --- a/Assets/Origins/README-Catalogue-List.md +++ b/Assets/Origins/README-Catalogue-List.md @@ -191,7 +191,8 @@ - [背包问题知识(二)](https://github.com/itcharge/LeetCode-Py/blob/main/Contents/10.Dynamic-Programming/04.Knapsack-Problem/02.Knapsack-Problem-02.md) - [背包问题知识(三)](https://github.com/itcharge/LeetCode-Py/blob/main/Contents/10.Dynamic-Programming/04.Knapsack-Problem/03.Knapsack-Problem-03.md) - [背包问题知识(四)](https://github.com/itcharge/LeetCode-Py/blob/main/Contents/10.Dynamic-Programming/04.Knapsack-Problem/04.Knapsack-Problem-04.md) - - [背包问题题目](https://github.com/itcharge/LeetCode-Py/blob/main/Contents/10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-List.md) + - [背包问题知识(五)](https://github.com/itcharge/LeetCode-Py/blob/main/Contents/10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-05.md) + - [背包问题题目](https://github.com/itcharge/LeetCode-Py/blob/main/Contents/10.Dynamic-Programming/04.Knapsack-Problem/06.Knapsack-Problem-List.md) - 区间 DP - [区间 DP 知识](https://github.com/itcharge/LeetCode-Py/blob/main/Contents/10.Dynamic-Programming/05.Interval-DP/01.Interval-DP.md) - [区间 DP 题目](https://github.com/itcharge/LeetCode-Py/blob/main/Contents/10.Dynamic-Programming/05.Interval-DP/02.Interval-DP-List.md) diff --git a/Contents/10.Dynamic-Programming/04.Knapsack-Problem/01.Knapsack-Problem-01.md b/Contents/10.Dynamic-Programming/04.Knapsack-Problem/01.Knapsack-Problem-01.md index d539bfe3..dfca3ed6 100644 --- a/Contents/10.Dynamic-Programming/04.Knapsack-Problem/01.Knapsack-Problem-01.md +++ b/Contents/10.Dynamic-Programming/04.Knapsack-Problem/01.Knapsack-Problem-01.md @@ -56,8 +56,8 @@ $dp[i][w] = \begin{cases} dp[i - 1][w] & w < weight[i - 1] \cr max \lbrace dp[i ###### 4. 初始条件 -- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[i][0] = 0$。 -- 前 0ドル$ 件物品所能获得的最大价值一定为 0ドル,ドル即 $dp[0][w] = 0$。 +- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[i][0] = 0,0 \le i \le size$。 +- 无论背包载重上限是多少,前 0ドル$ 件物品所能获得的最大价值一定为 0ドル,ドル即 $dp[0][w] = 0,0 \le w \le W$。 ###### 5. 最终结果 @@ -128,7 +128,7 @@ $dp[w] = \begin{cases} dp[w] & w < weight[i - 1] \cr max \lbrace dp[w], dp[w - w ###### 4. 初始条件 -- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[0] = 0$。 +- 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[w] = 0,0 \le w \le W$。 ###### 5. 最终结果 @@ -222,7 +222,7 @@ $dp[w] = \begin{cases} dp[w] & w < nums[i - 1] \cr max \lbrace dp[w], \quad dp[w ###### 4. 初始条件 -- 如果背包载重上限为 0ドル,ドル则无论选取什么元素,可以获得的元素和一定是 0ドル,ドル即 $dp[0] = 0$。 +- 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[w] = 0,0 \le w \le W$。 ###### 5. 最终结果 @@ -266,3 +266,5 @@ class Solution: - 【资料】[背包九讲 - 崔添翼](https://github.com/tianyicui/pack) - 【文章】[背包 DP - OI Wiki](https://oi-wiki.org/dp/knapsack/) +- 【文章】[背包问题 第四讲 - 宫水三叶的刷题日记](https://juejin.cn/post/7003243733604892685) +- 【文章】[Massive Algorithms: 讲透完全背包算法](https://massivealgorithms.blogspot.com/2015/06/unbounded-knapsack-problem.html) diff --git a/Contents/10.Dynamic-Programming/04.Knapsack-Problem/02.Knapsack-Problem-02.md b/Contents/10.Dynamic-Programming/04.Knapsack-Problem/02.Knapsack-Problem-02.md index 2ef7120c..879a96b6 100644 --- a/Contents/10.Dynamic-Programming/04.Knapsack-Problem/02.Knapsack-Problem-02.md +++ b/Contents/10.Dynamic-Programming/04.Knapsack-Problem/02.Knapsack-Problem-02.md @@ -40,8 +40,8 @@ $dp[i][w] = max \lbrace dp[i - 1][w - k \times weight[i - 1]] + k \times value[i ###### 4. 初始条件 -- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[i][0] = 0$。 -- 前 0ドル$ 种物品所能获得的最大价值一定为 0ドル,ドル即 $dp[0][w] = 0$。 +- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[i][0] = 0,0 \le i \le size$。 +- 无论背包载重上限是多少,前 0ドル$ 种物品所能获得的最大价值一定为 0ドル,ドル即 $dp[0][w] = 0,0 \le w \le W$。 ###### 5. 最终结果 @@ -133,8 +133,8 @@ $\quad dp[i][w] = \begin{cases} dp[i - 1][w] & w < weight[i - 1] \cr max \lbrac ###### 4. 初始条件 -- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[i][0] = 0$。 -- 前 0ドル$ 种物品所能获得的最大价值一定为 0ドル,ドル即 $dp[0][w] = 0$。 +- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[i][0] = 0,0 \le i \le size$。 +- 无论背包载重上限是多少,前 0ドル$ 种物品所能获得的最大价值一定为 0ドル,ドル即 $dp[0][w] = 0,0 \le w \le W$。 ###### 5. 最终结果 @@ -201,7 +201,7 @@ $dp[w] = \begin{cases} dp[w] & w < weight[i - 1] \cr max \lbrace dp[w], \quad d ###### 4. 初始条件 -- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[0] = 0$。 +- 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[w] = 0,0 \le w \le W$。 ###### 5. 最终结果 @@ -300,11 +300,15 @@ $dp[c] = \begin{cases} dp[c] & c < coins[i - 1] \cr min \lbrace dp[c], dp[c - co ###### 4. 初始条件 - 凑成总金额为 0ドル$ 的最少硬币数量为 0ドル,ドル即 $dp[0] = 0$。 +- 默认凑成总金额为 $w$ 的最少硬币数量为一个极大值(比如 $amount + 1$),表示无法凑成。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[c]$ 表示为:凑成总金额为 $c$ 的最少硬币数量。则最终结果为 $dp[amount]$。 +1. 如果 $dp[amount] \ne amount + 1,ドル则说明: $dp[amount]$ 为凑成金额 $amount$ 的最少硬币数量,则返回 $dp[amount]$。 +2. 如果 $dp[amount] = amount + 1,ドル则说明:无法凑成金额 $amount,ドル则返回 $-1$。 + ##### 思路 1:代码 ```Python diff --git a/Contents/10.Dynamic-Programming/04.Knapsack-Problem/03.Knapsack-Problem-03.md b/Contents/10.Dynamic-Programming/04.Knapsack-Problem/03.Knapsack-Problem-03.md index 8d235cfa..61263db6 100644 --- a/Contents/10.Dynamic-Programming/04.Knapsack-Problem/03.Knapsack-Problem-03.md +++ b/Contents/10.Dynamic-Programming/04.Knapsack-Problem/03.Knapsack-Problem-03.md @@ -26,8 +26,8 @@ $dp[i][w] = max \lbrace dp[i - 1][w - k \times weight[i - 1]] + k \times value[i ###### 4. 初始条件 -- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[i][0] = 0$。 -- 前 0ドル$ 种物品所能获得的最大价值一定为 0ドル,ドル即 $dp[0][w] = 0$。 +- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[i][0] = 0,0 \le i \le size$。 +- 无论背包载重上限是多少,前 0ドル$ 种物品所能获得的最大价值一定为 0ドル,ドル即 $dp[0][w] = 0,0 \le w \le W$。 ###### 5. 最终结果 @@ -83,7 +83,7 @@ $dp[w] = max \lbrace dp[w - k \times weight[i - 1]] + k \times value[i - 1] \rbr ###### 4. 初始条件 -- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[0] = 0$。 +- 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[w] = 0,0 \le w \le W$。 ###### 5. 最终结果 @@ -159,7 +159,7 @@ $dp[w] = max \lbrace dp[w - weight \underline{ } new[i - 1]] + value \underline{ ###### 4. 初始条件 -- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[0] = 0$。 +- 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[w] = 0,0 \le w \le W$。 ###### 5. 最终结果 diff --git a/Contents/10.Dynamic-Programming/04.Knapsack-Problem/04.Knapsack-Problem-04.md b/Contents/10.Dynamic-Programming/04.Knapsack-Problem/04.Knapsack-Problem-04.md index 59d8010c..d49531e8 100644 --- a/Contents/10.Dynamic-Programming/04.Knapsack-Problem/04.Knapsack-Problem-04.md +++ b/Contents/10.Dynamic-Programming/04.Knapsack-Problem/04.Knapsack-Problem-04.md @@ -1,10 +1,88 @@ -## 5. 分组背包问题 +## 5. 混合背包问题 + +> **混合背包问题**:有 $n$ 种物品和一个最多能装重量为 $W$ 的背包,第 $i$ 种物品的重量为 $weight[i],ドル价值为 $value[i],ドル件数为 $count[i]$。其中: +> +> 1. 当 $count[i] = -1$ 时,代表该物品只有 1ドル$ 件。 +> 2. 当 $count[i] = 0$ 时,代表该物品有无限件。 +> 3. 当 $count[i]> 0$ 时,代表该物品有 $count[i]$ 件。 +> +> 请问在总重量不超过背包载重上限的情况下,能装入背包的最大价值是多少? + +#### 思路 1:动态规划 + +混合背包问题其实就是将「0-1 背包问题」、「完全背包问题」和「多重背包问题」这 3ドル$ 种背包问题综合起来,有的是能取 1ドル$ 件,有的能取无数件,有的只能取 $count[i]$ 件。 + +其实只要理解了之前讲解的这 3ドル$ 种背包问题的核心思想,只要将其合并在一起就可以了。 + +并且在「多重背包问题」中,我们曾经使用「二进制优化」的方式,将「多重背包问题」转换为「0-1 背包问题」,那么在解决「混合背包问题」时,我们也可以先将「多重背包问题」转换为「0-1 背包问题」,然后直接再区分是「0-1 背包问题」还是「完全背包问题」就可以了。 + +#### 思路 1:代码 + +```Python +class Solution: + def mixedPackMethod1(self, weight: [int], value: [int], count: [int], W: int): + weight_new, value_new, count_new = [], [], [] + + # 二进制优化 + for i in range(len(weight)): + cnt = count[i] + # 多重背包问题,转为 0-1 背包问题 + if cnt> 0: + k = 1 + while k <= cnt: + cnt -= k + weight_new.append(weight[i] * k) + value_new.append(value[i] * k) + count_new.append(1) + k *= 2 + if cnt> 0: + weight_new.append(weight[i] * cnt) + value_new.append(value[i] * cnt) + count_new.append(1) + # 0-1 背包问题,直接添加 + elif cnt == -1: + weight_new.append(weight[i]) + value_new.append(value[i]) + count_new.append(1) + # 完全背包问题,标记并添加 + else: + weight_new.append(weight[i]) + value_new.append(value[i]) + count_new.append(0) + + dp = [0 for _ in range(W + 1)] + size = len(weight_new) + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 0-1 背包问题 + if count_new[i - 1] == 1: + # 逆序枚举背包装载重量(避免状态值错误) + for w in range(W, weight_new[i - 1] - 1, -1): + # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight_new[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 + dp[w] = max(dp[w], dp[w - weight_new[i - 1]] + value_new[i - 1]) + # 完全背包问题 + else: + # 正序枚举背包装载重量 + for w in range(weight_new[i - 1], W + 1): + # dp[w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」与「前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品所得的最大价值」两者中的最大值 + dp[w] = max(dp[w], dp[w - weight_new[i - 1]] + value_new[i - 1]) + + return dp[W] +``` + +#### 思路 1:复杂度分析 + +- **时间复杂度**:$O(W \times \sum \log_2{count[i]}),ドル其中 $W$ 为背包的载重上限,$count[i]$ 是第 $i$ 种物品的数量。 +- **空间复杂度**:$O(W)$。 + +## 6. 分组背包问题 > **分组背包问题**:有 $n$ 组物品和一个最多能装重量为 $W$ 的背包,第 $i$ 组物品的件数为 $group\underline{}count[i],ドル第 $i$ 组的第 $j$ 个物品重量为 $weight[i][j],ドル价值为 $value[i][j]$。每组物品中最多只能选择 1ドル$ 件物品装入背包。请问在总重量不超过背包载重上限的情况下,能装入背包的最大价值是多少? -### 5.1 分组背包问题基本思路 +### 6.1 分组背包问题基本思路 #### 思路 1:动态规划 + 二维基本思路 @@ -34,8 +112,8 @@ $dp[i][w] = max \lbrace dp[i - 1][w],dp[i - 1][w - weight[i - 1][k]] + value[i ###### 4. 初始条件 -- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[i][0] = 0$。 -- 前 0ドル$ 组物品所能获得的最大价值一定为 0ドル,ドル即 $dp[0][w] = 0$。 +- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[i][0] = 0,0 \le i \le size$。 +- 无论背包载重上限是多少,前 0ドル$ 组物品所能获得的最大价值一定为 0ドル,ドル即 $dp[0][w] = 0,0 \le w \le W$。 ###### 5. 最终结果 @@ -67,7 +145,7 @@ class Solution: - **时间复杂度**:$O(n \times W \times C),ドル其中 $n$ 为物品分组数量,$W$ 为背包的载重上限,$C$ 是每组物品的数量。因为 $n \times C = \sum group\underline{}count[i],ドル所以时间复杂度也可以写成 $O(W \times \sum group\underline{}count[i])$。 - **空间复杂度**:$O(n \times W)$。 -### 5.2 分组背包问题滚动数组优化 +### 6.2 分组背包问题滚动数组优化 #### 思路 2:动态规划 + 滚动数组优化 @@ -85,7 +163,7 @@ $dp[w] = max \lbrace dp[w], \quad dp[w - weight[i - 1][k]] + value[i - 1][k] \r ###### 4. 初始条件 -- 如果背包载重上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[0] = 0$。 +- 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[w] = 0,0 \le w \le W$。 ###### 5. 最终结果 @@ -118,11 +196,11 @@ class Solution: - **时间复杂度**:$O(n \times W \times C),ドル其中 $n$ 为物品分组数量,$W$ 为背包的载重上限,$C$ 是每组物品的数量。因为 $n \times C = \sum group\underline{}count[i],ドル所以时间复杂度也可以写成 $O(W \times \sum group\underline{}count[i])$。 - **空间复杂度**:$O(W)$。 -## 6. 二维费用背包问题 +## 7. 二维费用背包问题 > **二维费用背包问题**:有 $n$ 件物品和有一个最多能装重量为 $W$、容量为 $V$ 的背包。第 $i$ 件物品的重量为 $weight[i],ドル体积为 $volume[i],ドル价值为 $value[i],ドル每件物品有且只有 1ドル$ 件。请问在总重量不超过背包载重上限、容量上限的情况下,能装入背包的最大价值是多少? -### 6.1 二维费用背包问题基本思路 +### 7.1 二维费用背包问题基本思路 我们可以参考「0-1 背包问题」的状态定义和基本思路,在「0-1 背包问题」基本思路的基础上,增加一个维度用于表示物品的容量。 @@ -144,8 +222,13 @@ $dp[i][w][v] = max(dp[i - 1][w][v], dp[i - 1][w - weight[i - 1]][v - volume[i - ###### 4. 初始条件 -- 如果背包载重上限为 0ドル$ 或者容量上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[i][0][v] = 0,dp[i][w][0] = 0$。 -- 前 0ドル$ 组物品所能获得的最大价值一定为 0ドル,ドル即 $dp[0][w][v] = 0$。 +- 如果背包载重上限为 0ドル$ 或者容量上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即: + - $dp[i][w][0] = 0,0 \le i \le size,0 \le w \le W$ + - $dp[i][0][v] = 0,0 \le i \le size,0 \le v \le V$ + +- 无论背包载重上限是多少,前 0ドル$ 种物品所能获得的最大价值一定为 0ドル,ドル即: + - $dp[0][w][v] = 0,0 \le w \le W,0 \le v \le V$ + ###### 5. 最终结果 @@ -182,7 +265,7 @@ class Solution: - **时间复杂度**:$O(n \times W \times V),ドル其中 $n$ 为物品分组数量,$W$ 为背包的载重上限,$V$ 为背包的容量上限。 - **空间复杂度**:$O(n \times W \times V)$。 -### 6.2 二维费用背包问题滚动数组优化 +### 7.2 二维费用背包问题滚动数组优化 #### 思路 2:动态规划 + 滚动数组优化 @@ -200,7 +283,10 @@ $dp[w][v] = max \lbrace dp[w][v], \quad dp[w - weight[i - 1]][v - volume[i - 1]] ###### 4. 初始条件 -- 如果背包载重上限为 0ドル$ 或者容量上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即 $dp[w][0] = 0,dp[0][v] = 0$。 +- 如果背包载重上限为 0ドル$ 或者容量上限为 0ドル,ドル则无论选取什么物品,可以获得的最大价值一定是 0ドル,ドル即: + - $dp[w][0] = 0,0 \le w \le W$ + - $dp[0][v] = 0,0 \le v \le V$ + ###### 5. 最终结果 @@ -232,59 +318,9 @@ class Solution: - **时间复杂度**:$O(n \times W \times V),ドル其中 $n$ 为物品分组数量,$W$ 为背包的载重上限,$V$ 为背包的容量上限。 - **空间复杂度**:$O(W \times V)$。 -## 7. 背包问题变种 - -### 7.1 背包问题求方案总数 - -> **背包问题求方案数**:在给定背包重量 $W,ドル每件物品重量 $weight[i],ドル物品间相互关系(分组、依赖等)的背包问题中。求解出装满背包或将背包装至某一指定重量的方案总数。 - -这种问题就是将原有状态转移方程中的「求最大值」变为「求和」即可。我们以「完全背包问题」求解方案总数为例。 - -> **完全背包问题求解方案数**:有 $n$ 种物品和一个最多能装重量为 $W$ 的背包,第 $i$ 种物品的重量为 $weight[i],ドル价值为 $value[i],ドル每种物品数量没有限制。请问装满重量为 $W$ 的背包,一共有多少种方案。 - -如果使用二维状态定义,可定义状态 $dp[i][w]$ 为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,一共有多少种方案。 - -则状态转移方程为:$dp[i][w] = sum(dp[i - 1][w], \quad dp[i][w - weight[i - 1]])$。 - -如果进行滚动数组优化中,使用一位状态定义,可定义状态 $dp[w]$ 为:装满重量为 $w$ 的背包的方案总数。则状态转移方程为:$dp[w] = sum(dp[w], \quad dp[w - weight[i - 1]])$。 - -###### 1. 划分阶段 - -按照物品种类的序号、当前背包的载重上限进行阶段划分。 - -###### 2. 定义状态 - -定义状态 $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中的方案总数。 - -###### 3. 状态转移方程 - -$dp[w] = sum \lbrace dp[w], \quad dp[w - weight[i - 1]] \rbrace $ - -###### 4. 初始条件 - -- 如果背包载重上限为 0ドル,ドル则一共有 1ドル$ 种方案(什么也不装),即 $dp[0] = 1$。 - -###### 5. 最终结果 - -根据我们之前定义的状态, $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中的方案总数。则最终结果为 $dp[W],ドル其中 $W$ 为背包的载重上限。 - -#### 思路 1:代码 - -```Python - -``` - -#### 思路 1:复杂度分析 - -- **时间复杂度**:$O(n \times W),ドル其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 -- **空间复杂度**:$O(W)$。 - -### 7.2 背包问题求最优方案数 - -### 7.3 背包问题输出方案 - ## 参考资料 - 【资料】[背包九讲 - 崔添翼](https://github.com/tianyicui/pack) - 【文章】[背包 DP - OI Wiki](https://oi-wiki.org/dp/knapsack/) -- 【文章】[【动态规划/背包问题】分组背包问题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487504&idx=1&sn=9ac523ec0ac14c8634a229f8c3f919d7&chksm=fd9cbb0fcaeb32196b80a40e4408f6a7e2651167e0b9e31aa6d7c6109fbc2117340a59db12a1&scene=178&cur_album_id=1751702161341628417#rd) +- 【文章】[【动态规划/背包问题】分组背包问题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487504&idx=1&sn=9ac523ec0ac14c8634a229f8c3f919d7&chksm=fd9cbb0fcaeb32196b80a40e4408f6a7e2651167e0b9e31aa6d7c6109fbc2117340a59db12a1&token=1936267333&lang=zh_CN&scene=21#wechat_redirect) +- 【文章】[【动态规划/背包问题】背包问题第一阶段最终章:混合背包问题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487034&idx=1&sn=eaa05b76387d34aa77f7f14f35fa78a4&chksm=fd9ca525caeb2c33095d285222dcee0dd072465bf7288bda0aab39e90a04bb7b1af018b89fd4&token=1872331648&lang=zh_CN&scene=21#wechat_redirect) diff --git a/Contents/10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-05.md b/Contents/10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-05.md new file mode 100644 index 00000000..274e5e1b --- /dev/null +++ b/Contents/10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-05.md @@ -0,0 +1,327 @@ +## 8. 背包问题变种 + +### 8.1 求恰好装满背包的最大价值 + +> **背包问题求恰好装满背包的最大价值**:在给定背包重量 $W,ドル每件物品重量 $weight[i],ドル物品间相互关系(分组、依赖等)的背包问题中,请问在恰好装满背包的情况下,能装入背包的最大价值总和是多少? + +在背包问题中,有的题目不要求把背包装满,而有的题目要求恰好装满背包。 + +如果题目要求「恰好装满背包」,则我们可在原有状态定义、状态转移方程的基础上,在初始化时,令 $dp[0] = 0,ドル以及 $d[w] = -\infty,1 \le w \le W$。 这样就可以保证最终得到的 $dp[W]$ 为恰好装满背包的最大价值总和。 + +这是因为:初始化的 $dp$ 数组实际上就是在没有任何物品可以放入背包时的「合法状态」。 + +如果不要求恰好装满背包,那么: + +1. 任何载重上限下的背包,在不放入任何物品时,都有一个合法解,此时背包所含物品的最大价值为 0ドル,ドル即 $dp[w] = 0,0 \le w \le W$。 + +而如果要求恰好装满背包,那么: + +1. 只有载重上限为 0ドル$ 的背包,在不放入物品时,能够恰好装满背包(有合法解),此时背包所含物品的最大价值为 0ドル,ドル即 $dp[0] = 0$。 +2. 其他载重上限下的背包,在放入物品的时,都不能恰好装满背包(都没有合法解),此时背包所含物品的最大价值属于未定义状态,值应为 $-\infty,ドル即 $dp[w] = 0,0 \le w \le W$。 + +这样在进行状态转移时,我们可以通过判断 $dp[w]$ 与 $-\infty$ 的关系,来判断是否能恰好装满背包。 + +下面我们以「0-1 背包问题」求恰好装满背包的最大价值为例。 + +> **0-1 背包问题求恰好装满背包的最大价值**:有 $n$ 种物品和一个最多能装重量为 $W$ 的背包,第 $i$ 种物品的重量为 $weight[i],ドル价值为 $value[i],ドル每件物品有且只有 1ドル$ 件。请问在恰好装满背包的情况下,能装入背包的最大价值总和是多少? + +#### 思路 1:动态规划 + 一维状态 + +1. **划分阶段**:按照物品种类的序号、当前背包的载重上限进行阶段划分。 +2. **定义状态**:定义状态 $dp[w]$ 表示为:将物品装入一个最多能装重量为 $w$ 的背包中,恰好装满背包的情况下,能装入背包的最大价值总和。 +3. **状态转移方程**:$dp[w] = dp[w] + dp[w - weight[i - 1]]$ +4. **初始条件**: + 1. 只有载重上限为 0ドル$ 的背包,在不放入物品时,能够恰好装满背包(有合法解),此时背包所含物品的最大价值为 0ドル,ドル即 $dp[0] = 0$。 + 2. 其他载重上限下的背包,在放入物品的时,都不能恰好装满背包(都没有合法解),此时背包所含物品的最大价值属于未定义状态,值应为 $-\infty,ドル即 $dp[w] = 0,0 \le w \le W$。 +5. **最终结果**:根据我们之前定义的状态, $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中的方案总数。则最终结果为 $dp[W],ドル其中 $W$ 为背包的载重上限。 + +#### 思路 1:代码 + +```Python +class Solution: + # 0-1 背包问题 求恰好装满背包的最大价值 + def zeroOnePackJustFillUp(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [float('-inf') for _ in range(W + 1)] + dp[0] = 0 + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 逆序枚举背包装载重量(避免状态值错误) + for w in range(W, weight[i - 1] - 1, -1): + # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 + dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1]) + + if dp[W] == float('-inf'): + return -1 + return dp[W] +``` + +#### 思路 1:算法复杂度 + +- **时间复杂度**:$O(n \times W),ドル其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 +- **空间复杂度**:$O(W)$。 + +### 8.2 求方案总数 + +> **背包问题求方案数**:在给定背包重量 $W,ドル每件物品重量 $weight[i],ドル物品间相互关系(分组、依赖等)的背包问题中,请问在总重量不超过背包载重上限的情况下,或者在总重量不超过某一指定重量的情况下,一共有多少种方案? + +这种问题就是将原有状态转移方程中的「求最大值」变为「求和」即可。 + +下面我们以「0-1 背包问题」求方案总数为例。 + +> **0-1 背包问题求方案数**:有 $n$ 件物品和有一个最多能装重量为 $W$ 的背包。第 $i$ 件物品的重量为 $weight[i],ドル价值为 $value[i],ドル每件物品有且只有 1ドル$ 件。 +> +> 请问在总重量不超过背包载重上限的情况下,一共有多少种方案? + +- 如果使用二维状态定义,可定义状态 $dp[i][w]$ 为:前 $i$ 件物品放入一个最多能装重量为 $w$ 的背包中的方案总数。则状态转移方程为:$dp[i][w] = dp[i - 1][w] + dp[i][w - weight[i - 1]]$。 +- 如果使用一维状态定义,可定义状态 $dp[w]$ 表示为:将物品装入一个最多能装重量为 $w$ 的背包中的方案总数。则状态转移方程为:$dp[w] = dp[w] + dp[w - weight[i - 1]]$。 + +下面我们使用一维状态定义方式解决「0-1 背包问题求解方案数」问题。 + +#### 思路 2:动态规划 + 一维状态 + +1. **划分阶段**:按照物品种类的序号、当前背包的载重上限进行阶段划分。 +2. **定义状态**:定义状态 $dp[w]$ 表示为:将物品装入一个最多能装重量为 $w$ 的背包中的方案总数。 +3. **状态转移方程**:$dp[w] = dp[w] + dp[w - weight[i - 1]]$ +4. **初始条件**:如果背包载重上限为 0ドル,ドル则一共有 1ドル$ 种方案(什么也不装),即 $dp[0] = 1$。 +5. **最终结果**:根据我们之前定义的状态, $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中的方案总数。则最终结果为 $dp[W],ドル其中 $W$ 为背包的载重上限。 + +#### 思路 2:代码 + +```Python +class Solution: + # 0-1 背包问题求方案总数 + def zeroOnePackNumbers(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [0 for _ in range(W + 1)] + dp[0] = 1 + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 逆序枚举背包装载重量 + for w in range(W, weight[i - 1] - 1, -1): + # dp[w] = 前 i - 1 件物品装入载重为 w 的背包中的方案数 + 前 i 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 件物品的方案数 + dp[w] = dp[w] + dp[w - weight[i - 1]] + + return dp[W] +``` + +#### 思路 2:复杂度分析 + +- **时间复杂度**:$O(n \times W),ドル其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 +- **空间复杂度**:$O(W)$。 + +### 8.3 求最优方案数 + +> **背包问题求最优方案数**:在给定背包重量 $W,ドル每件物品重量 $weight[i]$、物品价值 $value[i],ドル物品间相互关系(分组、依赖等)的背包问题中,请问在总重量不超过背包载重上限的情况下,使背包总价值最大的方案数是多少? + +通过结合「求背包最大可得价值」和「求方案数」两个问题的思路,我们可以分别定义两个状态: + +1. 定义 $dp[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,可获得的最大价值。 +2. 定义 $op[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,使背包总价值最大的方案数。 + +下面我们以「0-1 背包问题」求最优方案数为例。 + +> **0-1 背包问题求最优方案数**:有 $n$ 种物品和一个最多能装重量为 $W$ 的背包,第 $i$ 种物品的重量为 $weight[i],ドル价值为 $value[i],ドル每件物品有且只有 1ドル$ 件。请问在总重量不超过背包载重上限的情况下,使背包总价值最大的方案数是多少? + +#### 思路 3:动态规划 + +1. **划分阶段**:按照物品种类的序号、当前背包的载重上限进行阶段划分。 +2. **定义状态**: + 1. 定义 $dp[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,可获得的最大价值。 + 2. 定义 $op[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,使背包总价值最大的方案数。 +3. **状态转移方程**: + 1. 如果 $dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1],ドル则说明选择第 $i - 1$ 件物品获得价值更高,此时方案数 $op[i][w]$ 是在 $op[i - 1][w - weight[i - 1]]$ 基础上添加了第 $i - 1$ 件物品,因此方案数不变,即:$op[i][w] = op[i - 1][w - weight[i - 1]]$。 + 2. 如果 $dp[i - 1][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1],ドル则说明选择与不选择第 $i - 1$ 件物品获得价格相等,此时方案数应为两者之和,即:$op[i][w] = op[i - 1][w] + op[i - 1][w - weight[i - 1]]$。 + 3. 如果 $dp[i - 1][w]> dp[i - 1][w - weight[i - 1]] + value[i - 1],ドル则说明不选择第 $i - 1$ 件物品获得价值更高,此时方案数等于之前方案数,即:$op[i][w] = op[i - 1][w]$。 +4. **初始条件**:如果背包载重上限为 0ドル,ドル则一共有 1ドル$ 种方案(什么也不装),即 $dp[0] = 1$。 +5. **最终结果**:根据我们之前定义的状态, $op[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,使背包总价值最大的方案数。则最终结果为 $op[size][W],ドル其中 $size$ 为物品的种类数,$W$ 为背包的载重上限。 + +#### 思路 3:代码 + +```Python +class Solution: + # 0-1 背包问题求最优方案数 思路 1 + def zeroOnePackMaxProfitNumbers1(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] + op = [[1 for _ in range(W + 1)] for _ in range(size + 1)] + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 枚举背包装载重量 + for w in range(W + 1): + # 第 i - 1 件物品装不下 + if w < weight[i - 1]: + # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 + dp[i][w] = dp[i - 1][w] + op[i][w] = op[i - 1][w] + else: + # 选择第 i - 1 件物品获得价值更高 + if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] + # 在之前方案基础上添加了第 i - 1 件物品,因此方案数量不变 + op[i][w] = op[i - 1][w - weight[i - 1]] + # 两种方式获得价格相等 + elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w] + # 方案数 = 不使用第 i - 1 件物品的方案数 + 使用第 i - 1 件物品的方案数 + op[i][w] = op[i - 1][w] + op[i - 1][w - weight[i - 1]] + # 不选择第 i - 1 件物品获得价值最高 + else: + dp[i][w] = dp[i - 1][w] + # 不选择第 i - 1 件物品,与之前方案数相等 + op[i][w] = op[i - 1][w] + + return op[size][W] +``` + +#### 思路 3:复杂度分析 + +- **时间复杂度**:$O(n \times W),ドル其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 +- **空间复杂度**:$O(n \times W)$。 + +### 8.4 求具体方案 + +> **背包问题求具体方案**:在给定背包重量 $W,ドル每件物品重量 $weight[i]$、物品价值 $value[i],ドル物品间相互关系(分组、依赖等)的背包问题中,请问将哪些物品装入背包,可使这些物品的总重量不超过背包载重上限,且价值总和最大? + +一般背包问题都是求解一个最优值,但是如果要输出该最优值的具体方案,除了 $dp[i][w],ドル我们可以再定义一个数组 $path[i][w]$ 用于记录状态转移时,所取的状态是状态转移方程中的哪一项,从而确定选择的具体物品。 + +下面我们以「0-1 背包问题」求具体方案为例。 + +> **0-1 背包问题求具体方案**:有 $n$ 种物品和一个最多能装重量为 $W$ 的背包,第 $i$ 种物品的重量为 $weight[i],ドル价值为 $value[i],ドル每件物品有且只有 1ドル$ 件。请问将哪些物品装入背包,可使这些物品的总重量不超过背包载重上限,且价值总和最大? + +#### 思路 4:动态规划 + 路径记录 + +0-1 背包问题的状态转移方程为:$dp[i][w] = max \lbrace dp[i - 1][w], \quad dp[i - 1][w - weight[i - 1]] + value[i - 1] \rbrace$ + +则我们可以再定义一个 $path[i][w]$ 用于记录状态转移时,所取的状态是状态转移方程中的哪一项。 + +1. 如果 $path[i][w] = False,ドル说明:转移到 $dp[i][w]$ 时,选择了前一项 $dp[i - 1][w],ドル并且具体方案中不包括第 $i - 1$ 件物品。 +2. 如果 $paht[i][w] = True,ドル说明:转移到 $dp[i][w]$ 时,选择了后一项 $dp[i - 1][w - weight[i - 1]] + value[i - 1],ドル并且具体方案中包括第 $i - 1$ 件物品。 + +#### 思路 4:代码 + +```Python +class Solution: + # 0-1 背包问题求具体方案 + def zeroOnePackPrintPath(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] + path = [[False for _ in range(W + 1)] for _ in range(size + 1)] + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 枚举背包装载重量 + for w in range(W + 1): + # 第 i - 1 件物品装不下 + if w < weight[i - 1]: + # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 + dp[i][w] = dp[i - 1][w] + path[i][w] = False + else: + # 选择第 i - 1 件物品获得价值更高 + if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] + # 取状态转移式第二项:在之前方案基础上添加了第 i - 1 件物品 + path[i][w] = True + # 两种方式获得价格相等 + elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w] + # 取状态转移式第二项:尽量使用第 i - 1 件物品 + path[i][w] = True + # 不选择第 i - 1 件物品获得价值最高 + else: + dp[i][w] = dp[i - 1][w] + # 取状态转移式第一项:不选择第 i - 1 件物品 + path[i][w] = False + + res = [] + i, w = size, W + while i>= 1 and w>= 0: + if path[i][w]: + res.append(str(i - 1)) + w -= weight[i - 1] + i -= 1 + + return " ".join(res[::-1]) +``` + +#### 思路 4:复杂度分析 + +- **时间复杂度**:$O(n \times W),ドル其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 +- **空间复杂度**:$O(n \times W)$。 + +### 8.5 求字典序最小的具体方案 + +这里的「字典序最小」指的是序号为 0ドル \sim size - 1$ 的物品选择方案排列出来之后的字典序最小。 + +我们仍以「0-1 背包问题」求字典序最小的具体方案为例。 + +为了使「字典序最小」。我们可以先将物品的序号进行反转,从 0ドル \sim size - 1$ 变为 $size - 1 \sim 0,ドル然后在返回具体方案时,再根据 $i = size - 1$ 将序号变回来。 + +这是为了在选择物品时,尽可能的向后选择反转后序号大的物品(即原序号小的物品),从而保证原序号为 0ドル \sim size - 1$ 的物品选择方案排列出来之后的字典序最小。 + +#### 思路 5:代码 + +```Python +class Solution: + # 0-1 背包问题求具体方案,要求最小序输出 + def zeroOnePackPrintPathMinOrder(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] + path = [[False for _ in range(W + 1)] for _ in range(size + 1)] + + weight.reverse() + value.reverse() + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 枚举背包装载重量 + for w in range(W + 1): + # 第 i - 1 件物品装不下 + if w < weight[i - 1]: + # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 + dp[i][w] = dp[i - 1][w] + path[i][w] = False + else: + # 选择第 i - 1 件物品获得价值更高 + if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] + # 取状态转移式第二项:在之前方案基础上添加了第 i - 1 件物品 + path[i][w] = True + # 两种方式获得价格相等 + elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w] + # 取状态转移式第二项:尽量使用第 i - 1 件物品 + path[i][w] = True + # 不选择第 i - 1 件物品获得价值最高 + else: + dp[i][w] = dp[i - 1][w] + # 取状态转移式第一项:不选择第 i - 1 件物品 + path[i][w] = False + + res = [] + i, w = size, W + while i>= 1 and w>= 0: + if path[i][w]: + res.append(str(size - i)) + w -= weight[i - 1] + i -= 1 + + return " ".join(res) +``` + +#### 思路 5:复杂度分析 + +- **时间复杂度**:$O(n \times W),ドル其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 +- **空间复杂度**:$O(n \times W)$。 + +## 参考资料 + +- 【资料】[背包九讲 - 崔添翼](https://github.com/tianyicui/pack) +- 【文章】[背包 DP - OI Wiki](https://oi-wiki.org/dp/knapsack/) +- 【文章】[背包问题——"01背包"最优方案总数分析及实现 - wumuzi 的博客](https://blog.csdn.net/wumuzi520/article/details/7019131) +- 【文章】[背包问题——"完全背包"最优方案总数分析及实现 - wumuzi的博客](https://blog.csdn.net/wumuzi520/article/details/7019661) +- 【文章】[背包问题——"01背包"及"完全背包"装满背包的方案总数分析及实现 - wumuzi的博客](https://blog.csdn.net/wumuzi520/article/details/7021210) \ No newline at end of file diff --git a/Contents/10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-List.md b/Contents/10.Dynamic-Programming/04.Knapsack-Problem/06.Knapsack-Problem-List.md similarity index 100% rename from Contents/10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-List.md rename to Contents/10.Dynamic-Programming/04.Knapsack-Problem/06.Knapsack-Problem-List.md diff --git a/Contents/index.md b/Contents/index.md index 9466eea2..e4b8db57 100644 --- a/Contents/index.md +++ b/Contents/index.md @@ -193,7 +193,8 @@ - [背包问题知识(二)](./10.Dynamic-Programming/04.Knapsack-Problem/02.Knapsack-Problem-02.md) - [背包问题知识(三)](./10.Dynamic-Programming/04.Knapsack-Problem/03.Knapsack-Problem-03.md) - [背包问题知识(四)](./10.Dynamic-Programming/04.Knapsack-Problem/04.Knapsack-Problem-04.md) - - [背包问题题目](./10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-List.md) + - [背包问题知识(五)](./10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-05.md) + - [背包问题题目](./10.Dynamic-Programming/04.Knapsack-Problem/06.Knapsack-Problem-List.md) - 区间 DP - [区间 DP 知识](./10.Dynamic-Programming/05.Interval-DP/01.Interval-DP.md) - [区间 DP 题目](./10.Dynamic-Programming/05.Interval-DP/02.Interval-DP-List.md) diff --git a/README.md b/README.md index e3e902cf..6a8618cb 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,8 @@ - [背包问题知识(二)](./Contents/10.Dynamic-Programming/04.Knapsack-Problem/02.Knapsack-Problem-02.md) - [背包问题知识(三)](./Contents/10.Dynamic-Programming/04.Knapsack-Problem/03.Knapsack-Problem-03.md) - [背包问题知识(四)](./Contents/10.Dynamic-Programming/04.Knapsack-Problem/04.Knapsack-Problem-04.md) - - [背包问题题目](./Contents/10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-List.md) + - [背包问题知识(五)](./Contents/10.Dynamic-Programming/04.Knapsack-Problem/05.Knapsack-Problem-05.md) + - [背包问题题目](./Contents/10.Dynamic-Programming/04.Knapsack-Problem/06.Knapsack-Problem-List.md) - 区间 DP - [区间 DP 知识](./Contents/10.Dynamic-Programming/05.Interval-DP/01.Interval-DP.md) - [区间 DP 题目](./Contents/10.Dynamic-Programming/05.Interval-DP/02.Interval-DP-List.md) diff --git a/Templates/10.Dynamic-Programming/Pack-MixedPack.py b/Templates/10.Dynamic-Programming/Pack-MixedPack.py new file mode 100644 index 00000000..98de74f9 --- /dev/null +++ b/Templates/10.Dynamic-Programming/Pack-MixedPack.py @@ -0,0 +1,50 @@ +class Solution: + def mixedPackMethod1(self, weight: [int], value: [int], count: [int], W: int): + weight_new, value_new, count_new = [], [], [] + + # 二进制优化 + for i in range(len(weight)): + cnt = count[i] + # 多重背包问题,转为 0-1 背包问题 + if cnt> 0: + k = 1 + while k <= cnt: + cnt -= k + weight_new.append(weight[i] * k) + value_new.append(value[i] * k) + count_new.append(1) + k *= 2 + if cnt> 0: + weight_new.append(weight[i] * cnt) + value_new.append(value[i] * cnt) + count_new.append(1) + # 0-1 背包问题,直接添加 + elif cnt == -1: + weight_new.append(weight[i]) + value_new.append(value[i]) + count_new.append(1) + # 完全背包问题,标记并添加 + else: + weight_new.append(weight[i]) + value_new.append(value[i]) + count_new.append(0) + + dp = [0 for _ in range(W + 1)] + size = len(weight_new) + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 0-1 背包问题 + if count_new[i - 1] == 1: + # 逆序枚举背包装载重量(避免状态值错误) + for w in range(W, weight_new[i - 1] - 1, -1): + # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight_new[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 + dp[w] = max(dp[w], dp[w - weight_new[i - 1]] + value_new[i - 1]) + # 完全背包问题 + else: + # 正序枚举背包装载重量 + for w in range(weight_new[i - 1], W + 1): + # dp[w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」与「前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品所得的最大价值」两者中的最大值 + dp[w] = max(dp[w], dp[w - weight_new[i - 1]] + value_new[i - 1]) + + return dp[W] \ No newline at end of file diff --git a/Templates/10.Dynamic-Programming/Pack-ProblemVariants.py b/Templates/10.Dynamic-Programming/Pack-ProblemVariants.py index b34153f1..803b35ad 100644 --- a/Templates/10.Dynamic-Programming/Pack-ProblemVariants.py +++ b/Templates/10.Dynamic-Programming/Pack-ProblemVariants.py @@ -1,13 +1,274 @@ class Solution: - # 完全问题求解方案总数 + # 1. 求恰好装满背包的最大价值 + + # 0-1 背包问题 求恰好装满背包的最大价值 + def zeroOnePackJustFillUp(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [float('-inf') for _ in range(W + 1)] + dp[0] = 0 + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 逆序枚举背包装载重量(避免状态值错误) + for w in range(W, weight[i - 1] - 1, -1): + # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 + dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1]) + + if dp[W] == float('-inf'): + return -1 + return dp[W] + + # 完全背包问题 求恰好装满背包的最大价值 + def completePackJustFillUp(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [float('-inf') for _ in range(W + 1)] + dp[0] = 0 + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 正序枚举背包装载重量 + for w in range(weight[i - 1], W + 1): + # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 + dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1]) + + if dp[W] == float('-inf'): + return -1 + return dp[W] + + + # 2. 求方案总数 + + # 0-1 背包问题 求方案总数 def zeroOnePackNumbers(self, weight: [int], value: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] + dp[0] = 1 + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 逆序枚举背包装载重量 + for w in range(W, weight[i - 1] - 1, -1): + # dp[w] = 前 i - 1 件物品装入载重为 w 的背包中的方案数 + 前 i 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 件物品的方案数 + dp[w] = dp[w] + dp[w - weight[i - 1]] + + return dp[W] + + # 完全背包问题求方案总数 + def completePackNumbers(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [0 for _ in range(W + 1)] + dp[0] = 1 # 枚举前 i 种物品 for i in range(1, size + 1): # 正序枚举背包装载重量 for w in range(weight[i - 1], W + 1): - dp[w] = sum(dp[w], dp[w - weight[i - 1]]) + # dp[w] = 前 i - 1 种物品装入载重为 w 的背包中的方案数 + 前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品的方案数 + dp[w] = dp[w] + dp[w - weight[i - 1]] - return dp[W] \ No newline at end of file + return dp[W] + + + # 3. 求最优方案数 + + # 0-1 背包问题 求最优方案数 思路 1 + def zeroOnePackMaxProfitNumbers1(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] + op = [[1 for _ in range(W + 1)] for _ in range(size + 1)] + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 枚举背包装载重量 + for w in range(W + 1): + # 第 i - 1 件物品装不下 + if w < weight[i - 1]: + # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 + dp[i][w] = dp[i - 1][w] + op[i][w] = op[i - 1][w] + else: + # 选择第 i - 1 件物品获得价值更高 + if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] + # 在之前方案基础上添加了第 i - 1 件物品,因此方案数量不变 + op[i][w] = op[i - 1][w - weight[i - 1]] + # 两种方式获得价格相等 + elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w] + # 方案数 = 不使用第 i - 1 件物品的方案数 + 使用第 i - 1 件物品的方案数 + op[i][w] = op[i - 1][w] + op[i - 1][w - weight[i - 1]] + # 不选择第 i - 1 件物品获得价值最高 + else: + dp[i][w] = dp[i - 1][w] + # 不选择第 i - 1 件物品,与之前方案数相等 + op[i][w] = op[i - 1][w] + + return op[size][W] + + # 0-1 背包问题求最优方案数 思路 2 + def zeroOnePackMaxProfitNumbers2(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [0 for _ in range(W + 1)] + op = [1 for _ in range(W + 1)] + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 枚举背包装载重量 + for w in range(W, weight[i - 1] - 1, -1): + # 选择第 i - 1 件物品获得价值更高 + if dp[w] < dp[w - weight[i - 1]] + value[i - 1]: + dp[w] = dp[w - weight[i - 1]] + value[i - 1] + # 在之前方案基础上添加了第 i - 1 件物品,因此方案数量不变 + op[w] = op[w - weight[i - 1]] + # 两种方式获得价格相等 + elif dp[w] == dp[w - weight[i - 1]] + value[i - 1]: + # 方案数 = 不使用第 i - 1 件物品的方案数 + 使用第 i - 1 件物品的方案数 + op[w] = op[w] + op[w - weight[i - 1]] + + return op[W] + + # 完全背包问题求最优方案数 思路 1 + def completePackMaxProfitNumbers1(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] + op = [[1 for _ in range(W + 1)] for _ in range(size + 1)] + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 枚举背包装载重量 + for w in range(W + 1): + # 第 i - 1 件物品装不下 + if w < weight[i - 1]: + # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 + dp[i][w] = dp[i - 1][w] + op[i][w] = op[i - 1][w] + else: + # 选择第 i - 1 件物品获得价值更高 + if dp[i - 1][w] < dp[i][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i][w - weight[i - 1]] + value[i - 1] + # 在之前方案基础上添加了 1 件第 i - 1 种物品,因此方案数量不变 + op[i][w] = op[i][w - weight[i - 1]] + # 两种方式获得价格相等 + elif dp[i - 1][w] == dp[i][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w] + # 方案数 = 不使用第 i - 1 种物品的方案数 + 使用 1 件第 i - 1 种物品的方案数 + op[i][w] = op[i - 1][w] + op[i][w - weight[i - 1]] + # 不选择第 i - 1 件物品获得价值最高 + else: + dp[i][w] = dp[i - 1][w] + # 不选择第 i - 1 种物品,与之前方案数相等 + op[i][w] = op[i - 1][w] + + return dp[size][W] + + # 完全背包问题求最优方案数 思路 2 + def completePackMaxProfitNumbers2(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [0 for _ in range(W + 1)] + op = [1 for _ in range(W + 1)] + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 枚举背包装载重量 + for w in range(weight[i - 1], W + 1): + # 选择第 i - 1 件物品获得价值更高 + if dp[w] < dp[w - weight[i - 1]] + value[i - 1]: + dp[w] = dp[w - weight[i - 1]] + value[i - 1] + # 在之前方案基础上添加了 1 件第 i - 1 种物品,因此方案数量不变 + op[w] = op[w - weight[i - 1]] + # 两种方式获得价格相等 + elif dp[w] == dp[w - weight[i - 1]] + value[i - 1]: + # 方案数 = 不使用第 i - 1 种物品的方案数 + 使用 1 件第 i - 1 种物品的方案数 + op[w] = op[w] + op[w - weight[i - 1]] + + return dp[size][W] + + + # 4. 求具体方案 + + # 0-1 背包问题求具体方案 + def zeroOnePackPrintPath(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] + path = [[False for _ in range(W + 1)] for _ in range(size + 1)] + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 枚举背包装载重量 + for w in range(W + 1): + # 第 i - 1 件物品装不下 + if w < weight[i - 1]: + # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 + dp[i][w] = dp[i - 1][w] + path[i][w] = False + else: + # 选择第 i - 1 件物品获得价值更高 + if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] + # 取状态转移式第二项:在之前方案基础上添加了第 i - 1 件物品 + path[i][w] = True + # 两种方式获得价格相等 + elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w] + # 取状态转移式第二项:尽量使用第 i - 1 件物品 + path[i][w] = True + # 不选择第 i - 1 件物品获得价值最高 + else: + dp[i][w] = dp[i - 1][w] + # 取状态转移式第一项:不选择第 i - 1 件物品 + path[i][w] = False + + res = [] + i, w = size, W + while i>= 1 and w>= 0: + if path[i][w]: + res.append(str(i - 1)) + w -= weight[i - 1] + i -= 1 + + return " ".join(res[::-1]) + + # 0-1 背包问题求具体方案,要求最小序输出 + def zeroOnePackPrintPathMinOrder(self, weight: [int], value: [int], W: int): + size = len(weight) + dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] + path = [[False for _ in range(W + 1)] for _ in range(size + 1)] + + weight.reverse() + value.reverse() + + # 枚举前 i 种物品 + for i in range(1, size + 1): + # 枚举背包装载重量 + for w in range(W + 1): + # 第 i - 1 件物品装不下 + if w < weight[i - 1]: + # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 + dp[i][w] = dp[i - 1][w] + path[i][w] = False + else: + # 选择第 i - 1 件物品获得价值更高 + if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] + # 取状态转移式第二项:在之前方案基础上添加了第 i - 1 件物品 + path[i][w] = True + # 两种方式获得价格相等 + elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: + dp[i][w] = dp[i - 1][w] + # 取状态转移式第二项:尽量使用第 i - 1 件物品 + path[i][w] = True + # 不选择第 i - 1 件物品获得价值最高 + else: + dp[i][w] = dp[i - 1][w] + # 取状态转移式第一项:不选择第 i - 1 件物品 + path[i][w] = False + + res = [] + i, w = size, W + while i>= 1 and w>= 0: + if path[i][w]: + res.append(str(size - i)) + w -= weight[i - 1] + i -= 1 + + return " ".join(res) \ No newline at end of file