2.确定递推公式
定义出了dp数组之后, 按照前面几题的经验, 递推公式就没太大难度了, 还是每种状态都由两种情况组合而来, 最终取最大值:
根据上面我们定义的k何时变化, 体现在递推公式中就是在推导no[i][j]的第二种情况时用到的have[i-1][j-1], 因为卖出股票时k会变化, 所以上一个持有股票的状态就是k-1了. 这点一定要注意, 它不止在递推公式中很重要, 也在之后的空间优化时很重要!
3.dp数组初始化
本题的初始化过程还是有点复杂的, 但是并不难理解, 这里就不多解释了:
4.最终返回结果
由于在所有的 n 天结束后, 手上不持有股票对应的最大利润一定是严格大于手上持有股票对应的最大利润的, 然而完成的交易数并不是越多越好(例如数组prices 单调递减,我们不进行任何交易才是最优的), 因此最终的答案即为no[n−1][0…k]中的最大值.
5.几个重要的注意点
5.1 k的正序和倒序
本题为了避免状态压缩造成的数据覆盖, 应该采用k倒序才对. 它的解释如下: 最后一行计算noj时所用的have[j-1], 本应为have[i-1][j-1]。然而由于采用了状态压缩, i不变, j的前一个循环刚刚计算了have[i][j-1]赋值给了have[j-1], 所以have[j-1]实际为have[i][j-1], 与转移方程产生了偏差.
这是需要采用倒序的原因, 但是本题采用正序也是可以得到相同的结果的, 官方对此也进行了解释, 当然也有人专门写了文章进行推理说明, 导图中也放了相应的链接, 感兴趣的可以去看看, 过程还是挺复杂的.
但是从实际来说, 将k进行倒序, 在代码上是非常容易实现的操作, 只有在j的遍历范围上有一点改动, 其他是完全一样的, 所以我们还是尽可能用倒序遍历保证安全性, 毕竟本题没有影响, 不代表其他动态规划的题目也没有影响, 而且这么容易实现的操作, 根本没什么成本代价可言!
5.2 k的范围
如果交易次数大于n/2, 必然存在有一天交易了两次, 然而这是毫无意义的, 因为 n 天最多只能进行⌊n/2⌋ 笔交易, 其中⌊x⌋ 表示对x向下取整。因此我们可以将 k 对⌊n/2⌋ 取较小值之后再进行动态规划.
5.3 have数组的维数
k+1维是没问题的, 如果维数只有k, 就表示不了交易0次了, 要是数组一直是递减的, 一交易就是亏损.
5.4 Integer.MIN_VALUE / 2
在这是在java中可能遇到的问题, 因为在之后的操作中可能会减去prices[i], 这样就可能导致Integer.MIN_VALUE越界翻转, 所以这里进行了除以2的操作, 在python中是不用担心这个问题的.
5.5 j的循环
no[i][j]的状态转移方程中包含have[i−1][j−1], 在j=0 时其表示不合法的状态, 因此在 j=0 时, 我们无需对 no[i][j] 进行转移, 让其保持值为 0 即可, 所以在j=0时, 我们需要单独对have[i][0]进行处理.
源码
Python:
## 未进行空间优化
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
if not prices:
return 0
n = len(prices)
k = min(k, n // 2) # k最大为总天数的一半
have = [[0] \* (k + 1) for _ in range(n)]
no = [[0] \* (k + 1) for _ in range(n)]
have[0][0], no[0][0] = -prices[0], 0
for i in range(1, k + 1): # 不合法状态
have[0][i] = no[0][i] = float("-inf")
for i in range(1, n): # j=0时, no[i][0]不合法
have[i][0] = max(have[i - 1][0], no[i - 1][0] - prices[i])
for j in range(1, k + 1):
have[i][j] = max(have[i - 1][j], no[i - 1][j] - prices[i])
no[i][j] = max(no[i - 1][j], have[i - 1][j - 1] + prices[i]);
return max(no[n - 1])
## 进行空间优化
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
if not prices:
return 0
n = len(prices)
k = min(k, n // 2) # k最大为总天数的一半
have = [0] \* (k + 1)
no = [0] \* (k + 1)
have[0], no[0] = -prices[0], 0
for i in range(1, k + 1): # 不合法状态
have[i] = no[i] = float("-inf")
for i in range(1, n): # j=0时, no[0]不合法
have[0] = max(have[0], no[0] - prices[i])
for j in range(k, 0, -1): # 优化空间, k倒序
have[j] = max(have[j], no[j] - prices[i])
no[j] = max(no[j], have[j - 1] + prices[i]);
return max(no)
java:
// 未进行空间优化
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices.length == 0) {
return 0;
}
int n = prices.length;
k = Math.min(k, n / 2); // k最大为总天数的一半
int[][] have = new int[n][k + 1];
int[][] no = new int[n][k + 1];
have[0][0] = -prices[0];
no[0][0] = 0;
for (int i = 1; i <= k; ++i) { // 不合法状态,/2防止越界翻转
have[0][i] = no[0][i] = Integer.MIN_VALUE / 2;
}
for (int i = 1; i < n; ++i) { // j=0时, no[i][0]不合法
have[i][0] = Math.max(have[i - 1][0], no[i - 1][0] - prices[i]);
for (int j = 1; j <= k; ++j) {
have[i][j] = Math.max(have[i - 1][j], no[i - 1][j] - prices[i]);
no[i][j] = Math.max(no[i - 1][j], have[i - 1][j - 1] + prices[i]);
}
}
return Arrays.stream(no[n - 1]).max().getAsInt();
}
}
我的更多精彩文章链接, 欢迎查看
各种电脑/软件/生活/音乐/动漫/电影技巧汇总(你肯定能够找到你需要的使用技巧)
力扣算法刷题 根据思维导图整理笔记快速记忆算法重点内容(欢迎和博主一起打卡刷题哦)
计算机专业知识 思维导图整理
最值得收藏的 Python 全部知识点思维导图整理, 附带常用代码/方法/库/数据结构/常见错误/经典思想(持续更新中)
最值得收藏的 C++ 全部知识点思维导图整理(清华大学郑莉版), 东南大学软件工程初试906科目
最值得收藏的 计算机网络 全部知识点思维导图整理(王道考研), 附带经典5层结构中英对照和框架简介
最值得收藏的 算法分析与设计 全部知识点思维导图整理(北大慕课课程)
最值得收藏的 数据结构 全部知识点思维导图整理(王道考研), 附带经典题型整理
最值得收藏的 人工智能导论 全部知识点思维导图整理(王万良慕课课程)
最值得收藏的 数值分析 全部知识点思维导图整理(东北大学慕课课程)
最值得收藏的 数字图像处理 全部知识点思维导图整理(武汉大学慕课课程)
红黑树 一张导图解决红黑树全部插入和删除问题 包含详细操作原理 情况对比
各种常见排序算法的时间/空间复杂度 是否稳定 算法选取的情况 改进 思维导图整理
人工智能课件 算法分析课件 Python课件 数值分析课件 机器学习课件 图像处理课件
考研相关科目 知识点 思维导图整理
考研经验–东南大学软件学院软件工程(这些基础课和专业课的各种坑和复习技巧你应该知道)
东南大学 软件工程 906 数据结构 C++ 历年真题 思维导图整理
最值得收藏的 考研高等数学 全部知识点思维导图整理(张宇, 汤家凤), 附做题技巧/易错点/知识点整理
最值得收藏的 考研线性代数 全部知识点思维导图整理(张宇, 汤家凤), 附带惯用思维/做题技巧/易错点整理
考研思修 知识点 做题技巧 同类比较 重要会议 1800易错题 思维导图整理
考研近代史 知识点 做题技巧 同类比较 重要会议 1800易错题 思维导图整理
考研马原 知识点 做题技巧 同类比较 重要会议 1800易错题 思维导图整理
考研数学课程笔记 考研英语课程笔记 考研英语单词词根词缀记忆 考研政治课程笔记
Python相关技术 知识点 思维导图整理