一, 问题描述: 有 n 个物品, 它们有各自的重量和价值, 现有给定容量的背包, 如何让背包里装入的物品具有最大的价值总和?
二, 总体思路: 根据动态规划解题步骤 (问题抽象化, 建立模型, 寻找约束条件, 判断是否满足最优性原理, 找大问题与小问题的递推关系式, 填表, 寻找解组成) 找出 01 背包问题的最优解以及解组成, 然后编写代码实现;
三, 动态规划的原理及过程:
eg:number=4,capacity=8
i | 1 | 2 | 3 | 4 |
w(体积) | 2 | 3 | 4 | 5 |
v(价值) | 3 | 4 | 5 | 6 |
1, 原理
动态规划与分治法类似, 都是把大问题拆分成小问题, 通过寻找大问题与小问题的递推关系, 解决一个个小问题, 最终达到解决原问题的效果. 但不同的是, 分治法在子问题和子子问题等上被重复计算了很多次, 而动态规划则具有记忆性, 通过填写表把所有已经解决的子问题答案纪录下来, 在新问题里需要用到的子问题可以直接提取, 避免了重复计算, 从而节约了时间, 所以在问题满足最优性原理之后, 用动态规划解决问题的核心就在于填表, 表填写完毕, 最优解也就找到.
2, 过程
a) 把背包问题抽象化(X1,X2,...,Xn, 其中 Xi 取 0 或 1, 表示第 i 个物品选或不选),Vi 表示第 i 个物品的价值, Wi 表示第 i 个物品的体积(重量);
b) 建立模型, 即求 max(V1X1+V2X2+...+VnXn);
c) 约束条件, W1X1+W2X2+...+WnXn<capacity;
d) 定义 V(i,j): 当前背包容量 j, 前 i 个物品最佳组合对应的价值;
e) 最优性原理是动态规划的基础, 最优性原理是指 "多阶段决策过程的最优决策序列具有这样的性质: 不论初始状态和初始决策如何, 对于前面决策所造成的某一状态而言, 其后各阶段的决策序列必须构成最优策略". 判断该问题是否满足最优性原理, 采用反证法证明:
假设 (X1,X2,...,Xn) 是 01 背包问题的最优解, 则有 (X2,X3,...,Xn) 是其子问题的最优解,
假设 (Y2,Y3,...,Yn) 是上述问题的子问题最优解, 则理应有(V2Y2+V3Y3+...+VnYn)+V1X1> (V2X2+V3X3+...+VnXn)+V1X1;
而(V2X2+V3X3+...+VnXn)+V1X1=(V1X1+V2X2+...+VnXn), 则有(V2Y2+V3Y3+...+VnYn)+V1X1> (V1X1+V2X2+...+VnXn);
该式子说明 (X1,Y2,Y3,...,Yn) 才是该 01 背包问题的最优解, 这与最开始的假设 (X1,X2,...,Xn) 是 01 背包问题的最优解相矛盾, 故 01 背包问题满足最优性原理;
f) 寻找递推关系式, 面对当前商品有两种可能性:
第一, 包的容量比该商品体积小, 装不下, 此时的价值与前 i-1 个的价值是一样的, 即 V(i,j)=V(i-1,j);
第二, 还有足够的容量可以装该商品, 但装了也不一定达到当前最优价值, 所以在装与不装之间选择最优的一个, 即 V(i,j)=max{ V(i-1,j),V(i-1,j-w(i))+v(i) }
其中 V(i-1,j)表示不装, V(i-1,j-w(i))+v(i) 表示装了第 i 个商品, 背包容量减少 w(i)但价值增加了 v(i);
由此可以得出递推关系式:
- ) j<w(i)V(i,j)=V(i-1,j)
- ) j>=w(i)V(i,j)=max{
- V(i-1,j),V(i-1,j-w(i))+v(i)
- }
g) 填表, 首先初始化边界条件, V(0,j)=V(i,0)=0;
h) 然后一行一行的填表,
1) 如, i=1,j=1,w(1)=2,v(1)=3, 有 j<w(1), 故 V(1,1)=V(1-1,1)=0;
2) 又如 i=1,j=2,w(1)=2,v(1)=3, 有 j=w(1), 故 V(1,2)=max{ V(1-1,2),V(1-1,2-w(1))+v(1) }=max{0,0+3}=3;
3) 如此下去, 填到最后一个, i=4,j=8,w(4)=5,v(4)=6, 有 j>w(4), 故 V(4,8)=max{ V(4-1,8),V(4-1,8-w(4))+v(4) }=max{9,4+6}=10; 所以填完表如下图:
- void FindMax()// 动态规划
- {
- int i,j;
- // 填表
- for(i=1;i<=number;i++)
- {
- for(j=1;j<=capacity;j++)
- {
- if(j<w[i])// 包装不进
- {
- V[i][j]=V[i-1][j];
- }
- else// 能装
- {
- if(V[i-1][j]>V[i-1][j-w[i]]+v[i])// 不装价值大
- {
- V[i][j]=V[i-1][j];
- }
- else// 前 i-1 个物品的最优解与第 i 个物品的价值之和更大
- {
- V[i][j]=V[i-1][j-w[i]]+v[i];
- }
- }
- }
- }
- }
i) 表格填完, 最优解即是 V(number,capacity)=V(4,8)=10, 但还不知道解由哪些商品组成, 故要根据最优解回溯找出解的组成, 根据填表的原理可以有如下的寻解方式:
1) V(i,j)=V(i-1,j)时, 说明没有选择第 i 个商品, 则回到 V(i-1,j);
2) V(i,j)=V(i-1,j-w(i))+v(i)实时, 说明装了第 i 个商品, 该商品是最优解组成的一部分, 随后我们得回到装该商品之前, 即回到 V(i-1,j-w(i));
3) 一直遍历到 i=0 结束为止, 所有解的组成都会找到.
j) 如上例子,
1) 最优解为 V(4,8)=10, 而 V(4,8)!=V(3,8)却有 V(4,8)=V(3,8-w(4))+v(4)=V(3,3)+6=4+6=10, 所以第 4 件商品被选中, 并且回到 V(3,8-w(4))=V(3,3);
2) 有 V(3,3)=V(2,3)=4, 所以第 3 件商品没被选择, 回到 V(2,3);
3) 而 V(2,3)!=V(1,3)却有 V(2,3)=V(1,3-w(2))+v(2)=V(1,0)+4=0+4=4, 所以第 2 件商品被选中, 并且回到 V(1,3-w(2))=V(1,0);
4) 有 V(1,0)=V(0,0)=0, 所以第 1 件商品没被选择;
k) 到此, 01 背包问题已经解决, 利用动态规划解决此问题的效率即是填写此张表的效率, 所以动态规划的时间效率为 O(number*capacity)=O(n*c), 由于用到二维数组存储子问题的解, 所以动态规划的空间效率为 O(n*c);
- void FindWhat(int i,int j)// 寻找解的组成方式
- {
- if(i>=0)
- {
- if(V[i][j]==V[i-1][j])// 相等说明没装
- {
- item[i]=0;// 全局变量, 标记未被选中
- FindWhat(i-1,j);
- }
- else if( j-w[i]>=0 && V[i][j]==V[i-1][j-w[i]]+v[i] )
- {
- item[i]=1;// 标记已被选中
- FindWhat(i-1,j-w[i]);// 回到装包之前的位置
- }
- }
- }
3, 空间优化
l) 空间优化, 每一次 V(i)(j)改变的值只与 V(i-1)(x) {x:1...j}有关, V(i-1)(x)是前一次 i 循环保存下来的值;
因此, 可以将 V 缩减成一维数组, 从而达到优化空间的目的, 状态转移方程转换为 B(j)= max{B(j), B(j-w(i))+v(i)};
并且, 状态转移方程, 每一次推导 V(i)(j)是通过 V(i-1)(j-w(i))来推导的, 所以一维数组中 j 的扫描顺序应该从大到小(capacity 到 0), 否者前一次循环保存下来的值将会被修改, 从而造成错误.
m) 同样以上述例子中 i=3 时来说明, 有:
1) i=3,j=8,w(3)=4,v(3)=5, 有 j>w(3), 则 B(8)=max{B(8),B(8-w(3))+v(3)}=max{B(8),B(4)+5}=max{7,4+5}=9;
2) j- - 即 j=7, 有 j>w(3), 则 B(7)=max{B(7),B(7-w(3))+v(3)}=max{B(7),B(3)+5}=max{7,4+5}=9;
3) j- - 即 j=6, 有 j>w(3), 则 B(6)=max{B(6),B(6-w(3))+v(3)}=max{B(6),B(2)+5}=max{7,3+5}=8;
4) j- - 即 j=5, 有 j>w(3), 则 B(5)=max{B(5),B(5-w(3))+v(3)}=max{B(5),B(1)+5}=max{7,0+5}=7;
5) j- - 即 j=4, 有 j=w(3), 则 B(4)=max{B(4),B(4-w(3))+v(3)}=max{B(4),B(0)+5}=max{4,0+5}=5;
6) j- - 即 j=3, 有 j<w(3), 继续访问数组会出现越界, 所以本轮操作停止, B(0)到 B(3)的值保留上轮循环 (i=2 时) 的值不变, 进入下一轮循环 i++;
如果 j 不逆序而采用正序 j=0...capacity, 如上图所示, 当 j=8 时应该有 B(8)=B(8-w(3))+v(3)=B(4)+5, 然而此时的 B(4)已经在 j=4 的时候被修改过了, 原来的 B(4)=4, 现在 B(4)=5, 所以计算得出 B(8)=5+5=10, 显然这于正确答案不符合; 所以该一维数组后面的值需要前面的值进行运算再改动, 如果正序便利, 则前面的值将有可能被修改掉从而造成后面数据的错误; 相反如果逆序遍历, 先修改后面的数据再修改前面的数据, 此种情况就不会出错了;
- void FindMaxBetter()// 优化空间后的动态规划
- {
- int i,j;
- for(i=1;i<=number;i++)
- {
- for(j=capacity;j>=0;j--)
- {
- if(B[j]<=B[j-w[i]]+v[i] && j-w[i]>=0 )// 二维变一维
- {
- B[j]=B[j-w[i]]+v[i];
- }
- }
- }
- }
n) 然而不足的是, 虽然优化了动态规划的空间, 但是该方法不能找到最优解的解组成, 因为动态规划寻早解组成一定得在确定了最优解的前提下再往回找解的构成, 而优化后的动态规划只用了一维数组, 之前的数据已经被覆盖掉, 所以没办法寻找, 所以两种方法各有其优点.
四, 蛮力法检验:
1) 蛮力法是解决 01 背包问题最简单最容易的方法, 但是效率很低
2) (X1,X2,...,Xn)其中 Xi=0 或 1 表示第 i 件商品选或不选, 共有 n(n-1)/2 种可能;
3) 最简单的方式就是把所有拿商品的方式都列出来, 最后再做判断此方法是否满足装包条件, 并且通过比较和记录找出最优解和解组成(如果满足则记录此时的价值和装的方式, 当下一次的装法优于这次, 则更新记录, 如此下去到最后便会找到最优解, 同时解组成也找到);
4) n 件商品, 共有 n(n-1)/2 种可能, 故蛮力法的效率是指数级别的, 可见效率很低;
5) 蛮力法效率低不建议采取, 但可以用于检验小规模的动态规划解背包问题的正确性和可行性, 如下图输出可见, 解 01 背包问题用动态规划是可行的:
五, 总结:
对于 01 背包问题, 用蛮力法与用动态规划解决得到的最优解和解组成是一致的, 所以动态规划解决此类问题是可行的. 动态规划效率为线性, 蛮力法效率为指数型, 结合以上内容和理论知识可以得出, 解决此问题用动态规划比用蛮力法适合得多. 对于动态规划不足的是空间开销大, 数据的存储得用到二维数组; 好的是, 当前问题的解只与上一层的子问题的解相关, 所以, 可以把动态规划的空间进行优化, 使得空间效率从 O(n*c)转化为 O(c), 遗憾的是, 虽然优化了空间, 但优化后只能求出最优解, 解组成的探索方式在该方法运行的时候已经被破坏掉; 总之动态规划和优化后的动态规划各有优缺点, 可以根据实际问题的需求选择不同的方式.
六, 引申:
动态规划可以解决哪些类型的问题?
待解决的原问题较难, 但此问题可以被不断拆分成一个个小问题, 而小问题的解是非常容易获得的; 如果单单只是利用递归的方法来解决原问题, 那么采用的是分治法的思想, 动态规划具有记忆性, 将子问题的解都记录下来, 以免在递归的过程中重复计算, 从而减少了计算量.
来源: http://www.bubuko.com/infodetail-3153327.html