1. 题目
读题
考查点
2. 解法
思路
有两种解法
动态规划
双指针
代码逻辑
具体实现
动态规划
思路
动态规划的思路是这样的:
-
我们可以把问题分解成多个子问题,每个子问题都是在某一天结束时,完成了多少次交易,手上是否持有股票,以及此时的最大利润是多少。
-
我们可以用一个二维数组来表示这些子问题的状态,其中第一维表示天数,第二维表示交易次数和持有状态。例如,dp[i][j][0]表示在第i天结束时,完成了j次交易,手上没有股票,此时的最大利润是多少;dp[i][j][1]表示在第i天结束时,完成了j次交易,手上有股票,此时的最大利润是多少。
-
我们可以根据题目的限制,确定数组的大小和边界条件。由于我们最多可以完成两次交易,所以第二维的大小是3(0,1,2);由于持有状态只有两种(0,1),所以第三维的大小是2。边界条件是,在第0天结束时,如果没有股票,那么利润为0;如果有股票,那么利润为负无穷(因为还没有买入)。
-
我们可以根据状态转移方程,递推地计算出每个子问题的答案。状态转移方程是:
- dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]),表示在第i天结束时,如果没有股票,那么要么是前一天也没有股票,要么是前一天有股票但是今天卖出了,取两者中较大的利润。
- dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]),表示在第i天结束时,如果有股票,那么要么是前一天也有股票,要么是前一天没有股票但是今天买入了,取两者中较大的利润。
-
我们可以从数组的最后一个元素,找出最终的答案。由于我们想要的是最大利润,而且最后一天结束时不持有股票才能获得利润,所以我们只需要比较dp[n-1][0][0], dp[n-1][1][0], dp[n-1][2][0]三者中的最大值即可。
初始化 dp数线时为什么设置成 Integer.MIN_VALUE
这是因为我们想要表示一个不可能的状态,即在没有交易的情况下,却持有股票。这样的状态是不合理的,因为我们必须先买入股票才能持有股票,而买入股票就算一次交易。所以我们用一个很小的负数来表示这种状态,这样在比较的时候,它就不会被选中。Integer.MIN_VALUE是JAVA中可以表示的最小的整数,所以我们用它来初始化dp数组。
class Solution {
public int maxProfit(int[] prices) {
// 如果数组为空或只有一个元素,直接返回0
if (prices == null || prices.length < 2) {
return 0;
}
// 创建一个三维数组,表示每个子问题的状态
int[][][] dp = new int[prices.length][3][2];
// 初始化边界条件
dp[0][0][0] = 0; // 第0天结束时,没有交易,没有股票,利润为0
dp[0][0][1] = Integer.MIN_VALUE; // 第0天结束时,没有交易,有股票,利润为负无穷
dp[0][1][0] = 0; // 第0天结束时,交易了一次,没有股票,利润为0
dp[0][1][1] = -prices[0]; // 第0天结束时,交易了一次,有股票,利润为负的第一天价格
dp[0][2][0] = 0; // 第0天结束时,交易了两次,没有股票,利润为0
dp[0][2][1] = Integer.MIN_VALUE; // 第0天结束时,交易了两次,有股票,利润为负无穷
// 递推地计算每个子问题的答案
for (int i = 1; i < prices.length; i++) {
for (int j = 0; j <= 2; j++) {
// 如果没有股票,要么是前一天也没有股票,要么是前一天有股票但是今天卖出了
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]);
// 如果有股票,要么是前一天也有股票,要么是前一天没有股票但是今天买入了
// 注意j=0时不能买入股票,因为没有交易次数了
dp[i][j][1] = Math.max(dp[i-1][j][1], j > 0 ? dp[i-1][j-1][0] - prices[i] : Integer.MIN_VALUE);
}
}
// 返回最终的答案,即最后一天结束时不持有股票的最大利润
return Math.max(dp[prices.length - 1][0][0], Math.max(dp[prices.length - 1][1][0], dp[prices.length - 1][2][0]));
}
}
双指针算法的思路是这样的:
- 我们可以把问题分解成两个子问题,即在某个分割点之前和之后分别完成一次交易,然后找出最大的总利润。
- 我们可以用两个数组来表示这两个子问题的答案,其中一个数组从左到右遍历,记录每一天之前(包括当天)完成一次交易的最大利润;另一个数组从右到左遍历,记录每一天之后(包括当天)完成一次交易的最大利润。
- 我们可以用两个指针来更新这两个数组,一个指针从左到右,记录最小的买入价格和最大的卖出利润;另一个指针从右到左,记录最大的卖出价格和最大的买入利润。
- 我们可以遍历所有可能的分割点,即数组的每个下标,将对应的左右子数组的值相加,得到总利润,然后更新最大的总利润。
class Solution {
public int maxProfit(int[] prices) {
// 如果数组为空或只有一个元素,直接返回0
if (prices == null || prices.length < 2) {
return 0;
}
// 创建两个数组,分别存储从左到右和从右到左的最大利润
int[] left = new int[prices.length]; // left[i]表示在第i天之前(包括第i天)完成一次交易的最大利润
int[] right = new int[prices.length]; // right[i]表示在第i天之后(包括第i天)完成一次交易的最大利润
// 初始化左边数组的第一个元素和右边数组的最后一个元素为0
left[0] = 0;
right[prices.length - 1] = 0;
// 初始化左边数组的最小价格和右边数组的最大价格为第一个元素和最后一个元素
int minPrice = prices[0];
int maxPrice = prices[prices.length - 1];
// 从左到右遍历数组,更新左边数组和最小价格
for (int i = 1; i < prices.length; i++) {
minPrice = Math.min(minPrice, prices[i]); // 更新最小价格为当前价格和之前的最小价格中较小的一个
left[i] = Math.max(left[i - 1], prices[i] - minPrice); // 更新左边数组为当前价格减去最小价格和之前的最大利润中较大的一个
}
// 从右到左遍历数组,更新右边数组和最大价格
for (int i = prices.length - 2; i >= 0; i--) {
maxPrice = Math.max(maxPrice, prices[i]); // 更新最大价格为当前价格和之后的最大价格中较大的一个
right[i] = Math.max(right[i + 1], maxPrice - prices[i]); // 更新右边数组为最大价格减去当前价格和之后的最大利润中较大的一个
}
// 遍历所有可能的分割点,找出最大的总利润
int maxProfit = 0;
for (int i = 0; i < prices.length; i++) {
maxProfit = Math.max(maxProfit, left[i] + right[i]); // 更新最大利润为左边数组和右边数组之和和之前的最大利润中较大的一个
}
// 返回最终的结果
return maxProfit;
}
}