01 背包问题
PS: 我找到的谢的最好的一个 转载如下
原创文章,转载请注明出处~ http://www.cnblogs.com/justinzhang/archive/2012/04/10/2441199.html
记得大二参加数学建模的时候,开始接触了动态规划,在听过张老师的课后,当时使用matlab写了背包问题的代码,记得使用递归实现的。总之,对这个问题认识得非常的不清楚。以前强制记了好几次代码,过一段时间又忘了。虽然每次看书的时候还是可以回想起解题思路,但是心里总有种不踏实的感觉,由于上次腾讯笔试的刺激,发现这么多年以来,自己做的事情都有点南辕北辙、舍本逐末。计算机核心的离散数学、编译原理、组合数学、自动机理论,被放在了一边,从不过问。而去追求一些花哨的东西。真的是勿以往之不谏啊~~所以,虽然自己天天都在努力的工作,但是心里始终有不踏实的感觉,总是又被谴责鼻子走的感觉,那是因为自己根本没有抓住本质的东西,所以才会如此被动~~~计算机虽然已经和数学分得很开了,但是计算机毕竟产生于数学,所以逃避数学的做法是万万不可的。如果背离了数学而去搞计算,那么高度肯定是上不去了,以前认为只要把算法学好了就够了,现在才发现,离散数学、组合数学、自动机理论是如此的重要,在以后的学习和工作中,一定要勤思考才行。。
背包问题有0-1背包问题和fraction背包问题,前者规定每个物品要么选,要么不选,而fraction knanpsack允许选取一个物品的一部分,0-1背包问题是NP难的,而fraction knapsacks的复杂度是O(n*logn), 只需要将单位物品的价值按降序排列,利用贪心策略选取即可得到最优解。
给定一个背包,容量为C,有n个物品,重量为n维行向量w,价值为n维行向量v. |v|=|w|=n, n维向量y=[0,1,0….yn]代表物品的选法,其中的没有元素yi,要么是0、要么是1,为零代表选取第i个物品,为0表示不选取第i个物品。 目标函数是:Max(Sum(vi*yi)), 约束条件是sum(wi*yi) <=C, 其中 1=< i <=n.
背包问题具有最优子结构,令f(n,C)代表,有n个待选物品,背包容量为C时的最优解,此时物品选择向量为y=[y1,y2,…yn], 那么当yn=1时,y’=[y1, y2, …yn-1],必然为f(n-1, C-wn)的物品选择向量,当yn=0时,必然为f(n-1,C)的最优物品选择向量。所以背包问题可以由动态规划来求解。
根据上面的分析,我们可以得到如下的递归式:
当wn>C时, f(n,C)=f(n-1,C);
当wn<=C时,f(n,C) = max(f(n-1,C), vn+f(n-1, C-wn) );
初始条件为:f(i, 0) = 0; f(0,i) = 0; f(0,0) = 0;
根据上面的分析用递归实现的0-1背包代码如下:
1: /* 2: * 3: *时间:22012年4月10日19:46:27 4: *作者:JustinZhang 5: *Email:uestczhangchao@gmail.com 6: */ 7: 8: #include <iostream> 9: #include <iomanip> 10: using namespace std; 11: 12: int w[]={1,3,4,5};//物品重量数组 13: int v[]={2,30,44,20};//物品价值数组 14: int C=5;//背包容量 15: int y[4]={-1,-1,-1,-1};//解向量,y[i]=1表示选取物品,y[i]=0表示不选取物品 16: 17: 18: int f(int n, int C) 19: { 20: if (n==0 || C==0)//当物品数量为0,或者背包容量为0时,最优解为0 21: { 22: return 0; 23: } 24: else 25: { 26: //从当前所剩物品的最后一个物品开始向前,逐个判断是否要添加到背包中 27: for (int i=n-1; i>=0;i++) 28: { 29: //如果当前要判断的物品重量大于背包当前所剩的容量,那么就不选择这个物品 30: //在这种情况的最优解为f(n-1,C) 31: 32: if (w[i]>C) 33: { 34: y[i]=0; 35: return f(n-1,C); 36: } 37: else 38: { 39: //如果当前待判断的物品重量wi<C,那么就选取f(n-1,C)和vi+f(n-1,C-wi)中的最大值 40: int tmp1 = f(n-1,C);//不选择物品i的情况下的最优解 41: int tmp2 = v[i] + f(n-1,C-w[i]);//选择物品i的情况下的最优解 42: 43: //返回选择物品i和不选择物品i中最优解大的一个 44: if (tmp1 > tmp2) 45: { 46: y[i]=0;//这种情况下表示物品i未被选取 47: return tmp1; 48: } 49: else 50: { 51: y[i]=1;//物品i被选取 52: return tmp2; 53: } 54: 55: } 56: } 57: 58: } 59: 60: } 61: 62: 63: int main() 64: { 65: 66: int maxvalue = f(4,5); 67: for (int i=0; i<4; i++) 68: { 69: if (y[i]==1)//为1表示相应的物品被选取 70: { 71: cout << "Object "<< i+1 << " is selected. " << "It's Vaule is " << setw(2)<< v[i] \ 72: << " It's Weight is"<< setw(2)<<w[i] << endl; 73: } 74: } 75: 76: cout << "Maximum Value is: "<< maxvalue << endl; 77: return 0; 78: } 79: 80:
运行结果如下:
在算法导论上,背包问题的伪代码如下:
我们可以看到复杂度为O(nW),初看之下还以为这个算法的复杂度是多项式时间,但是书中明明说0-1背包问题是NP难问题啊,难道书中说错了吗?其实这里忽略的问题是,在上面的程序中n才是输入规模,而W并不是输入规模,因为它是背包的容量,而背包的数量一直都是为1的,如果在物品数量为n的情况下,背包的容量为2^n,那么这个算法的复杂度就是O(n*2^n), 所以这个问题是NP难的~~
对该算法的实现如下所示:
1: /* 2: 时间:2012年4月10日21:08:25 3: 作者:JustinZhang 4: Email:uestczhangchao@gmail.com 5: Desc:算法导论中背包问题的实现,采用非递归的方式;更加深入的理解递归是如何工作的,因为此时没有用递归调用, 6: 是使用矩阵来记录每一步计算的结果,最后得到最优解;在最优解找到过后,还要给出那些物品被选中,此时是使用c[i][j] 7: 从c[n][W]往前查找,利用递归公式做出判断(如果当前物品被选中,那么没有该物品的最优解(c[i-1][j-w[i])就等于当前的最优解减去当前物品的价值(c[i][j]-v[i])); 8: */ 9: 10: #include <iostream> 11: #include <iomanip> 12: using namespace std; 13: 14: 15: 16: const int n = 4; 17: const int W = 5; 18: 19: int c[n+1][W+1]; 20: 21: /* 22: input: 23: int v[]:物品的价值数组; 24: int w[]:物品重量数组; 25: int n:物品个数; 26: int W:背包容量; 27: 28: */ 29: void knapsack_0_1_problem(int v[],int w[], int n, int W) 30: { 31: c[0][0] = 0; 32: //c[i][j]表示,将i个物品放入容量为j的背包中所具有的价值最大值; 33: for (int i=1; i<n; i++) 34: { 35: c[i][0] = 0;//如果背包容量为0,那么i个物品放入该背包的最优值都是0; 36: } 37: for (int j=1; j<W; j++) 38: { 39: c[0][j] = 0;//如果物品的数量为0,那么将0个物品放入大小为j的背包中的最优值就是0; 40: } 41: 42: //下面开始使用递归公式求解最优值 43: for(int i=1; i<=n;i++) 44: { 45: for (int ww=1; ww<=W; ww++) 46: { 47: //如果当前物品的重量大于背包的体积 48: if (w[i]>ww) 49: { 50: c[i][ww] = c[i-1][ww]; 51: } 52: else 53: { 54: if (c[i-1][ww] > (v[i]+c[i-1][ww-w[i]])) 55: { 56: c[i][ww] = c[i-1][ww]; 57: } 58: else 59: { 60: c[i][ww] = v[i]+c[i-1][ww-w[i]]; 61: } 62: } 63: } 64: 65: } 66: } 67: 68: 69: 70: int main() 71: { 72: 73: //v[]和w[]分别为价值数组和重量数组,并且下标都是从1开始算的,第0个元素设为-999; 74: //这里要下标必须从1开始,因为在迭代的过程当中有c[i][j]=max{c[i-1][j], c[i-1][j-w[i]]}, 75: //如果元素的下标从0开始取的话,我们在遍历物品价值和重量数组的时候,就会有c[0][j]=max{c[i-1][j], c[-1][j-w[0]]},这里就会出现数组下标为0的情况, 76: //所以下标选择从1开始。 77: int v[n+1] = {-999, 6, 7, 14, 8}; 78: int w[n+1] = {-999, 1, 2, 4, 5}; 79: 80: knapsack_0_1_problem(v,w,n,W); 81: 82: for (int i=0; i<=n; i++) 83: { 84: for (int j=0; j<=W; j++) 85: { 86: cout << setw(3) << c[i][j]; 87: } 88: cout << endl; 89: } 90: 91: int remainspace = W;//该变量用来记录背包当前还剩余的容量 92: 93: //以下过程,从c[n][W]开始向前寻找,那些物品被选择装进背包中; 94: //判断条件为:如果第n个物品被选择装进背包中,那么c[n][W]=c[n-1][W-w[n]] + v[n];此时reminspace将变为remainspace-w[n] 95: //否则物品n没有被选择,此时背包剩余的容量remainspace不会发生改变; 96: //按照上述方法一直往前找,即可以将所有装入背包中物品找到。 97: 98: for(int i=n; i>=1; i--) 99: { 100: 101: if (remainspace >= w[i]) 102: { 103: 104: if ((c[i][remainspace]-c[i-1][remainspace-w[i]]==v[i])) 105: { 106: cout << "item " << i << " is selected!" << endl; 107: remainspace = remainspace - w[i];//如果第i个物品被选择,那么背包剩余容量将减去第i个物品的重量 ; //,2012年5月2日21:45:33 修改为remainspace - w[i];以前为 W-w[i]; 108: } 109: } 110: 111: } 112: 113: cout << "Maximum values is " << c[n][W] << endl; 114: 115: return 0; 116: }
运行结果如下: