动态规划刷题也有一部分了,来总结一下刷到的几大类问题和比较有意思的问题。
1. 打家劫舍系列
这个系列的问题其实相对来说是比较好解决的,只有【337. 打家劫舍 III】是相对来说比较难的树形动态规划。
1.1. 打家劫舍
这道题其实搞清楚了思路,实现起来很简单。
dp[i]
的含义:只在 [0, i] 范围的房子里偷钱,能够偷取的最大值。- 递推公式:
dp[i] = max(dp[i - 1], dp[i - 2] + money[i])
- 初始化方式:
dp[p] = money[0]
(只偷第0间房子的话肯定最大值就是房子里的钱);dp[1] = max(money[0], money[1])
(只偷前两间房子的话挑大的偷) - 迭代顺序:正序迭代
这样子写代码就思路清晰很多了:
#include <iostream>
#include <vector>
#include <sstream>
using namespace std;
int main()
{
int n;
while (cin >> n) {
// 处理输入
vector<int> money(n, 0);
for (int i = 0; i < n; i++) {
cin >> money[i];
}
// 动态规划
if (money.size() == 1) { // 别忘了这个逻辑
cout << money[money.size() - 1];
}
else {
vector<int> dp(n, 0);
dp[0] = money[0];
dp[1] = max(money[0], money[1]);
for (int i = 2; i < n; i++) {
dp[i] = max(dp[i - 2] + money[i], dp[i - 1]);
}
cout << dp[dp.size() - 1] << endl;
}
}
}
1.2. 打家劫舍 II
跟前一题不一样的是,这道题的房子在一个环路上。其实思路是:分别去掉开头的房子和结尾的房子,把这俩数组送入第一题中的逻辑,分别返回两个结果,然后取最大值就可以了。
#include <iostream>
#include <vector>
#include <sstream>
using namespace std;
int maxMoney(vector<int> value) {
if (value.size() == 1)
return value[value.size() - 1];
vector<int> dp(value.size(), 0);
dp[0] = value[0];
dp[1] = max(value[0], value[1]);
for (int i = 2; i < value.size(); i++) {
dp[i] = max(dp[i - 1], dp[i - 2] + value[i]);
}
return dp[value.size() - 1];
}
int main()
{
int n;
while (cin >> n) {
// 处理输入
vector<int> money(n, 0);
for (int i = 0; i < n; i++) {
cin >> money[i];
}
// 动态规划
if (money.size() == 1) { // 别忘了这个逻辑
cout << money[money.size() - 1];
}
else {
int former = maxMoney(vector<int>(money.begin(), money.end() - 1));
int latter = maxMoney(vector<int>(money.begin() + 1, money.end()));
cout << max(former, latter) << endl;
}
}
}
1.3. 打家劫舍 III
题目链接:打家劫舍III
这道题是这个系列里面最难的一道,也是这类树形动态规划的入门题。
思路:
- 用后序遍历(为什么用后序遍历呢?因为根节点的最大值是由子节点的结果求得的,所以是一个自底向上的遍历顺序,用后序遍历)去遍历这个二叉树;
- 递归的过程中,我去返回这个节点 【偷】 和 【不偷】 所能获取的最大价值。
所以接下来:
- 确定递归函数参数列表和返回值
vector<int> robTree(TreeNode* cur);
- 确定递归终止条件
if (!cur) {
return vector<int> { 0, 0 }; // 因为对于一个空节点,无论偷不偷,收益都是0
}
- 确定单层处理逻辑
int moneyStole = cur->val + leftDP[0] + rightDP[0]; // 偷当前节点:左叶子和右叶子都不能偷了
int moneyNotStole = max(leftDP[0], leftDP[1]) + max(rightDP[0], rightDP[1]); // 不偷当前节点:左(右)叶子节点偷不偷都行,那么偷还是不偷由收益大小决定。
return vector<int> { moneyNotStole, moneyStole };
整合起来就是:
vector<int> robTree(TreeNode* cur) {
if (!cur) {
return vector<int> { 0, 0 }; // 因为对于一个空节点,无论偷不偷,收益都是0
}
vector<int> leftDP = robTree(cur->left);
vector<int> rightDP = robTree(cur->right);
int moneyStole = cur->val + leftDP[0] + rightDP[0]; // 偷当前节点:左叶子和右叶子都不能偷了
int moneyNotStole = max(leftDP[0], leftDP[1]) + max(rightDP[0], rightDP[1]); // 不偷当前节点:左(右)叶子节点偷不偷都行,那么偷还是不偷由收益大小决定。
return vector<int> { moneyNotStole, moneyStole };
}
int rob(TreeNode* root) {
vector<int> res = robTree(root);
return max(res[0], res[1]);
}
2. 买卖股票系列
这个系列也是前两道题很简单,第三道题开始上压力。
2.1. 买卖股票的最佳时机
你只能在某一天买入,之后的某一天卖出。这道题会有一点难想,但是写过之后就很清晰了。这道题有一个很基本的用贪心的方法来做:
#include <iostream>
#include <vector>
#include <sstream>
using namespace std;
int main()
{
int n;
while (cin >> n) {
// 输入
vector<int> nums(n, 0);
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
// 处理
int cost = INT_MAX, profit = 0;
for (int i : nums) {
cost = min(cost, i);
profit = max(profit, i - cost);
}
cout << profit << endl;
}
}
另外,这道题也可以使用动态规划的方法来做:
- 动态规划数组的含义:
dp[i][0]
表示第i天持有这支股票所可能产生的最大收益,dp[i][1]
表示第i天不持有这支股票所可能产生的最大收益。 - 递推公式:
dp[i][0] = max(dp[i - 1][0], -nums[i])
要么状态继承自前一天持有,要么在第i天买入,取最大;dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + nums[i])
要么状态继承自前一天不持有,要么在第i天卖出,取最大。 - 迭代顺序:正序
- 初始化:
dp[0][0] = -nums[i], dp[0][1] = 0
#include <iostream>
#include <vector>
#include <sstream>
using namespace std;
int main()
{
int n;
while (cin >> n) {
// 输入
vector<int> nums(n, 0);
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
// 动态规划数组
vector<vector<int>> dp(n, vector<int>(2, 0));
// 初始化
dp[0][0] = -nums[0];
dp[0][1] = 0;
for (int i = 1; i < n; i++) {
dp[i][0] = max(dp[i - 1][0], - nums[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + nums[i]);
}
cout << max(dp[n - 1][0], dp[n - 1][1]) << endl;
}
}
2.2. 买卖股票的最佳时机 II
每一天都可以买入卖出,这道题就别用动态规划了,老实用贪心吧,还好记:
#include <iostream>
#include <vector>
#include <sstream>
using namespace std;
int main()
{
int n;
while (cin >> n) {
// 输入
vector<int> nums(n, 0);
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
// 贪心
int sum = 0;
for (int i = 1; i < n; i++)
{
if (nums[i] - nums[i - 1] > 0) {
sum += nums[i] - nums[i - 1];
}
}
cout << sum << endl;
}
}
2.3. 买卖股票的最佳时机III
这道题其实思路也不是很难,跟 2.1 的思路是类似的,但实际情况多了很多,因为是买卖两次,所以有5种状态。
- 动态规划数组含义:
dp[i][0]
:一次操作都没进行dp[i][1]
:只进行了一次买入dp[i][2]
:进行了一次买入和卖出dp[i][3]
:进行了一次买卖的基础上,又进行了一次买入dp[i][4]
:完成了两次买卖
- 递推公式:
dp[i][0] = dp[i - 1][0]
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - nums[i])
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + nums[i])
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - nums[i])
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + nums[i])
(其实这里不难看出,思路其实很简单的)
- 迭代顺序:正序
- 初始化:
dp[0][0] = 0
dp[0][1] = -nums[0]
dp[0][2] = 0
dp[0][3] = -nums[0]
dp[0][4] = 0
梳理完了,根据以上的规则,代码其实实现起来并不复杂:
#include <iostream>
#include <vector>
#include <sstream>
using namespace std;
int main()
{
int n;
while (cin >> n) {
// 输入
vector<int> nums(n, 0);
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
// 动态规划
vector<vector<int>> dp(n, vector<int>(5, 0));
// 初始化
dp[0][1] = -nums[0]; dp[0][3] = -nums[0];
// 迭代
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0];
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - nums[i]);
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + nums[i]);
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - nums[i]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + nums[i]);
}
// 输出
cout << dp[n - 1][4] << endl;
}
}