背包问题是动态规划问题中选或者不选的典型代表, 从 0x3f 博主视频中,可以学习到用记忆化搜索的方式思考dp问题,这样可以让我们在写递推的时候,比较容易地想到状态数组的定义。
本文将依照 记忆化搜索 -> dp数组 -> dp数组优化 的方式一步步讲解背包问题。
1. 01背包问题
问题描述:有 件物品和一个容量是 的背包。每件物品只有一个。第 件物品的体积是 ,价值是 。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
首先我们想怎么样写出一个暴力搜索,可以覆盖全部的情况然后记录最大价值呢?于是开始构造搜索函数,可以发现对于第 件物品,我们要么选择它要么不选择它,选取这二者的最大值,根据这两种情况得到搜索的方向,可以得到搜索函数:
代表第 个物品, 代表当前背包的体积, 代表从前 个物品中选,选出体积为 的物品的最大价值。
可以知道搜索函数的代码:
public int dfs(int i, int j) { if(i < 0) { return 0; } int ans = dfs(i - 1, j); if(j >= v[i]) { ans = Math.max(ans, dfs(i - 1, j - v[i]) + w[i]); } return ans;}在搜索的过程中会有一些节点被重复的搜索,导致算法的时间性能下降,为了改进这一点,我们需要对搜索进行记忆化,对于 01背包 这个问题,我们可以开一个二维数组用来进行记忆化每一个计算过的节点,记忆化搜索的代码如下:
public int dfs(int i, int j) { if(i < 0) { return 0; } if(memo[i][j] != -1) { return memo[i][j]; } int ans = dfs(i - 1, j); if(j >= v[i]) { ans = Math.max(ans, dfs(i - 1, j - v[i]) + w[i]); } return memo[i][j] = ans;}下面让我们把这个代码改成递推的,其实一旦搜索的代码写出来了,动态规划的状态转移方程就出来了。
设置 dp[i][j] 表示为从前 个物品中选,选出体积为 的物品的最大值,状态转移方程为:
可以得到递推的代码如下:
int[][] dp = new int[n + 1][volume + 1];
for(int i = 0; i < n; i ++) { for(int j = 0; j <= volume; j ++) { dp[i + 1][j] = dp[i][j]; if(j >= v[i]) { dp[i + 1][j] = Math.max(dp[i + 1][j], dp[i][j - v[i]] + w[i]); } }}这里我们对递推的代码分析一下,可以知道对于 dp[i + 1] 层的数据的更新只需要用到 dp[i] 层的数据,所以空间上还可以优化。利用滚动数组的思想,我们不需要二维数组,只需要一维就够了。
在每次更新的时候,我们只需要保证“用来更新的数据是上一层的数据”就可以了,可以发现如果改成一维数组之后仍然使用体积从小到大进行遍历的话,就会导致更新时用到了当前层刚更新的数据,导致更新错误,所以我们要按照体积从大到小进行遍历
可以得到优化空间后的代码如下:
int[] dp = new int[volume + 1];
for(int i = 0; i < n; i ++) { for(int j = volume; j >= v[i]; j --) { dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]); }}2. 完全背包问题
问题描述:有 件物品和一个容量是 的背包。每件物品有无穷多个。第 件物品的体积是 ,价值是 。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
和01背包一样,我们也可以想一个覆盖全部情况的一个暴力搜索