买卖股票系列是动态规划的经典题目,六种变型
leetcode 121题
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
【注意】:只有一次交易机会,那么这道题就简化了许多。
我们只需要用一个数组来维护即可,数组 dp[i] 表示在第 i 天卖出能获得的最大利润,用一个变量minPrice来记录 i 以前的最小价格,那么我们只需在价格最小时买入,在第 i 天卖出即可获得 dp[i],最后找出最大的dp[i]
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.empty()) return 0;
vector<int> nums(prices.size(), 0);
int profitMax = 0;
int minPrice = prices[0];
for(int i = 1; i < prices.size(); i++) {
minPrice = min(prices[i], minPrice );
dp[i] = prices[i] - nums[i];
if(dp[i] > profitMax) profitMax = dp[i];
}
return profitMax;
}
};
这里可以对空间复杂度进行优化,因为dp数组是不需要的,构建 dp[i] 的时候是用不上dp[0]到dp[i-1]的,所以我们只需要在遍历时更新profitMax即可,根本无需使用dp数组。
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.empty()) return 0;
int minPrice = prices[0], profitMax = 0;
for(int i = 1; i < prices.size(); i++) {
profitMax = max(profitMax, prices[i] - minPrice );
minPrice = min(minPrice , prices[i]);
}
return profitMax;
}
};
leetcode 122题
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
【注意】:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
这道题相比于前一道变化在于允许多次交易,但每次购买股票前需要出售掉之前的股票,即最多只允许手里存在一只股票。
因为对买卖次数没有限制,所以可以直接这么判断:对于第 i 天,如果第 i+1 天的价格 > 第 i 天的价格,那么就在第 i 天买入, 第 i+1 天卖出,如果第 i+2 天的价格 > 第 i+1 天的价格,那么就在第 i+1 天接着买入,在第 i+2 天卖出,其本质与第 i 天买入,第i+2天卖出是一样。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int profitMax = 0;
for(int i = 0; i < prices.size()-1; i++) {
if(prices[i+1] > prices[i]) profitMax += prices[i+1] - prices[i];
}
return profitMax;
}
};
构建一个二维数组 dp[2][len],其中dp[0][i] 表示第 i 天持有现金所获取的最大利润,dp[1][i] 表示第 i 天持有股票所获取的最大利润
边界:
dp[0][0] = 0,dp[1][0] = -prices[0]
状态转移方程:
dp[0][i] = max{ dp[0][i-1], d[1][i-1] + prices[i] }
dp[1][i] = max{ dp[0][i-1] - prices[i], dp[1][i-1] }
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(2, vector<int>(prices.size()));
dp[0][0] = 0;
dp[1][0] = -prices[0];
for(int i = 1; i < prices.size(); i++) {
dp[0][i] = max(dp[0][i-1], dp[1][i-1] + prices[i]);
dp[1][i] = max(dp[0][i-1] - prices[i], dp[1][i-1]);
}
return dp[0][prices.size()-1];
}
};
leetcode 123题
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
限制交易次数为2次,这里我们可以把交易次数2,分解为前后两次,对于 0, 1, 2 … n-1 这 n 天,找寻一个 i ,使得在 0 - i 之间交易一次,在 i - n-1 之间交易一次,求这样的 i ,使得 这两次交易的利润之和最大。那么就可以用第一题那种买卖一次股票的方式来求解。实际上就是求第 i 天以前的和第 i 天以后的最大利润之和,然后再娶个最大值。
但这里要注意,第一题里我们遍历到 i 的时候,实际上求得是第 i 天以前(包括第 i 天)交易一次所能获得的最大利润,而这道题我们还要求第 i 天以后(包括第 i 天)交易一次所能获得的最大利润,这里就不能正向遍历了,只能反向遍历。
如何反向遍历呢,就是定义一个 maxPrice,反向遍历来更新 maxPrice,(正向遍历是定义一个minPrice),这样只需要两次遍历就能求出结果,时间复杂度0(n)。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if(n == 0) return 0;
int minPrice = prices[0];
int profitMax = 0;
vector<int> leftProfit(prices.size(), 0); // 存 i 天以前交易一次获得的最大利润
for(int i = 1; i < prices.size(); i++) {
profitMax = max(profitMax, prices[i] - minPrice);
leftProfit[i] = profitMax;
minPrice = min(prices[i], minPrice);
}
int maxPrice = prices[n-1];
profitMax = 0;
vector<int> rightProfit(prices.size(), 0); // 存 i 天以后交易一次获得的最大利润
for(int i = n-2; i >= 0; i--) {
profitMax = max(profitMax, maxPrice - prices[i]);
rightProfit[i] = profitMax;
maxPrice = max(prices[i], maxPrice );
}
// 计算两者利润之和,再取最大值
int maxPro = leftProfit[n-1];
for(int i = 0; i < n-1; i++){
maxPro = max(maxPro, leftProfit[i] + rightProfit[i+1]);
}
return maxPro;
}
};
leetcode 188题
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)
该问题是对问题三的推广,可用动态规划构建一个三维数组来解决
构建 dp[k][i][2]
其中k表示的是交易次数,i表示的是第几天,2表示的是有两种状态:手上没有股票/手上有股票
dp[k][i][0] 数组中的数字表示的是到了第i天,交易了k次,手上没有股票获得的最大利益
dp[k][i][1] 数组中的数字表示的是到了第i天,交易了k次,手上持有股票获得的最大利益
我们定义买入即视为一次操作,那么状态转移方程为:
i 天 t 次交易现在手上不持有 = max( i-1 天 t 次交易手上不持有,i-1 天 t 次交易手上持有 + i 天卖出价格prices)
dp[t][i][0] = max(dp[t][i - 1][0], dp[t][i - 1][1] + prices[i])
i 天 t 次交易现在手上持有 = max( i-1 天 t 次交易手上持有,i-1 天 t-1次 交易手上不持有 - i 天买入价格)
dp[t][i][1] = max(dp[t][i - 1][1], dp[t - 1][i - 1][0] - prices[i])
边界为:
dp[t][0][0] 0天t次交易,手上不持有:可能的 0
dp[t][0][1] 0天t次交易,手上持有:不可能(0天没有股票,所以无法买入持有;持有说明至少进行了一次买入,买入就交易,因此这里不可能【不可能意思就是不能从这里转移】
dp[0][i][0] i天0次交易,手上不持有:0
dp[0][i][1] i天0次交易,手上持有:不可能(不交易手上不可能持有)
【注意】:当 k .>= prices.size() / 2 时,退化为无限次买卖,此时可用问题二的贪心来做,否则会造成数组太大,内存溢出
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
if(!prices.size()) return 0;
// 当 k >= prices.size()/2时,退化为无限次交易,此时可用问题二的贪心来做
if(k >= prices.size()/2)
{
int sum = 0;
for(int i = 1; i < prices.size(); i++)
{
if(prices[i] > prices[i - 1]) sum += prices[i] - prices[i - 1];
}
return sum;
}
vector<vector<vector<int>>> dp(k + 1, vector<vector<int>>(prices.size() + 1, vector<int>(2, 0)));
for(int t = 0; t <= k; t++)
dp[t][0][1] = INT_MIN;
for(int i = 0; i <= prices.size(); i++)
dp[0][i][1] = INT_MIN;
for(int t = 1; t <= k; t++)
{
for(int i = 1; i <= prices.size(); i++)
{
// 注意这里的prices[i-1],是因为我们的dp是从1开始的,而prices是从0开始的
dp[t][i][0] = max(dp[t][i - 1][0], dp[t][i - 1][1] + prices[i - 1]);
dp[t][i][1] = max(dp[t][i - 1][1], dp[t - 1][i - 1][0] - prices[i - 1]);
}
}
return dp[k][prices.size()][0];
}
};
二维数组DP是在三维数组DP的基础上合并买入卖出那两维构造的,将一次买和一次卖均视为一次交易,那么总的交易次数时k*2,注意,这里遍历k的时候,如果为奇数,代表当前执行买操作,持有股票,如果为偶数,代表当前执行卖操作,持有现金,即dp[t][i] 表示第 i 天 t 次交易的最大利润。
状态转移方程和边界条件也是依据三维来改的。
状态转移方程:
当 t 为奇数时,代表持有股票:
dp[t][i] = max(dp[t][i-1], dp[t-1][i-1] - prices[i])
当 t 为偶数时,代表没有股票:
dp[t][i] = max(dp[t][i-1], dp[t-1][i-1] + prices[i])
边界:
dp[0][0] = 0 0 天没有股票,利润为0
dp[t][0] = INT_MIN( t 为奇数) 0天t次交易,不可能
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
if(!prices.size()) return 0;
// 当 k >= prices.size()/2时,退化为无限次交易,此时可用问题二的贪心来做
if(k >= prices.size()/2)
{
int sum = 0;
for(int i = 1; i < prices.size(); i++)
{
if(prices[i] > prices[i - 1]) sum += prices[i] - prices[i - 1];
}
return sum;
}
vector<vector<int>> dp(2*k+1, vector<int>(prices.size()+1, 0));
// 边界
for(int t = 1; t <= 2*k; t += 2) dp[t][0] = INT_MIN;
// 状态转移方程
for(int t = 1; t <= 2*k; t++) {
for(int i = 1; i <= prices.size(); i++) {
if(t & 1 == 1) { // 奇数
dp[t][i] = max(dp[t][i-1], dp[t-1][i-1] - prices[i-1]);
} else { // 偶数
dp[t][i] = max(dp[t][i-1], dp[t-1][i-1] + prices[i-1]);
}
}
}
return dp[2*k][prices.size()];
}
};
leetcode 309题
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
这道题有三个交易状态,买入(持有股票)、卖出(没有股票)和冷冻期(持有股票或没有股票),那么就可以设置一个二维的dp数组 dp[n][3],其中
dp[i][0] 代表第 i 天状态为卖出所获得的最大利润,
dp[i][1] 代表第 i 天状态为买入所获得的最大利润,
dp[i][2] 代表第 i 天状态为冷冻期所获得的最大利润。
状态转移方程:
dp[i][0] = max{ dp[i-1][0], dp[i-1][1] + prices[i] }
dp[i][1] = max{ dp[i-1][1], dp[i-1][2] - prices[i] }
dp[i][2] = { dp[i-1][0], dp[i-1][1], dp[i-1][2] }
边界:
dp[0][0] = 0
dp[0][1] = -prices[0]
dp[0][2] = 0
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.empty()) return 0;
vector<vector<int>> dp(prices.size(), vector<int>(3, 0));
// 边界
dp[0][1] = -prices[0];
// 状态转移方程
for(int i = 1; i < prices.size(); i++) {
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]); // 持有现金
dp[i][1] = max(dp[i-1][1], dp[i-1][2] - prices[i]); // 持有股票
dp[i][2] = max(dp[i-1][0], max(dp[i-1][1], dp[i-1][2])); // 冷冻期
}
return max(dp[prices.size()-1][0], dp[prices.size()-1][2]);
}
};
leetcode 714题
这道题也是无限次买卖,但是因为加入了手续费,所以需要使用尽可能少的次数来达到最大利润,所以不能用贪心来做,贪心会增加交易次数,导致多扣除手续费。所以只能用dp来做,相比于题二,只需要让每次购买股票时的成本增加fee即可,也就是说每次买股票的时候就吧手续费扣掉,卖股票的时候不扣。
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
vector<vector<int>> dp(prices.size(), vector<int>(2, 0));
dp[0][0] = 0;
dp[0][1] = -prices[0] - fee;
for(int i = 1; i < prices.size(); i++) {
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee);
}
return dp[prices.size()-1][0];
}
};