背包问题一直是动态规划中的经典问题。这个问题又分成01背包,完全背包,多重背包,分组背包等等。。我在这里只记录下01背包 (0-1knapsack)和完全背包(unbounded knapsack)。背包问题的简单描述就是有一个背包和一堆物品。每个物品有自己的大小和价值。我们希望在一个特定容量的背包中放入价值尽可能大的物 品。01背包呢就是每个物品最多只能放一次,也就是要么放要么不放,所以被称为01背包。而完全背包呢就是每个物品可以放无限次。我们更喜欢 unbounded knapsack这个名字。因为完全这次词其实没有表达清楚不限的意思。下面就是对完全背包和01背包就动态规划的方法做一些解析。
这两个问题中似乎01背包比较爽快,就从爽快的先说。类似的,我们还是要找到一个优化的子结构,然后递归式,然后代码。。
我们用M[i,j]来表示选用1...i件物品放入容量为j的背包时最大的价值。那样就吧这个情况分成2中情况分析。很简单。用这第i个物品,或者 不用。如果不用那么M[i,j]就等于M[i-1,j](如果看不明白可以想象一下M的定义,这里M[i-1,j]的意义就是用1...i-1个物品来装 容量为j的背包时的最优策略,也就是不用第i个物品了。) 。那如果用呢?那就是M[i-1, j - si ]+vi。(因为前面的策略已经满了j个容量,所以如果选用第i个物品,就要把总的容量j中减去第i个物品的容量,然后找到对应的M[i-1, j - si]加i的价值) 然后取一个大的值来决定选用那种策略。这个描述虽然有点拗口,但仔细琢磨意思还是直观的。所以这个表达式可以写成
M[i,j] = max { M[i-1,j], M[i-1,j-si]+vi }. 如果我们把他放在二维数组里面的话,可以看出来这里的M[i,j]取决与上一层的M[i-1]中的2个元素。
接下来我们再来看看完全背包的问题。当然在这里M[i,j]就不是选择或者不选择i能够决定的。而是选择哪个i是合适。所以我们把i从这个 M[i,j]中搬出去,寻找一个更简洁的表示法。M[j]表示容量为j时的最优方案。这个方案取决与所有M[1...j-1]的值。那怎么表示呢?可以写 成
M(j) = max { M(j-1), max {M(j-si)+vi, si<j } }
可以理解成M[j]取决与任何一个si<j的子结构加上pi的最大值。如果一个都不存在那就取决于M[j-1]。或者说从M[1...j- 1]中选择某些个最优的方案。M[j-s1].M[j-s2].. M[j-sn]这些方案加上现有的si可能组成可能的M[j]的方案,找到这些子方案的最优值。这样就可以保证得到的M[j]是最优的。
下面就是一个实现好的C++代码:
- // Knapsack.cpp : Defines the entry point for the console application.
- //
- #include "stdafx.h"
- #include <vector>
- #include <limits>
- #include <iostream>
- //Unbounded knapsack problem
- struct Item
- {
- int w;
- int p;
- };
- /**
- * Description: Calulate the max profit of unbounded knapsack problem
- *@param b, item vector
- *@param c, knapsack capacity
- *@return the max profit
- */
- int unbounded_knapsack( const std::vector <Item>& b, unsigned int c )
- {
- std::vector<int > m (c+1);
- std::vector<int > p (c+1); // use to backtrack the item chosen
- for ( int j=1;j<=c;j++)
- {
- int max = 0;
- for ( int i=0;i<b.size();i++)
- {
- if ( j-b[i].w >=0)
- {
- if ( max < m[j-b[i].w] + b[i].p )
- {
- max = m[j-b[i].w] + b[i].p;
- p[j] = j-b[i].w;
- }
- }
- }
- m[j] = std::max(max, m[j-1]);
- if ( max < m[j-1] )
- {
- p[j] = p[j-1];
- }
- m[j] = max;
- }
- return m[c];
- }
- /**
- * Description: Calulate the max profit of 0-1 knapsack problem
- *@param b, item vector
- *@param c, knapsack capacity
- *@return the max profit
- */
- int zero_one_knapsack( const std::vector <Item>& b, unsigned int c )
- {
- if (b.size() < 1) return 0;
- std::vector< std::vector<int > > m ( b.size()+1, std::vector< int >(c+1) );
- for ( int i=1;i<=b.size();i++)
- {
- for ( int j=1;j<=c;j++)
- {
- if ( j-b[i-1].w >=0 )
- m[i][j] = std::max<>( m[i-1][j], m[i-1][j-b[i-1].w]+b[i-1].p );
- else
- m[i][j] = m[i-1][j];
- }
- }
- return m[b.size()][c];
- }
- int _tmain( int argc, _TCHAR* argv[])
- {
- std::vector<Item> b(5);
- b[0].w = 3;
- b[0].p = 4;
- b[1].w = 4;
- b[1].p = 5;
- b[2].w = 7;
- b[2].p = 10;
- b[3].w = 8;
- b[3].p = 11;
- b[4].w = 9;
- b[4].p = 13;
- int c = 17;
- std::cout<<unbounded_knapsack(b,c)<<std::endl;
- std::cout<<zero_one_knapsack(b,c)<<std::endl;
- system("pause" );
- return 0;
- }
现在我们手上有了这些解决方案就可以顺带看看其他一些类似的问题。希望可以用现有的方案来解释那些类似的问题。
1. Subset Sum Problem . Partition Problem . 这是两个类似的问题。Subset Sum Problem被称为子集和问题。题目的意思是给定一个集合,判断是否存在和等于某特定值s的子集。 Partition Problem的中文名字我不知道:)但是题目的意思是一个给定的集合A,把他分成A1和A2两个子集。使得两个子集合的差|A1-A2|尽量小。第二个 问题其实可以转化成第一个问题。可以找到全集A的合1/2。然后找到和A/2最接近的子集就可以了。这样的问题和01背包还是很类似的。我们依然定义一个 M[i,j]这里的意义有些不一样,M[i,j]表示能否用第1...i-1个整数找到和为j的方案。如果可以就是1,不能就为0。那样这个M[i,j] 可以表示为:M[i,j] = M[i-1,j] || M[i-1,j-Ai] 也就是说对于第i个整数来说,要么存在不用这个整数的方案M[i-1,j]或者存在方案M[i-i,j-Ai],这第二个方案加上了当年Ai也就可以了。有兴趣的话可以实现一下。
2. change-making Problem . 这个问题通俗的描述就是你去超市买东西,最后的零钱是75分。你肯定不希望营业员给你75一个1分的硬币,营业员需要做的事情就是计算出一个给你最少个数 硬币的方案。如果我们假定特定的硬币营业员都有的话,我们可以把这个问题看成近似的完全背包问题。不过完全背包问题的要求是满足重量的价值尽量大。这里在 于满足条件的硬币数量尽可能小。一样的我们定义一个和完全背包类似的M[j]来表示找零钱j需要的硬币数量。那么M[j] = min { M[j-Ai] + 1, 1<=i<=n }这个j取决于比之小的所有M[j-Ai]的最优硬币数量方案的最小值。除了这里的max换乘了min。其他的都非常像吧。
其实类似的问题还是有很多,这里先贴一些出来总结,如果有更好的例子。以后在补充上去,但是现在这种工作的方式让人真的很舒服^_^