背包问题描述:
有一个小偷在偷窃一家商店时发现有 n 件物品,第 i 件物品价值 vi 元,重 wi 磅,此处 vi 和 wi 都是整数。小偷希望带走的物品价值越高越好,但他的背包至多只能装下 W 磅的东西,W 为整数。那么,小偷应该带走哪些物品呢?
背包问题可以包含不同的限定条件:
- 部分背包问题:可以带走物品的一部分,而不必做出 0-1 的二分选择。
- 0-1 背包问题:每件物品或者带走或者留下,不能只带走一部分或者带走两次以上。
部分背包问题拥有典型的贪心选择性质,可以使用贪心算法来解决。0-1 背包问题中需要考虑背包是否填满以取得最高的价值,在比较物品加进去或不加进去的子问题时产生了许多重叠的子问题,可以使用动态规划算法来解决。
部分背包问题
有一个小偷在偷窃一家商店时发现有 n 件物品,第 i 件物品价值 vi 元,重 wi 磅,此处 vi 和 wi 都是整数。小偷希望带走的物品价值越高越好,但他的背包至多只能装下 W 磅的东西,W 为整数。为尽可能多的带走更多的东西,小偷可以带走物品的一部分,而不必做出 0-1 的二分选择。那么,小偷应该带走哪些物品呢?
最优子结构
如果从最优物品组合中去掉某物品 j 的重量 w 磅,则余下的物品必须是可以从 n - 1 件物品和物品 j 余下的 wj - w 磅中可带走的、重量至多为 W - w 的最值钱的一组东西。
贪心选择性质
每件物品计算每磅的价值 vi / wi,小偷开始从具有最大价值的物品尽量多取一些,如果拿完该物品后背包还有余量,则可继续取价值次大的物品,以此类推,直到背包取满为止。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 5 namespace AlgorithmTests 6 { 7 class Program 8 { 9 static void Main(string[] args) 10 { 11 int n = 3; 12 int[] values = new int[3] { 60, 100, 120 }; 13 int[] weights = new int[3] { 10, 20, 30 }; 14 int W = 50; 15 16 GreedyKnapsack(n, values, weights, W); 17 18 Console.ReadKey(); 19 } 20 21 static void GreedyKnapsack(int n, int[] values, int[] weights, int W) 22 { 23 // 计算物品每磅价值 24 Dictionary<int, double> valuePerPound = new Dictionary<int, double>(); 25 for (int i = 0; i < n; i++) 26 { 27 valuePerPound.Add(i, values[i] / weights[i]); 28 } 29 30 // 按照每磅价值从大到小排序 31 int[] orderedPerPound = valuePerPound 32 .OrderByDescending(v => v.Value) 33 .Select(v => v.Key) 34 .ToArray(); 35 36 // 计算装包量 37 int filling = W; 38 int totalValue = 0; 39 int j = 0; 40 List<int> packed = new List<int>(); 41 while (filling > 0) 42 { 43 if (filling >= weights[orderedPerPound[j]]) 44 { 45 // 完全装入 46 filling -= weights[orderedPerPound[j]]; 47 totalValue += values[orderedPerPound[j]]; 48 packed.Add(weights[orderedPerPound[j]]); 49 } 50 else 51 { 52 // 部分填装 53 totalValue += filling * values[orderedPerPound[j]] / weights[orderedPerPound[j]]; 54 filling = 0; 55 packed.Add(weights[orderedPerPound[j]]); 56 } 57 58 j++; 59 } 60 61 Console.WriteLine(totalValue); 62 foreach (var item in packed) 63 { 64 Console.WriteLine(item); 65 } 66 } 67 } 68 }
0-1 背包问题
有一个小偷在偷窃一家商店时发现有 n 件物品,第 i 件物品价值 vi 元,重 wi 磅,此处 vi 和 wi 都是整数。小偷希望带走的物品价值越高越好,但他的背包至多只能装下 W 磅的东西,W 为整数。每件物品或者带走或者留下,不能只带走一部分或者带走两次以上。那么,小偷应该带走哪些物品呢?
最优子结构
重量至多为 W 磅的价值最高的一组物品,如果从中去掉物品 j,余下的必须是小偷从除 j 以外的 n - 1 件物品中可以带走的重量至多为 W - wj 的价值最高的一组物品。
重复子问题
当考虑是否要把一件物品加到背包中时,必须把该物品加进去的子问题的解与不加进去的子问题的解进行比较,也就产生了许多重叠的子问题。
状态
dp[i, j] 表示前 i 个物品装到剩余容量为 j 的背包里能达到的最大价值。
int[,] dp = new int[n + 1, W + 1];
状态转移方程
考查第 i 个物品装入背包的时候,其实是在考查第 i-1 个物品装不装入背包。
dp[i, j] = max{ dp[i-1, j], dp[i-1, j-weights[i-1]] + values[i-1] }
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 5 namespace AlgorithmTests 6 { 7 class Program 8 { 9 static void Main(string[] args) 10 { 11 int n = 3; 12 int[] values = new int[3] { 60, 100, 120 }; 13 int[] weights = new int[3] { 10, 20, 30 }; 14 int W = 50; 15 16 DynamicProgrammingKnapsack(n, values, weights, W); 17 18 Console.ReadKey(); 19 } 20 21 static void DynamicProgrammingKnapsack(int n, int[] values, int[] weights, int W) 22 { 23 // 状态 dp[i, j] 表示前 i 个物品装到剩余容量为 j 的背包里能达到的最大价值 24 int[,] dp = new int[n + 1, W + 1]; 25 26 // 状态转移方程 27 // dp[i, j] = max{ dp[i-1, j], dp[i-1, j-weights[i-1]] + values[i-1] } 28 // 考查第 i 个物品装入背包的时候,其实是在考查第 i-1 个物品装不装入背包 29 for (int i = 0; i <= n; i++) 30 { 31 for (int j = 0; j <= W; j++) 32 { 33 if (i > 0 && j >= weights[i - 1]) 34 { 35 int v = dp[i - 1, j - weights[i - 1]] + values[i - 1]; 36 if (v > dp[i - 1, j]) 37 { 38 dp[i, j] = v; 39 } 40 else 41 { 42 dp[i, j] = dp[i - 1, j]; 43 } 44 } 45 } 46 } 47 48 Console.WriteLine(dp[n, W]); 49 } 50 } 51 }
本篇文章《背包问题》由 Dennis Gao 发表自博客园,未经作者本人同意禁止任何形式的转载,任何自动或人为的爬虫转载行为均为耍流氓。