0-1背包的二维和一维dp数组
有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
背包问题,最难的就是初始化和遍历顺序!
二维dp数组01背包
// 0-1背包问题 // 二维dp数组 // n个物品 背包容量为m int knapSack(int n, int m, vector<int>& weight, vector<int>& value) { vector<vector<int> dp(n+1, vector<int>(m+1, 0)); // dp[i][j]:从前i个物品中选择放入容量为j的背包中得到的最大价值 // 注意这种定义,第i件物品的重量为weight[i-1],价值为value[i-1] // dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]] + value[i-1]) // 初始化 // 当j=0时,背包容量为0,最大价值为0;当i=0时,也就是前0件物品,也就是没有物品,最大价值也是0 for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (j - weight[i-1] < 0) // 如果当前背包容量放不下第i件物品,那么前i件物品放入背包得到的最大价值就是前i-1件物品放入获得的最大价值 dp[i][j] = dp[i-1][j]; else { // 如果能放下,从放和不放两种选择里取最大值 dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]] + value[i-1]); } } } return dp[n][m]; }
为什么两个for循环的嵌套顺序这么写?反过来行不行?
1.先遍历物品和先遍历背包容量其实都可以,但是先遍历物品更好理解。
2.反过来也是可以的,这跟递归的本质和递推的方向有关系。dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i-1]] + value[i-1]);递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i-1]]推导出来的,他们俩都在dp[i][j]的左上角方向(包括正左和正上两个方向),分析先遍历物品和先遍历背包的过程,在计算dp[i][j]之前都已经得到了,不影响公式的推导!
初始化的逻辑
根据dp数组的定义,dp[i][j]:从前i个物品中选择放入容量为j的背包中得到的最大价值,那么从前0个物品中选择放入,也就是没有物品,最大价值是0;背包容量为0的时候,最大价值为0,所以初始化为0就可以了。但是要注意数组的初始化,因为价值是求最大的,如果所有物品的价值都是大于0的,那数组初始化为0没问题,如果价值有负数,必须初始化为负无穷才行。
一维dp数组01背包
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i-1]] + value[i-1],可以看出dp[i][j]这一层的推导只跟上一层dp[i-1][]有关系,所以其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i-1]] + value[i-1]);于其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j]。
dp[j]:容量为j的背包能装的最大价值
dp[j]可以通过dp[j - weight[i-1]]推导出来,dp[j - weight[i-1]]表示容量为j - weight[i-1]的背包所背的最大价值。
dp[j - weight[i-1]] + value[i-1] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])
此时dp[j]有两个选择,一个是取自己dp[j],一个是取dp[j - weight[i-1]] + value[i-1],指定是取最大的,毕竟是求最大价值。
所以,递推公式为:dp[j] = max(dp[j], dp[j - weight[i-1]] + value[i-1]);
初始化时候跟定义吻合:容量为0的时候,能装的最大价值dp[0] = 0,其他位置的初始化也就是数组的初始化,跟二维一样的考虑,跟价值正负有关。
// 0-1背包问题 // 一维dp数组 // n个物品 背包容量为m int knapSack(int n, int m, vector<int>& weight, vector<int>& value) { vector<int> dp(m+1, 0); // 当j=0时,背包容量为0,最大价值为0 for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { if (j - weight[i-1] >= 0) { dp[j] = max(dp[j], dp[j-weight[i-1]] + value[i-1]); } } } return dp[m]; }
或者可以优化为(主要是容量遍历条件优化):
// 0-1背包问题 // 一维dp数组 // n个物品 背包容量为m int knapSack(int n, int m, vector<int>& weight, vector<int>& value) { vector<int> dp(m+1, 0); // 当j=0时,背包容量为0,最大价值为0 for (int i = 1; i <= n; i++) { for (int j = m; j >= weight[i-1]; j--) { dp[j] = max(dp[j], dp[j-weight[i-1]] + value[i-1]); } } return dp[m]; }
需要注意一维dp数组的遍历顺序和二维dp数组的遍历顺序是不一样的!
二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
倒叙遍历是为了保证物品i只被放入一次!
从后往前循环,每次取的状态不会和之前取的状态重合,这样每种物品就只取一次了。
二维dp数组不用倒叙的原因是:dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖。
一维数组的0-1背包,两个for循环的顺序反过来写行不行?为什么?
不可以!因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。
所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!