动态规划问题解读与实现
这道题目属于动态规划中经典的股票买卖问题变种,涉及状态定义、状态转移方程和约束条件。通过分步理解并实现,可以帮助巩固对动态规划问题的掌握。
1. 问题解析
问题要求: 给定股票价格数组 stocks,需要计算在特定交易规则下,能够获得的最大利润:
- 可以多次买卖股票,但每次新的买入前必须卖出之前的股票。
- 每次卖出后有一天的冷冻期,在冷冻期内不能买入。
示例分析:
-
示例 1
:
stocks = [1, 2]- 第 1 天买入,第二天卖出,利润为 2−1=12 - 1 = 12−1=1。
-
示例 3
:
stocks = [1, 2, 3, 0, 2]- 第 1 天买入,第 2 天卖出;第 4 天买入,第 5 天卖出。利润为 2−1+2−0=32 - 1 + 2 - 0 = 32−1+2−0=3。
-
示例 4
:
stocks = [2, 3, 4, 5, 6, 7]- 从第 1 天买入到第 6 天卖出,利润为 7−2=57 - 2 = 57−2=5。
2. 思路分析
-
动态规划状态定义:
-
使用三个数组表示不同的状态:
hold[i]:第 iii 天结束时,手上持有股票的最大利润。sold[i]:第 iii 天结束时,刚刚卖出股票的最大利润。rest[i]:第 iii 天结束时,处于冷冻期或无操作的最大利润。
-
-
状态转移方程:
ℎ𝑜𝑙𝑑[𝑖]=𝑚𝑎𝑥(ℎ𝑜𝑙𝑑[𝑖−1],𝑟𝑒𝑠𝑡[𝑖−1]−𝑠𝑡𝑜𝑐𝑘𝑠[𝑖])- 表示要么保持持有状态,要么在冷冻期后买入。
-
𝑠𝑜𝑙𝑑[𝑖]=ℎ𝑜𝑙𝑑[𝑖−1]+𝑠𝑡𝑜𝑐𝑘𝑠[𝑖] - 表示当天卖出股票,收益等于前一天持有股票的利润加上当天价格。
-
𝑟𝑒𝑠𝑡[𝑖]=𝑚𝑎𝑥(𝑟𝑒𝑠𝑡[𝑖−1],𝑠𝑜𝑙𝑑[𝑖−1]) - 表示当天不操作,处于冷冻期或继续保持。
-
初始条件:
-
第 0 天:
- hold[0]=−stocks[0]:只能买入股票。
- sold[0]=0:不可能卖出股票。
- rest[0]=0:初始利润为 0。
-
-
目标值:
- 最终的最大利润为: max(sold[n−1],rest[n−1])因为结束时不可能持有股票。
-
时间复杂度与空间优化:
- 时间复杂度:O(n)O(n)O(n)。
- 空间复杂度:可以优化为 O(1)O(1)O(1),通过滚动变量替代数组。
3. 完整代码
public class Main {
public static int solution(int[] stocks) {
// 若无股票价格,利润为 0
if (stocks == null || stocks.length == 0) {
return 0;
}
int n = stocks.length;
// 动态规划数组
int[] hold = new int[n]; // 持有股票
int[] sold = new int[n]; // 刚卖出股票
int[] rest = new int[n]; // 休息或冷冻期
// 初始化
hold[0] = -stocks[0];
sold[0] = 0;
rest[0] = 0;
// 动态规划计算
for (int i = 1; i < n; i++) {
hold[i] = Math.max(hold[i - 1], rest[i - 1] - stocks[i]); // 保持持有或买入
sold[i] = hold[i - 1] + stocks[i]; // 当天卖出
rest[i] = Math.max(rest[i - 1], sold[i - 1]); // 冷冻期或休息
}
// 返回最终最大利润
return Math.max(sold[n - 1], rest[n - 1]);
}
public static void main(String[] args) {
// 测试样例
System.out.println(solution(new int[]{1, 2}) == 1); // 输出:1
System.out.println(solution(new int[]{2, 1}) == 0); // 输出:0
System.out.println(solution(new int[]{1, 2, 3, 0, 2}) == 3); // 输出:3
System.out.println(solution(new int[]{2, 3, 4, 5, 6, 7}) == 5); // 输出:5
System.out.println(solution(new int[]{1, 6, 2, 7, 13, 2, 8}) == 12); // 输出:12
}
}
4. 空间优化版本
将数组替换为滚动变量,仅需三个变量保存当前状态即可。
public class Main {
public static int solution(int[] stocks) {
if (stocks == null || stocks.length == 0) {
return 0;
}
// 初始化状态变量
int hold = -stocks[0]; // 持有股票
int sold = 0; // 刚卖出股票
int rest = 0; // 冷冻期或休息
for (int i = 1; i < stocks.length; i++) {
int prevHold = hold;
int prevSold = sold;
hold = Math.max(hold, rest - stocks[i]); // 保持持有或买入
sold = prevHold + stocks[i]; // 卖出
rest = Math.max(rest, prevSold); // 冷冻期或休息
}
// 返回最终最大利润
return Math.max(sold, rest);
}
public static void main(String[] args) {
// 测试样例
System.out.println(solution(new int[]{1, 2}) == 1); // 输出:1
System.out.println(solution(new int[]{2, 1}) == 0); // 输出:0
System.out.println(solution(new int[]{1, 2, 3, 0, 2}) == 3); // 输出:3
System.out.println(solution(new int[]{2, 3, 4, 5, 6, 7}) == 5); // 输出:5
System.out.println(solution(new int[]{1, 6, 2, 7, 13, 2, 8}) == 12); // 输出:12
}
}
5. 关键总结
-
动态规划的核心:
- 明确问题状态和状态转移方程。
- 使用辅助变量记录约束条件(如冷冻期)。
-
空间优化技巧:
- 滚动数组可以显著减少空间占用,但需谨慎避免覆盖状态。
-
测试用例覆盖:
- 边界条件(如空数组)。
- 单一价格、多次涨跌等复杂情形。
6.学习方法与建议
在解决算法问题时,掌握一套系统的学习方法和实践技巧能够显著提升学习效率。以下是针对股票交易问题总结的学习方法与建议:
-
理解问题背景和约束
- 在解决问题前,仔细阅读题目,明确约束条件是解题的关键。例如,本题中的冷冻期限制直接影响交易策略,需要在动态规划转移方程中体现。
- 多用示例帮助自己理清逻辑,确保在思路上没有遗漏。
-
熟悉常见算法思想
- 动态规划(DP) 是许多优化问题的核心工具,理解其状态定义、转移方程和边界条件的设计至关重要。
- 针对 DP 问题,养成提炼“状态”和“转移关系”的习惯。本题通过设计三种状态(
hold、sold和rest)清晰地模拟了交易过程。
-
画图或列表辅助理解
- 动态规划问题通常需要多个状态的递推,因此将状态的演变过程画图或用表格列出有助于直观理解。
- 比如,本题可以列出每天的
hold、sold和rest值,逐步推导出状态变化的规律。
-
编写测试用例验证代码
- 在完成代码后,为算法设计多样化的测试用例,包括:
- **简单测试**:如只有一天或两天数据的情况。
- **边界测试**:如空数组或极值数组(价格不变)。
- **复杂测试**:如频繁波动的股票价格。
- 通过测试用例确保代码的正确性和鲁棒性。
-
总结思路并多加练习
- 每次解决完问题后,尝试总结解决的思路和经验,并记录在学习笔记中。
- 对于动态规划类问题,多练习类似的题目(如股票系列问题),培养对 DP 状态转移的敏感度。
-
与他人交流和讨论
- 学习过程中,和同学、朋友或在线社区交流能够帮助发现自己的不足之处,也能学习到新的解题技巧。
- 在线平台(如 LeetCode 或力扣)上可以参考其他人的优秀解答,并尝试优化自己的代码。
-
保持耐心,逐步提升
- 动态规划问题通常难度较高,初学时可能会感到吃力,但只要保持耐心和练习,随着经验积累,思考和实现效率会显著提高。
- 遇到困难时,可以参考别人的代码,但务必自己多思考,弄清楚每一步的推导过程。