文章目录
121.买卖股票的最佳时机(只能买卖一次)
思路一:贪心
贪心策略:我们要得到最大利润,最好就是价格最低买入,价格最高卖出。具体的方法是在遍历prices
数组时维护价格的最小值Min
,当访问到prices[i]
,假设此时卖出,那么所能得到的最大利润为prices[i]-Min
,对于i=0~pricesNums,取最大值。
代码一
int maxProfit(int* prices, int pricesSize) {
int Min = prices[0]; // 初始化最低股价为第一天价格
int ans = 0; // 初始化最大利润为0
for (int i = 1; i < pricesSize; i++) { // 从第二天开始遍历
// 计算当前卖出能获得的利润,并与历史最大值比较
ans = fmax(prices[i] - Min, ans);
// 更新最低股价,确保Min始终是已遍历天数中的最小值
Min = fmin(prices[i], Min);
}
return ans; // 返回最大利润
}
思路二:dp
1.定义状态
dp[i][0]
:表示第i
天 持有股票 时的最大现金。dp[i][1]
:表示第i
天 不持有股票 时的最大现金。
2.初始化
dp[0][0] = -prices[0]
:第 0 天持有股票,现金为负的股票价格(表示买入股票)。dp[0][1] = 0
:第 0 天不持有股票,现金为 0。
3.状态转移方程
-
持有股票:
- 如果第
i
天持有股票,可能是前一天已经持有股票,或者第i
天买入股票,即前i-1天买入或者第i天买入两种情况。
d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , − p r i c e s [ i ] ) dp[i][0]=max(dp[i−1][0],−prices[i]) dp[i][0]=max(dp[i−1][0],−prices[i])
- 如果第
-
不持有股票:
- 如果第
i
天不持有股票,可能是前一天已经不持有股票,或者第i
天卖出股票,即前i-1天卖出或者第i天才卖出两种情况。
d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] ) dp[i][1]=max(dp[i−1][1],dp[i−1][0]+prices[i]) dp[i][1]=max(dp[i−1][1],dp[i−1][0]+prices[i])
- 如果第
4.最终结果
- 最终结果为
dp[pricesSize-1][1]
,即最后一天不持有股票时的最大现金,因为不持有股票手中的现金肯定更大。
代码二
#include <stdio.h>
#include <limits.h> // 用于 INT_MIN
int maxProfit(int* prices, int pricesSize) {
if (pricesSize == 0) return 0; // 空数组直接返回0
// 定义动态规划数组
int dp[pricesSize][2];
// 初始化
dp[0][0] = -prices[0]; // 第0天持有股票
dp[0][1] = 0; // 第0天不持有股票
// 动态规划递推
for (int i = 1; i < pricesSize; i++) {
// 第i天持有股票:前一天已经持有,或者第i天买入
dp[i][0] = fmax(dp[i - 1][0], -prices[i]);
// 第i天不持有股票:前一天已经不持有,或者第i天卖出
dp[i][1] = fmax(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
// 返回最后一天不持有股票的最大现金
return dp[pricesSize - 1][1];
}
122.买股票的最佳时机Ⅱ(可以买卖多次)
思路一:贪心
本题的贪心思路在前面的博客已经介绍了,可以点击链接查看。
思路二:dp
1.dp数组含义
定义一个二维数组 dp[i][j]
表示第 i 天结束时的最大利润,其中:
dp[i][0]
表示第 i 天结束时持有股票时的最大利润。dp[i][1]
表示第 i 天结束时不持有股票时的最大利润。
2.初始状态
- 第 0 天(i = 0):
- 如果在第 0 天买入股票,那么
dp[0][0] = -prices[0]
,因为需要花费prices[0]
元。 - 如果第 0 天不进行交易,则
dp[0][1] = 0
。
- 如果在第 0 天买入股票,那么
3.状态转移方程
对于第 i 天(i ≥ 1),我们有两种状态的转移:
-
持有股票状态(
dp[i][0]
):- 情况 1:保持昨天已经持有股票的状态,即
dp[i-1][0]
。 - 情况 2:昨天没有股票,但今天买入了股票,因此利润为
dp[i-1][1] - prices[i]
。
所以,转移方程为:
d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] ) dp[i][0]=max(dp[i−1][0], dp[i−1][1]−prices[i]) dp[i][0]=max(dp[i−1][0],dp[i−1][1]−prices[i])与上一题中的
dp[i][0]=max(dp[i−1][0],−prices[i])
不同 - 情况 1:保持昨天已经持有股票的状态,即
-
不持有股票状态(
dp[i][1]
):- 情况 1:保持昨天不持有股票的状态,即
dp[i-1][1]
。 - 情况 2:昨天持有股票,今天卖出,因此利润为
dp[i-1][0] + prices[i]
。
所以,转移方程为:
d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] ) dp[i][1]=max(dp[i−1][1], dp[i−1][0]+prices[i]) dp[i][1]=max(dp[i−1][1],dp[i−1][0]+prices[i]) - 情况 1:保持昨天不持有股票的状态,即
4.遍历与最终结果
我们从第 1 天开始,按照上面的状态转移方程逐步更新 dp[i][0]
和 dp[i][1]
。最后一天(pricesSize - 1
)结束时,答案为 dp[pricesSize-1][1]
,因为不持有股票的状态才是真正的“平仓”,实现了利润。
代码
int maxProfit(int* prices, int pricesSize) {
int dp[pricesSize][2];
// 初始化:第0天的状态
dp[0][0] = -prices[0]; // 买入股票
dp[0][1] = 0; // 不买股票
// 从第1天开始遍历
for (int i = 1; i < pricesSize; i++) {
// 如果持有股票:要么继续持有,要么今天买入(必须之前没有股票)
dp[i][0] = fmax(dp[i-1][0], dp[i-1][1] - prices[i]);
// 如果不持有股票:要么继续不持有,要么今天卖出(之前持有股票)
dp[i][1] = fmax(dp[i-1][0] + prices[i], dp[i-1][1]);
}
return dp[pricesSize-1][1];
}
123.买卖股票的最佳时机Ⅲ(至多买卖两次)
问题描述
给定一个数组 prices
,其中 prices[i]
表示第 i
天的股票价格。允许最多进行两笔交易(一次买入和卖出为一次交易),求出能获得的最大利润。
注意:同一时间只能持有一只股票,即在买入前必须完成前一次卖出。
思路:dp
1.dp数组含义
定义一个二维数组 dp[i][j]
,其中 i
表示第 i
天,而 j
则表示当天的状态。
状态的含义如下:
dp[i][0]
:不操作
表示第i
天未进行任何交易操作,该状态其实作为一个初始状态使用,初始值为0
。dp[i][1]
:第一次持有股票
表示在第i
天经过第一次买入后持有股票的最大利润。
初始状态为:dp[0][1] = -prices[0]
(买入股票花费prices[0]
)。dp[i][2]
:第一次不持有股票(完成第一次卖出)
表示在第i
天完成第一次交易(卖出后)获得的最大利润。
初始状态为:dp[0][2] = 0
。dp[i][3]
:第二次持有股票
表示在完成第一次交易之后,第二次买入后持有股票的状态。
初始状态为:dp[0][3] = -prices[0]
(虽然一般来说第二次买入应该依赖于第一次卖出的收益,这里用-prices[0]
进行初始化,后续会不断更新)。dp[i][4]
:第二次不持有股票(完成第二次卖出)
表示在第i
天完成第二笔交易后的最大利润。
初始状态为:dp[0][4] = 0
。
2.状态转移公式
不操作状态:
d
p
[
i
]
[
0
]
=
d
p
[
i
−
1
]
[
0
]
dp[i][0]=dp[i−1][0]
dp[i][0]=dp[i−1][0]
保持上一天的状态不变。
第一次持有状态:
当天可以选择买入股票,也可以保持之前的状态。
-
如果今天买入股票,则利润为前一天的“不操作状态”减去当前价格:
dp[i−1][0]−prices[i]
-
或者保持昨天已经买入的状态:
dp[i−1][1]
故:
d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] , d p [ i − 1 ] [ 1 ] ) dp[i][1]=max(dp[i−1][0]−prices[i], dp[i−1][1]) dp[i][1]=max(dp[i−1][0]−prices[i],dp[i−1][1])
第一次卖出状态(第一次不持有):
当天可以选择卖出第一次买入的股票,或者保持之前的状态。
- 卖出后利润为:
dp[i−1][1]+prices[i]
- 或者保持之前卖出后的状态:
dp[i−1][2]
故:
d p [ i ] [ 2 ] = m a x ( d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] , d p [ i − 1 ] [ 2 ] ) dp[i][2]=max(dp[i−1][1]+prices[i], dp[i−1][2]) dp[i][2]=max(dp[i−1][1]+prices[i],dp[i−1][2])
第二次持有状态:
第二次买入股票可在第一次卖出后进行。
- 如果今天买入股票,则利润为第一次卖出后的利润减去当前价格:
dp[i−1][2]−prices[i]
- 或者保持之前的第二次买入状态:
dp[i−1][3]
故:
d p [ i ] [ 3 ] = m a x ( d p [ i − 1 ] [ 2 ] − p r i c e s [ i ] , d p [ i − 1 ] [ 3 ] ) dp[i][3]=max(dp[i−1][2]−prices[i], dp[i−1][3]) dp[i][3]=max(dp[i−1][2]−prices[i],dp[i−1][3])
第二次卖出状态(第二次不持有):
当天可以选择卖出第二次买入的股票,或者保持之前的状态。
- 卖出后利润为:
dp[i−1][3]+prices[i]
- 或者保持之前的状态:
dp[i−1][4]
故:
d p [ i ] [ 4 ] = m a x ( d p [ i − 1 ] [ 3 ] + p r i c e s [ i ] , d p [ i − 1 ] [ 4 ] ) dp[i][4]=max(dp[i−1][3]+prices[i], dp[i−1][4]) dp[i][4]=max(dp[i−1][3]+prices[i],dp[i−1][4])
3.最终结果
在最后一天,可能完成了一笔交易(状态 dp[n-1][2]
)或者完成了两笔交易(状态 dp[n-1][4]
)。因此最终答案为这两者中的最大值:
r
e
s
u
l
t
=
m
a
x
(
d
p
[
p
r
i
c
e
s
S
i
z
e
−
1
]
[
2
]
,
d
p
[
p
r
i
c
e
s
S
i
z
e
−
1
]
[
4
]
)
result=max(dp[pricesSize−1][2], dp[pricesSize−1][4])
result=max(dp[pricesSize−1][2],dp[pricesSize−1][4])
代码
int maxProfit(int* prices, int pricesSize) {
// 定义二维数组 dp,用于保存每一天的五种状态的最大利润
int dp[pricesSize][5];
// 初始化第 0 天的状态
dp[0][0] = 0; // 状态 0:不操作,利润为 0
dp[0][1] = -prices[0]; // 状态 1:第一次买入,花费 prices[0]
dp[0][2] = 0; // 状态 2:第一次卖出尚未发生,利润为 0
dp[0][3] = -prices[0]; // 状态 3:第二次买入的初始值(后续更新会修正)
dp[0][4] = 0; // 状态 4:第二次卖出尚未发生,利润为 0
// 从第 1 天开始,依次更新每一天的状态
for (int i = 1; i < pricesSize; i++) {
// 状态 0:不操作状态保持不变
dp[i][0] = dp[i-1][0];
// 状态 1:第一次买入
// 可能选择在当天买入,或者保持昨天买入的状态:
// - 买入:使用不操作状态 dp[i-1][0] 并减去今天的价格 prices[i]
// - 保持:延续昨天的第一次买入状态 dp[i-1][1]
dp[i][1] = fmax(dp[i-1][0] - prices[i], dp[i-1][1]);
// 状态 2:第一次卖出
// 可能选择在当天卖出,或者保持昨天卖出的状态:
// - 卖出:将昨天的第一次买入状态 dp[i-1][1] 加上今天的价格 prices[i]
// - 保持:延续昨天的第一次卖出状态 dp[i-1][2]
dp[i][2] = fmax(dp[i-1][1] + prices[i], dp[i-1][2]);
// 状态 3:第二次买入
// 可能选择在当天进行第二次买入,或者保持昨天的第二次买入状态:
// - 买入:使用昨天的第一次卖出状态 dp[i-1][2] 减去今天的价格 prices[i]
// - 保持:延续昨天的第二次买入状态 dp[i-1][3]
dp[i][3] = fmax(dp[i-1][2] - prices[i], dp[i-1][3]);
// 状态 4:第二次卖出
// 可能选择在当天卖出,或者保持昨天的第二次卖出状态:
// - 卖出:将昨天的第二次买入状态 dp[i-1][3] 加上今天的价格 prices[i]
// - 保持:延续昨天的第二次卖出状态 dp[i-1][4]
dp[i][4] = fmax(dp[i-1][3] + prices[i], dp[i-1][4]);
}
// 返回最后一天完成一次交易或两次交易中获得的最大利润
return fmax(dp[pricesSize-1][2], dp[pricesSize-1][4]);
}
188.买卖股票的最佳时机Ⅳ(至多买卖k次)
思路:dp
上一题(123. 买卖股票的最佳时机 III)是本题k=2的特殊情况,如何上题理解的化,那么本题可以将2扩展至k。具体看代码。
代码
#include <stdio.h>
#include <limits.h>
#include <math.h>
int maxProfit(int k, int* prices, int pricesSize) {
// 创建二维数组 dp[pricesSize][2*k+1]
// dp[i][j] 表示第 i 天处于状态 j 时的最大利润
int dp[pricesSize][2 * k + 1];
// 初始化第 0 天的状态
dp[0][0] = 0; // 状态 0:不操作,利润为 0
// 初始化状态 1 ~ 2*k
for (int j = 1; j <= 2 * k; j++) {
// 如果 j 为奇数,表示买入状态:初始利润为 -prices[0]
// 如果 j 为偶数,表示卖出状态:初始利润为 0
if (j & 1)
dp[0][j] = -prices[0];
else
dp[0][j] = 0;
}
// 从第 1 天开始,更新每一天的状态
for (int i = 1; i < pricesSize; i++) {
// 状态 0:不操作,直接继承前一天的值
dp[i][0] = dp[i - 1][0];
// 对于状态 1 到 2*k
for (int j = 1; j <= 2 * k; j++) {
// 如果 j 为奇数,表示买入状态
if (j & 1)
// 选择:
// 1. 今天买入:前一天处于 j-1 状态(卖出状态)的利润减去今天价格
// 2. 或者保持昨天买入的状态(不操作)
dp[i][j] = fmax(dp[i - 1][j - 1] - prices[i], dp[i - 1][j]);
else
// 如果 j 为偶数,表示卖出状态
// 选择:
// 1. 今天卖出:前一天处于 j-1 状态(买入状态)的利润加上今天价格
// 2. 或者保持昨天的卖出状态(不操作)
dp[i][j] = fmax(dp[i - 1][j - 1] + prices[i], dp[i - 1][j]);
}
}
// 答案在最后一天的各个卖出状态中取最大值
// 卖出状态对应偶数下标:2, 4, ..., 2*k
int ans = 0;
for (int i = 1; i <= k; i++) {
ans = fmax(ans, dp[pricesSize - 1][2 * i]);
}
return ans;
}
309.买卖股票的最佳时机含冷冻期
题目描述
给定一个数组 prices
,其中 prices[i]
表示第 i
天的股票价格。
设计一个算法,计算在存在“冷冻期”约束下的最大利润。
“冷冻期”约束说明:在卖出股票后的第二天处于冷冻状态,在冷冻状态下不能买入股票。
思路:dp
1.dp数组含义
用二维dp数组表示状态,对于dp[i][j]
,
i
表示第 i 天(从 0 开始计数)。j
表示当天的状态,共有 4 种状态,每个状态的含义如下:dp[i][0]
—— 持有股票
表示第 i 天结束时持有股票的最大利润(买入后持有,未卖出)。dp[i][1]
—— 当天卖出股票
表示第 i 天卖出股票后得到的最大利润,即当天刚卖出股票。dp[i][2]
—— 自由状态(未持有股票且非冻结)
表示第 i 天未持有股票且没有受到冷冻期限制的状态,处于可以买入的自由状态。dp[i][3]
—— 冻结状态
表示第 i 天处于冻结状态,这通常是由于昨天卖出了股票,所以今天不能立即买入股票。
2.状态转移的解释
对于每天的状态更新,我们根据前一天的状态进行转移:
1.状态 0:持有股票
第 i 天若要保持或进入持有股票状态,可以有以下三种情况:
- 继续持有:如果昨天已经持有股票,则利润为
dp[i-1][0]
。 - 从自由状态买入:如果昨天处于自由状态(
dp[i-1][2]
),则今天买入股票后利润为dp[i-1][2] - prices[i]
。 - 从冻结状态买入:如果昨天处于冻结状态(
dp[i-1][3]
),今天买入股票后利润为dp[i-1][3] - prices[i]
。
因此,状态转移公式为:
d
p
[
i
]
[
0
]
=
m
a
x
(
d
p
[
i
−
1
]
[
0
]
,
d
p
[
i
−
1
]
[
2
]
−
p
r
i
c
e
s
[
i
]
,
d
p
[
i
−
1
]
[
3
]
−
p
r
i
c
e
s
[
i
]
)
dp[i][0]=max(dp[i−1][0], dp[i−1][2]−prices[i], dp[i−1][3]−prices[i])
dp[i][0]=max(dp[i−1][0], dp[i−1][2]−prices[i], dp[i−1][3]−prices[i])
2.状态 1:当天卖出股票
要在第 i 天卖出股票,必须前一天处于持有状态(dp[i-1][0]
),当天卖出获得收益:
d
p
[
i
]
[
1
]
=
d
p
[
i
−
1
]
[
0
]
+
p
r
i
c
e
s
[
i
]
dp[i][1]=dp[i−1][0]+prices[i]
dp[i][1]=dp[i−1][0]+prices[i]
3.状态 2:自由状态(未持有且非冻结)
第 i 天处于自由状态有两种来源:
- 保持自由:如果昨天已经处于自由状态,则利润为
dp[i-1][2]
。 - 冻结结束:如果昨天处于冻结状态,今天冷冻期结束,变为自由状态,则利润为
dp[i-1][3]
。
因此,状态转移公式为:
d
p
[
i
]
[
2
]
=
m
a
x
(
d
p
[
i
−
1
]
[
2
]
,
d
p
[
i
−
1
]
[
3
]
)
dp[i][2]=max(dp[i−1][2], dp[i−1][3])
dp[i][2]=max(dp[i−1][2], dp[i−1][3])
4.状态 3:冻结状态
冻结状态通常由昨天刚卖出股票转移而来:
d
p
[
i
]
[
3
]
=
d
p
[
i
−
1
]
[
1
]
dp[i][3]=dp[i−1][1]
dp[i][3]=dp[i−1][1]
代码
#include <limits.h>
#include <math.h>
/*
* 题目:带冷冻期的股票交易问题
*
* dp 数组含义:
* dp[i][0]:第 i 天持有股票(买入后未卖出)的最大利润
* dp[i][1]:第 i 天卖出股票(当天刚卖出)的最大利润
* dp[i][2]:第 i 天未持有股票且处于自由状态(无冷冻限制)的最大利润
* dp[i][3]:第 i 天处于冻结状态(昨天卖出,今天冻结)的最大利润
*
* 状态转移:
* 1. dp[i][0] = max(继续持有股票, 自由状态买入, 冻结状态买入)
* = max(dp[i-1][0], dp[i-1][2] - prices[i], dp[i-1][3] - prices[i])
*
* 2. dp[i][1] = dp[i-1][0] + prices[i]
*
* 3. dp[i][2] = max(保持自由状态, 冻结结束转为自由状态)
* = max(dp[i-1][2], dp[i-1][3])
*
* 4. dp[i][3] = dp[i-1][1]
*
* 最终答案:
* 返回最后一天未持有股票的状态中的最大值:
* max(dp[n-1][1], dp[n-1][2], dp[n-1][3])
*/
int maxProfit(int* prices, int pricesSize) {
// 定义 dp 数组,行数为天数,列数为 4 个状态
int dp[pricesSize][4];
// 初始化第 0 天的状态
dp[0][0] = -prices[0]; // 第 0 天买入股票,持有股票
dp[0][1] = 0; // 第 0 天不可能卖出股票
dp[0][2] = 0; // 第 0 天未持有股票且非冻结状态
dp[0][3] = 0; // 第 0 天没有冻结状态
// 从第 1 天开始,根据前一天的状态更新当天状态
for (int i = 1; i < pricesSize; i++) {
// 状态 0:持有股票
dp[i][0] = fmax(fmax(dp[i-1][0], dp[i-1][2] - prices[i]), dp[i-1][3] - prices[i]);
// 状态 1:当天卖出股票(只能从前一天持有股票卖出)
dp[i][1] = dp[i-1][0] + prices[i];
// 状态 2:自由状态(未持有且非冻结),可由前一天自由或冻结状态转移
dp[i][2] = fmax(dp[i-1][2], dp[i-1][3]);
// 状态 3:冻结状态,由前一天卖出股票转移而来
dp[i][3] = dp[i-1][1];
}
// 最终答案:最后一天未持有股票的最大利润(可能是卖出、自由或冻结状态)
return fmax(fmax(dp[pricesSize-1][1], dp[pricesSize-1][2]), dp[pricesSize-1][3]);
}
714.买股票的最佳时机含手续费
思路:dp
与122. 买卖股票的最佳时机 II的思路类似,不同点是本题在卖出股票的时候要减去手续费,其他都一样,即
d
p
[
i
]
[
1
]
=
m
a
x
(
d
p
[
i
−
1
]
[
1
]
,
d
p
[
i
−
1
]
[
0
]
+
p
r
i
c
e
s
[
i
]
−
f
e
e
)
dp[i][1]=max(dp[i−1][1], dp[i−1][0]+prices[i]-fee)
dp[i][1]=max(dp[i−1][1],dp[i−1][0]+prices[i]−fee)
代码
int maxProfit(int* prices, int pricesSize, int fee) {
// 定义 dp 数组,dp[i][0] 表示第 i 天持有股票的最大利润,
// dp[i][1] 表示第 i 天不持有股票的最大利润
int dp[pricesSize][2];
// 初始化第 0 天的状态
dp[0][0] = -prices[0]; // 第 0 天买入股票,持有股票,利润为 -prices[0]
dp[0][1] = 0; // 第 0 天不持有股票,利润为 0
// 从第 1 天开始,根据前一天的状态更新今天的状态
for (int i = 1; i < pricesSize; i++) {
// 状态 0:持有股票
// 可能选择继续持有或从不持有状态转为买入股票
dp[i][0] = fmax(dp[i-1][0], dp[i-1][1] - prices[i]);
// 状态 1:不持有股票
// 可能选择继续不持有或卖出持有的股票(支付手续费)
dp[i][1] = fmax(dp[i-1][1], dp[i-1][0] + prices[i] - fee);
}
// 返回最后一天不持有股票的最大利润
return dp[pricesSize-1][1];
}