121.买卖股票的最佳时机
动态规划五部曲!
1、确定 dp 数组下标及值的含义
先想想本题 dp 应该怎么定义,别忘了之前说的,dp 数组的下标能够表示状态
在股票问题中,某个状态需要描述在某天,及是否持有股票
因此我们定义 dp 数组下标及值含义:
dp[i][0]:下标表示在第 i 天,未持有股票,值表示第 i 天未持有股票所得最多现金
dp[i][1]:下标表示在第 i 天,持有股票,值表示第 i 天持有股票所得最多现金
通过第一个下标 i 描述在哪一天,第二个下标为 0 或 1 描述在该天未持有或持有股票,这样通过两个下标就能够描述所有的状态了
2、确定递推公式
分别思考 dp[i][0] 和 dp[i][1] 分别应该怎么推
dp[i][0]:需要求第 i 天未持有股票所得最多现金,要考虑怎么转移到第 i 天未持有股票的这种状态
- 可能是在第 i 天卖出了股票,所得现金就是昨天持有股票,然后今天卖出股票后所得现金,即:dp[i - 1][1](昨天持有股票)+ prices[i](今天卖出股票)
- 可能是在第 i - 1 天就未持有股票,那么就保持现状,所得现金就是昨天未持有股票的所得现金,即:dp[i - 1][0](昨天未持有股票)
因为求的最多现金,即 dp[i][0] = max(dp[i - 1][1] + prices[i], dp[i - 1][0])
同理,dp[i][1]:
- 可能是在第 i 天买入股票,所得现金为 -prices[i](注意本题要求股票全程只能买一次,所以如果买入股票,买之前的现金一定为 0,买之后的现金一定为 -prices[i])
- 可能是在第 i - 1 天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金,即:dp[i - 1][1](昨天持有股票)
因为求的最多现金,即 dp[i][1] = max(-prices[i], dp[i - 1][1])
3、dp 数组初始化
从递推公式看出,求某一天 dp 需要知道其前一天的 dp 值,即都是要从dp[0][0]和dp[0][1]推导出来
那么dp[0][0]表示第 0 天未持有股票,不持有股票现金就是 0,所以 dp[0][0] = 0
dp[0][1]表示第 0 天持有股票,持有股票一定是在第 0 天买入了股票,所以 dp[0][1] = -prices[0]
4、确定遍历顺序
从递推公式可以看出 dp[i] 都是由 dp[i - 1] 推导出来的,那么一定是从前向后遍历
5、打印 dp 数组验证
代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() == 0 || prices.size() == 1) return 0;
// 定义dp数组下标及值含义
vector<vector<int> > dp(prices.size(), vector<int>(2));
// 确定递推公式:dp[i][0]和dp[i][1]分别确定
// dp数组初始化,仅初始化第一天即可
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 从前往后遍历,一行一行填充
for (int i = 1; i < prices.size(); ++i) {
dp[i][0] = max(dp[i - 1][1] + prices[i], dp[i - 1][0]);
dp[i][1] = max(-prices[i], dp[i - 1][1]);
}
// 最后一定卖了才能取得最大
return dp[prices.size() - 1][0];
}
};
从递推公式可以看出,dp[i] 只是依赖于 dp[i - 1] 的状态
dp[i][0] = max(dp[i - 1][1] + prices[i], dp[i - 1][0]);
dp[i][1] = max(-prices[i], dp[i - 1][1]);
可以使用滚动数组来节省空间,仅记录一天的 dp
如图所示,左边为原 dp 数组,右边是滚动数组。相当于就地更新原 dp 数组的每一行
注意滚动数组中更新 dp 时的遍历顺序,需要从左向右遍历,这样更新 dp[0] 的时候才保证等式右边的 dp 值能对应到原二维数组中 i - 1 层的值
代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() == 0 || prices.size() == 1) return 0;
// 滚动数组,仅记录原二维dp的一行
int dp[2];
// 初始化原二维dp数组的第一行
dp[0] = 0;
dp[1] = -prices[0];
// 一行一行遍历填充原二维数组
for (int i = 1; i < prices.size(); ++i) { // 遍历天数
dp[0] = max(dp[1] + prices[i], dp[0]);
dp[1] = max(-prices[i], dp[1]);
} // 遍历填充完毕后,滚动数组记录的是原二维dp的最后一行的数据
return dp[0];
}
};
122.买卖股票的最佳时机II
动态规划五部曲!
1、确定 dp 数组下标及值的含义
先想想本题 dp 应该怎么定义,别忘了之前说的,dp 数组的下标能够表示状态
在股票问题中,某个状态需要描述在某天,及是否持有股票
因此我们定义 dp 数组下标及值含义:
dp[i][0]:下标表示在第 i 天,未持有股票,值表示第 i 天未持有股票所得最多现金
dp[i][1]:下标表示在第 i 天,持有股票,值表示第 i 天持有股票所得最多现金
通过第一个下标 i 描述在哪一天,第二个下标为 0 或 1 描述在该天未持有或持有股票,这样通过两个下标就能够描述所有的状态了
2、确定递推公式
分别思考 dp[i][0] 和 dp[i][1] 分别应该怎么推
dp[i][0]:需要求第 i 天未持有股票所得最多现金,要考虑怎么转移到第 i 天未持有股票的这种状态
- 可能是在第 i 天卖出了股票,所得现金就是昨天持有股票,然后今天卖出股票后所得现金,即:dp[i - 1][1](昨天持有股票)+ prices[i](今天卖出股票)
- 可能是在第 i - 1 天就未持有股票,那么就保持现状,所得现金就是昨天未持有股票的所得现金,即:dp[i - 1][0](昨天未持有股票)
因为求的最多现金,即 dp[i][0] = max(dp[i - 1][1] + prices[i], dp[i - 1][0])
同理,dp[i][1]:
- 可能是在第 i 天买入股票,所得现金为 dp[i - 1][0](昨天未持有股票)- prices[i](今天买入股票)(因为一只股票可以买卖多次,所以当第 i 天买入股票的时候,所持有的现金需要考虑之前买卖过的利润)
- 可能是在第 i - 1 天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金,即:dp[i - 1][1](昨天持有股票)
因为求的最多现金,即 dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1])
3、dp 数组初始化
从递推公式看出,求某一天 dp 需要知道其前一天的 dp 值,即都是要从dp[0][0]和dp[0][1]推导出来
那么dp[0][0]表示第 0 天未持有股票,不持有股票现金就是 0,所以 dp[0][0] = 0
dp[0][1]表示第 0 天持有股票,持有股票一定是在第 0 天买入了股票,所以 dp[0][1] = -prices[0]
4、确定遍历顺序
从递推公式可以看出 dp[i] 都是由 dp[i - 1] 推导出来的,那么一定是从前向后遍历
5、打印 dp 数组验证
代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() == 0 || prices.size() == 1) return 0;
// 定义dp数组下标及值含义
vector<vector<int> > dp(prices.size(), vector<int>(2));
// 确定递推公式:dp[i][0]和dp[i][1]分别确定
// dp数组初始化,仅初始化第一天即可
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 从前往后遍历,一行一行填充
for (int i = 1; i < prices.size(); ++i) {
dp[i][0] = max(dp[i - 1][1] + prices[i], dp[i - 1][0]);
dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);
}
// 最后一定卖了才能取得最大
return dp[prices.size() - 1][0];
}
};
同样,能够通过滚动数组优化
为了更新 dp 时能够正确用到原二维 dp 中上一层的 dp 值,需要用到 temp 数组记录上一层的 dp 值
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() == 0 || prices.size() == 1) return 0;
// 滚动数组仅需维护原dp数组一行(一天)的数据
int dp[2];
// dp数组初始化,仅初始化原dp数组第一行(第一天)即可
dp[0] = 0;
dp[1] = -prices[0];
// 用于记录上一行(前一天)的数据
int temp[2];
// 一行一行遍历填充
for (int i = 1; i < prices.size(); ++i) {
// 储存二维dp上一行的数据
temp[0] = dp[0];
temp[1] = dp[1];
// 更新二维dp本行的数据
dp[0] = max(temp[1] + prices[i], temp[0]);
dp[1] = max(temp[0] - prices[i], temp[1]);
}
// 最后一定卖了才能取得最大
return dp[0];
}
};
回顾总结
关键是需要明确 dp 数组的含义:用第二维区分持有股票和不持有股票的状态