打家劫舍是一道经典的dp入门题,具体思路可以参考笔者上一篇。我们首先明确这道题的原问题和子问题,显然,原问题就是对于 n 个房屋,我们偷窃能够获得最大收益是多少;子问题就是对于前 i 间房屋,我们能获得的最大收益是多少。那么,这个问题的状态(自变量)就是房屋的数量。
确定了问题的dp数组含义以及状态,我们就可以来分析如何构建状态转移方程了。
首先,我们对于dp问题要明确一点,思考方式往往是自底向上思考的,所以我们就先从状态转移方程的边界情况进行考虑,因为边界情况往往是问题的最简单的情况。假设只有一间房屋,我们就没有选择,只能偷这间房屋;假设有两间房屋,根据题目要求,不能偷两间相邻的房屋,我们就得在两间房屋中偷金额最大的一间。
因此我们可以给出状态转移方程的第一部分
if (i == 0) {
dp[i] = nums[i];
} else if (i == 1) {
dp[i] = max(nums[i - 1], nums[i]);
}
假设有超过两间房屋,我们就从最后一间房屋开始考虑,如果说我们可以偷最后一间房屋,那么倒数第二间房屋肯定是不能被偷窃的,因此我们能获得的最大收益就是偷取前 n - 2 间房屋能获得的最大收益 + 偷窃最后一间房屋获得的收益;如果说我们不能偷最后一间房屋,那么倒数第二间房屋肯定是被偷窃了,因此我们能获得的最大收益就是我们偷窃前 n - 1 间房屋能获得的最大收益。
由此,我们可以确定状态转移方程的第二部分
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
我们将dp数组的最后一个元素输出即可。参考代码如下
class Solution {
public:
int rob(vector<int>& nums) {
int dp[105] = {0}; //dp数组表示偷前i间房子能获得的最大价值
for (int i = 0; i < nums.size(); i++) { //遍历填充dp数组
if (i == 0) {
dp[i] = nums[i];
} else if (i == 1) {
dp[i] = max(nums[i - 1], nums[i]);
} else {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
}
return dp[nums.size() - 1];
}
};
题目来源:LCR 166. 珠宝的最高价值 - 力扣(LeetCode)
打家劫舍问题可以通过一维dp数组来解决,那么珠宝的最高价值问题就可以通过二维dp数组解决。
同样的,我们首先分析dp数组的含义。根据题意,每个珠宝都对应着一个坐标(i, j),我们要求的是从架子左上角一直拿到架子右下角能拿到的最高价值,所以我们定义dp数组的含义为:dp[i][j]表示从左上角开始拿,一直拿到坐标为(i, j)的位置能拿到的最大价值。
确定了dp数组的含义,我们就可以来思考如何确定状态转移方程了。首先确定方程的边界情况,对于整个架子,可以看成一个二维数组。一般来说,除了位于边界上的点,我们要到达该二维数组上的某一点,只能够从该点的左边或者该点的上方到达,那么我们的边界情况就可以分为两种情况:在上边界与在左边界。在上边界的点只能够由该点的左边到达,在左边界上的点只能够由该点的上方到达,因此我们就确定了该dp数组的边界条件。
if (i == 0 && j == 0) {
dp[i][j] = frame[i][j];
} else if (i == 0) {
dp[i][j] = dp[i][j - 1] + frame[i][j];
} else if (j == 0) {
dp[i][j] = dp[i - 1][j] + frame[i][j];
}
对于一般的点,既可以从该点的左边到达也可以从该点的上方到达,至于如何选择,取决于从左边到达能拿到的价值多还是从上方到达能拿到的价值多,因此可以得到方程的一般情况。
dp[i][j] = max(dp[i - 1][j] + frame[i][j], dp[i][j - 1] + frame[i][j]);
最后输出dp数组右下角的元素即可。参考代码如下
class Solution {
public:
int jewelleryValue(vector<vector<int>>& frame) {
int dp[205][205]; //dp数组含义为坐标为(i, j)的货架能拿到的最大价值
for (int i = 0; i < frame.size(); i++) {
for (int j = 0; j < frame[i].size(); j++) {
if (i == 0 && j == 0) {
dp[i][j] = frame[i][j];
} else if (i == 0) {
dp[i][j] = dp[i][j - 1] + frame[i][j];
} else if (j == 0) {
dp[i][j] = dp[i - 1][j] + frame[i][j];
} else {
dp[i][j] = max(dp[i - 1][j] + frame[i][j], dp[i][j - 1] + frame[i][j]);
}
}
}
return dp[frame.size() - 1][frame[frame.size() - 1].size() - 1];
}
};
题目来源:洛谷P1048
这道题是典型的01背包问题,给定背包容量T与金币堆数M,求背包能装的最大价值。首先明确解决01背包问题用贪心算法是不行的,需要采用动态规划算法。
首先,用weight[i]表示物品重量,value[i]表示物品价值,然后我们来定义dp数组的含义:dp[i][j]表示以容量为 j 的背包放入前 i 个物品的最大价值。
明确了dp数组的含义,接下来来确定状态转移方程。
与笔者上一篇文章的例题类似,我们需要考虑当前背包容量与当前遍历到的物品重量的大小关系,倘若背包容量比当前物品的重量小,那么我们就只能在该物品的前方所有物品中选取最大价值的物品,即dp[i][j] = dp[i - 1][j];倘若背包容量充足,对于每个物品就有装这个物品与不装这个物品两种情况,我们要在这两种情况中找到最大价值的情况,因此就有:dp[i][j] = max(dp[i - 1][j - weight[i]] + value[i], dp[i - 1][j]);其中,dp[i - 1][j - weight[i]] + value[i]代表装入该物品,dp[i - 1][j]代表不装入该物品,对两种情况取最大值即可,最终输出结果。参考代码如下:
#include <iostream>
#include <math.h>
using namespace std;
int T, M;
int weight[105], value[105]; //weight表示草药重量,value表示草药价值
int dp[105][1005]; //dp[i][j]表示用j个容量取前i个草药的最大价值
int main() {
cin >> T >> M; //T表示采药的总时间,M表示草药的数目
for (int i = 1; i <= M; i++) {
cin >> weight[i] >> value[i];
}
for (int i = 1; i <= M; i++) {
for (int j = T; j >= 0; j--) {
if (j >= weight[i]) {
dp[i][j] = max(dp[i - 1][j - weight[i]] + value[i], dp[i - 1][j]);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
cout << dp[M][T] << endl;
return 0;
}