菜鸡acmer理解 【动态规划】01背包问题

下定决心,这次总结完一定要深刻理解,不能再学了忘忘了学了。。。。。。
01背包问题描述
有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?

大体思路如下:
根据动态规划解题的步骤:问题抽象 -> 建立模型 ->寻找约束条件 ->判断是否满足最优性原理 -> 找大问题与小问题的递推关系式(最重要) -> 填表(模拟试一下)-> 寻找解的组成 ,从而找到01背包问题的最优解以及解得组成。

动态规划的原理:
动态规划根据泰泰学长的讲述,即为优雅的暴力,所谓优雅的暴力在我的理解便是将暴力进行优雅化(这不是废话么)。动态规划是具有记忆性的暴力(个人理解),将大问题分为子问题分为子子问题分为子子子问题……一直分到最终我们可以一步解决的子子子子子问题,在解决这些问题的时候因为要调用之前的子问题,我们通常的暴力都是一遍一遍再算一遍,而dp算法则是存储记忆,遇到子问题的时候可以直接提取,避免了重复计算。

背包问题的解决具体思路
首先为了方便描述,定义变量:Vi表示第 i 个物品的价值,Wi表示第 i 个物品的体积,定义V(i,j):当前背包容量 j,前 i 个物品最佳组合对应的价值,同时背包问题抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 个物品选或不选)。
通过这样的变量我们很容易得到这样的思路:
1. 建立模型 我们所要求的便是 max(v1x1+v2x2+……+vn*xn)
2. 寻找约束条件 w1x1+w2x2+…+wnxn< j
3. 寻找递推关系式子: 当我们面对第 i 件物品的时候 我们有两个选择
(1) 当前背包容量 j<wi ,则我们无法装下 所以此时的价值与前i-1个价值是一样的 即v(i,j) = v(i-1,j)
(2) 当前背包容量j可以装下当前的第i个物品,但是我们要选择装还是不装,这样就用到了动态规划转移方程: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)}
而为什么能装得下要这么写这个物品的时候,方程长这个样子(不长这样子还长啥样子)
我们再次理解一下v(i,j)数组,它的意思是我们用 j个空间装下了前面i个物品,也就是我们还还剩下的背包容量应该是 capacity - j,那我们为什么不用剩下的背包容量去判断我们应不应该装第i个物品呢? 因为这涉及到一个思维的正序和逆序,在动态规划中,你想这个问题的时候是倒着想的,也就是用剩下的背包容量去判断第i个物品是否应该装进去,而写代码的时候我们用的是正序思维,也就是背包容量是固定的(当前容量固定),我们把物品装一个,再装一个,再装一个…
第一种是第i件商品没有装进去,第二种是第i件商品装进去了。没有装进去很好理解,就是V(i-1,j);装进去了怎么理解呢?如果装进去第i件商品,那么装入之前是什么状态,肯定是V(i-1,j-w(i))。由于最优性原理(上文讲到),V(i-1,j-w(i))就是前面决策造成的一种状态,后面的决策就要构成最优策略。两种情况进行比较,得出最优。
4.填表,首先初始化边界条件,V(0,j) = V(i,0)=0;

在这里插入图片描述
然后一行一行的填表:

如,i=1,j=1,w(1)=2,v(1)=3,有j<w(1),故V(1,1)=V(1-1,1)=0;
又如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;
如此下去,填到最后一个,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……
在这里插入图片描述
5.填完表我们就可以看出来 最优的解是v(number,capacity)= v(4,8)=10
代码:

#include<iostream>
using namespace std;
#include <algorithm> 
int main()
{
	int w[5] = { 0 , 2 , 3 , 4 , 5 };			//商品的体积2、3、4、5
	int v[5] = { 0 , 3 , 4 , 5 , 6 };			//商品的价值3、4、5、6
	int bagV = 8;					        //背包大小
	int dp[5][9] = { { 0 } };			        //动态规划表
	for (int i = 1; i <= 4; i++)
	 {
		for (int j = 1; j <= bagV; j++)    //这个j表示的就是 我们用j个背包容量去装前i个物品
		 {
			if (j < w[i])
				dp[i][j] = dp[i - 1][j];
			else
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
		}
	}
	//动态规划表的输出,最终答案应该为最后一行最后一列的答案
	for (int i = 0; i < 5; i++) {
		for (int j = 0; j < 9; j++) {
			cout << dp[i][j] << ' ';
		}
		cout << endl;
	}
	return 0;
}


再加一个zls说的滚动数组 一维01背包代码
第二层循环的遍历就是为了满足上述的第二个条件。 我们先设 j1 > j2 ,更新dp[ j1 ] 肯定在 更新dp[ j2 ] 的前面,就算 j1- w[i] = j2 , 也不会受影响。 故在 第 i 层的循环下,可以保证各个dp[ j ] 是由 前 i -1 层 dp 推出来的,而不会受本次循环其他dp更新的影响。

                  for(i=1;i<=n;i++)
                        for(j=t;j>=w[i];j--)
                             dp[j]=max(dp[j],dp[j-w[i]]+v[i]);

这里补充一个背包问题最优解回溯问题:
这个就是求出最优解的大小之后,输出背包中的每一个物品,即用到了回溯,回溯方式很简单:
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结束为止,所有解的组成都会找到。

void findWhat(int i, int j) {                //最优解情况
    if (i >= 0) {
        if (dp[i][j] == dp[i - 1][j])   //如果相等代表我没有拿第i个物品  则向前回溯
         {
            item[i] = 0;
            findWhat(i - 1, j);
        }
        else if (j - w[i] >= 0 && dp[i][j] == dp[i - 1][j - w[i]] + v[i])  //如果不相等 且满足转移方程的逆序,则向前回溯的同时 将背包容量减少(其实还是可以理解为装回去背包中)
         {
            item[i] = 1;
            findWhat(i - 1, j - w[i]);
        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值