一、目录
二、股票系列问题
1.买卖股票的最佳时机(121题)
1.1.题目
1.2.思路
1.3.代码实现(1种)
2.买卖股票的最佳时机II(122题)
2.1.题目
2.2.思路
2.3.代码实现(3种)
3.买卖股票的最佳时机III(123题)
3.1.题目
3.2.思路
3.3.代码实现(2种)
4.买卖股票的最佳时机IV(188题)
4.1.题目
4.2.思路
4.3.代码实现(1种)
5.买卖股票的最佳时机含冷冻期(309题)
5.1.题目
5.2.思路
5.3.代码实现(2种)
6.买卖股票的最佳时机含手续费(714题)
6.1.题目
6.2.思路
6.3.代码实现(2种)
7.股票的最大利润(剑指Offer63题)
三、总结
dp:根据题目要求,我们只有一次买卖股票的机会,设dp[i]表示第i天结束后的最大利润,要想使某日的利润最大,那就需要找出该日之前的价格最小的一天,然后相减,也就是dp[i]=max(dp[i],prices[i]-prices[之前最小的]),这里可以直接省略dp数组,动态维护一个int值。
class Solution {
/**
* dp:dp[i]表示在第i天卖出时的最大利润,此题可以省去dp数组,只需要动态维护一个int值即可
* 在某一天买入,然后在后边的某一天卖出,当这天卖出时,最大的利润就是当天的价钱-之前的最小价钱
* 所以需要动态维护i-1天的最小价钱,然后计算第i天卖出的价钱,再去动态维护0~i天的最大利润
*/
public int maxProfit(int[] prices) {
int n = prices.length;
int minPrice = prices[0]; //记录i-1天的最低价钱,初始化为第一天的price
int maxProfit = 0; //记录最大利润,0表示未买入
for (int i = 1; i < n; i++) {
minPrice = Math.min(minPrice, prices[i - 1]);
maxProfit = Math.max(maxProfit, prices[i] - minPrice);
}
return maxProfit;
}
}
与I相比,该题不限制我们买卖股票的次数。
1.贪心:把股票价格看成一个折线图,x轴是天数,y轴是价格,那么折线图上升的时期就是股票盈利的时期,既然不要求买卖次数,那么我们可以两天两天地去判断(因为买入卖出要分为两天),若当天的价格>前一天的价格,那么就把他们的差值累加,最后得到的就是所有上升时期所盈利的利润。
2.dp:设dp[i]为第i天结束后的最大收益,此时分为两种状态,手里有股票dp[i][0]和手里没股票dp[i][1],推动态转移方程: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]),今天没股票,说明昨天也没有股票或者是昨天还有股票,今天卖掉了股票。
2.3.1 贪心
public int maxProfit1(int[] prices) {
int ans = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] - prices[i - 1] > 0) {
ans += prices[i] - prices[i - 1];
}
}
return ans;
}
2.3.2 dp(无空间优化)
public int maxProfit2(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = -prices[0]; //第0天有股票说明今天买了,初始化-prices[0],其他值默认为0
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = Math.max(dp[i - 1][0] + prices[i], dp[i - 1][1]);
}
//最后一天手里还有股票无意义,直接返回dp[n-1][1]
return dp[n - 1][1];
//注意到第i天结束后的最大利润之和i-1有关,则可以优化空间复杂度
}
2.3.3 dp(有空间优化)
/**
* 1.贪心:因为不限制买卖次数,只限制同一时间持有一支股票的话,那可以随时进行买卖
* 只需要计算出所有上升的差值即可,即prices[i]-prices[i-1]
* 2.dp:设dp[i]代表第i天结束的最大利润,此时有2种状态,手里有股票和没股票
* 有股票dp[i][0],没有股票是dp[i][1],
* dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i]),可以是上一天的股票,也可以是上一天没股票,今天才买
* dp[i][1]=max(dp[i-1][0]+prices[i],dp[i-1][1]),今天没有股票,说明是昨天还有股票,今天卖了,或者是之前就卖了
*/
public int maxProfit(int[] prices) {
int n = prices.length;
int hold = -prices[0]; //第0天有股票说明今天买了,初始化-prices[0]
int no = 0; //记录当天结束后没有股票
for (int i = 1; i < n; i++) {
hold = Math.max(hold, no - prices[i]);
no = Math.max(hold + prices[i], no);
}
//最后一天手里还有股票无意义,直接返回no
return no;
}
与II不同的是,本题限制了买卖的次数为2次。
dp:设dp[i]为第i天结束后的最大盈利,那么此时有5种状态,买入第一次股票(dp[i][0])、卖出第一次股票(dp[i][1])、 买入第二次股票(dp[i][2])、 卖出第二次股票(dp[i][3])、什么也不做(此时收益为0,后续不再考虑此类情况)。
推导动态转移方程:dp[i][0]=max(dp[i-1][0],-prices[i]),表示之前已经买了第一次或者今天才买第一次;dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i]),表示之前已经卖了第一次或者今天才卖第一次(前提是之前要买过第一次); dp[i][2]=max(dp[i-1][2],dp[i-1][1]-prices[i]),表示之前已经买了第二次或者今天才买第二次(前提是之前要卖过第一次);dp[i][3]=max(dp[i-1][3],dp[i-1][2]+prices[i]),表示之前已经卖了第二次或者今天才卖第二次(前提是之前买过第二次)。
3.3.1 dp(无空间优化)
public int maxProfit1(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][4];
dp[0][0] = -prices[0]; //初始化,第0天买入第一次
dp[0][2] = -prices[0]; //初始化,其中包含了第0天买入第一次和第0天卖出第一次,然后再买入第二次
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] - prices[i]);
dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] + prices[i]);
}
//最终范围在0 dp[n-1][1] dp[n-1][3]之间,但后者的边界情况已经包含了0,且dp[n-1][3]又包括了同一天买入卖出的情况,所以直接返回即可
return dp[n - 1][3];
//浅浅优化一下空间复杂度
}
3.3.2 dp(有空间优化)
/**
* dp:设dp[i]为第i天结束后的最大收益,此时可能有5种状态
* 0.买了第一次股票 1.卖了第一次股票 2.买了第二次股票 3.卖了第二次股票 4.什么都没干
* 什么都没干的话收益为0,可以不进行记录
* dp[i][0]=max(dp[i-1][0],-prices[i]),今天可以什么都没干(之前买了第一次股票),也可以重新记录买的第一次股票
* dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i]),今天处于卖了第一次股票的状态,那么昨天可能也是卖了第一次股票,或者今天刚卖了第一次股票
* dp[i][2]=max(dp[i-1][2],dp[i-1][1]-prices[i]),处于买了第二次,可能是之前买的,也可能是今天买的
* dp[i][3]=max(dp[i-1][3],dp[i-1][2]+prices[i]),卖了第二次股票,可能是之前卖的,也可能是今天卖的
*/
public int maxProfit(int[] prices) {
int n = prices.length;
int buy1 = -prices[0]; //别忘了初始化
int sell1 = 0;
int buy2 = -prices[0]; //别忘了初始化
int sell2 = 0;
for (int i = 1; i < n; i++) {
buy1 = Math.max(buy1, -prices[i]);
sell1 = Math.max(sell1, buy1 + prices[i]);
buy2 = Math.max(buy2, sell1 - prices[i]);
sell2 = Math.max(sell2, buy2 + prices[i]);
}
return sell2;
}
与III不同的是,III要求最多两次买卖,本题要求k次,在III种,我们通过构建buy1、sell1、buy2、sell2来动态维护最大利润,III就是IV的一种特殊情况。我们只需要在建立dp数组时把dp数组变成动态的即可。
在本题中,我们可以看成k次买/卖,设dp[k][0/1]代表第k次的买/卖,这里的dp中存储的是动态维护的最大利润,而不是某天的最大利润,如果要存储每天的最大利润,我们所建立的dp数组就变成了dp[n][k][0/1],代表第n天的第k次买/卖。
需要注意的是,本题k的范围不固定,当k>数组长度的一半时,dp退化为贪心,有助于提高效率。
class Solution {
/**
* dp:设dp[i]表示第i天结束后的最大利润
* IV和III相似的一点就是,III中规定k=2,也就是有两次的买卖机会,当时我们用buy1,sell1,buy2,sell2表示,
* 那么IV中也一样,在创建dp的时候设置成动态的就行,
* 还有一点就是当k>数组长度的一半时,dp退化为贪心,此时类似于II的解法
*/
public int maxProfit(int k, int[] prices) {
if (k == 0) { //注意题目所给边界,k可能为0
return 0;
}
if (k > prices.length / 2) { //调用贪心算法
return greedy(prices);
}
//dp
int[][] dp = new int[k][2]; //一共有k组买卖,dp[i][0]代表第i次买,dp[i][1]代表第i次卖
for (int i = 0; i < k; i++) {
dp[i][0] = -prices[0]; //别忘了初始化买入的时候
}
for (int i = 1; i < prices.length; i++) {
//dp[0][0]表示第0次买,因为下一天的买卖之和前一天有关,所以这里节省了空间,否则是dp[n][k][0/1]
dp[0][0] = Math.max(dp[0][0], -prices[i]); //buy1
dp[0][1] = Math.max(dp[0][1], dp[0][0] + prices[i]); //sell1
for (int j = 1; j < k; j++) {
//这次的buy是通过上一次的sell-prices[i]得来的
dp[j][0] = Math.max(dp[j][0], dp[j - 1][1] - prices[i]); //buy(k+1)
dp[j][1] = Math.max(dp[j][1], dp[j][0] + prices[i]); //sell(k+1)
}
}
//和之前一样,后边的卖是包含了前面的卖的,直接返回最后一次卖出所获得的最大利润就可
return dp[k - 1][1];
}
public int greedy(int[] prices) {
//和II中一样
int ans = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] > prices[i - 1]) {
ans += prices[i] - prices[i - 1];
}
}
return ans;
}
}
与前面都不同的是,本题含有冷冻期。
dp:设dp[i]为第i天结束后的最大收益,此时可能的状态有,持有股票(dp[i][0]),没有股票且处于冷冻期(dp[i][1]),没有股票且不处于冷冻期(dp[i][2])。(这里的冷冻期是指今天结束后,明天会进入冷冻)。
推导动态转移方程 :
dp[i][0]=max(dp[i-1][0],dp[i-1][2]-prices[i]),表示可能之前就有股票或者之前没股票,但想要今天买股票的话,昨天必须不处于冷冻期;
dp[i][1]=dp[i-1][0]+prices[i],今天进入了冷冻期说明昨天有股票且把股票卖了;
dp[i][2]=max(dp[i-1][2],dp[i-1][1]),今天不持有股票且不处于冷冻期,说明昨天不持有股票处于冷冻期或者不持有股票不处于冷冻期。
5.3.1 dp(无空间优化)
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][3];
dp[0][0] = -prices[0]; //第0天买入后 dp[0][1]、dp[0][2]默认为0,此时无收益
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2] - prices[i]);
dp[i][1] = dp[i - 1][0] + prices[i];
dp[i][2] = Math.max(dp[i - 1][1], dp[i - 1][2]);
}
//第n-1天还持有股票的话无意义,直接省略
return Math.max(dp[n - 1][1], dp[n - 1][2]);
//注意到dp[i][..]只和dp[i-1][..]有关,可以空间优化,用变量存储dp[i-1][..],空间复杂度O(3n)->O(1)
}
5.3.2. dp(有空间优化)
/**
* dp:设dp[i][1/2/3]代表第i天结束之后的累计最大收益(此收益可能是负数)
* 只可能有三种状态:1.持有一支股票 2.不持有股票且处于冷冻期 3.不持有股票且不处于冷冻期
* 分别对应dp[i][0]、dp[i][1]、dp[i][2],这里的冷冻期是指第i天结束后的状态,也就是i+1天时会被冻结
* dp[i][0]=max(dp[i-1][0],dp[i-1][2]-prices[i]),可以是上一天的股票,也可以是上上一天卖掉股票后今天又买回来
* dp[i][1]=dp[i-1][0]+prices[i],明天会被冻结说明今天卖出了股票,那么在i-1天我们就需要持有股票
* dp[i][2]=max(dp[i-1][1],dp[i-1][2]),不处于冷冻期,说明昨天什么都没干,昨天可能处于冷冻期dp[i-1][1],也可能没处于冷冻期dp[i-1][2]
*/
public int maxProfit1(int[] prices) {
int n = prices.length;
int flag1 = -prices[0]; //别忘了初始化
int flag2 = 0;
int flag3 = 0;
for (int i = 1; i < n; i++) {
flag1 = Math.max(flag1, flag3 - prices[i]);
flag2 = flag1 + prices[i];
flag3 = Math.max(flag1, flag2);
}
//第n-1天还持有股票的话无意义,直接省略
return Math.max(flag2, flag3);
}
之前想到用贪心,但是想的太简单了,如果判断一小段时间内是盈利就卖出的话,如果下一天还是盈利,那么就浪费了手续费。
dp:设dp[i]为第i天结束后的最大收益,且设手续费是在卖出股票后缴纳。那么此时有两种状态,dp[i][0]表示有股票,dp[i][1]表示无股票。
推导动态转移方程:
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),表示以前就没股票或者今天才卖掉股票(要缴纳手续费)。
6.3.1 dp(无空间优化)
public int maxProfit1(int[] prices, int fee) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][1] = -prices[0]; //别忘了初始化
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[n - 1][0];
}
6.3.2 dp(有空间优化)
/**
* dp:和II类似,本题增加了手续费,设dp[i]表示第i天结束后的最大收益,此时有两种状态,
* 0.无股票 1.有股票 (设手续费是在卖出股票时结算)
* dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i]-fee),上一天也没股票,或上一天有股票,今天卖出
* dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]),上一天的股票,或上一天没股票,今天买的股票
*/
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
int flag1 = -prices[0]; //有股票
int flag2 = 0; //没股票
for (int i = 1; i < n; i++) {
flag1 = Math.max(flag1, flag2 - prices[i]);
flag2 = Math.max(flag2, flag1 + prices[i] - fee);
}
return flag2;
}
本题与 1.买卖股票的最佳时机(121题)一样,唯一不同的就是数组的大小可能为0,需要判断一下边界。
1.dp[i]表示的是第i天结束后的最大收益。
2.要考虑完整的状态,总的状态分为持有股票、不持有股票、什么也不做0收益(不考虑此类),不持有股票的时候可能处于冻结期或者非冻结期。
3.还有手续费的问题,手续费的缴纳时机分为买入和卖出,dp的时候放在买入,贪心的时候放在卖出(没搞懂)。
4.在更新dp数组的时候,已经把当天买入当天卖出的情况融入进去了,对结果不影响。
5.掌握好允许买卖k次的那道题目,其他题目差别不大。
感谢观看,如有问题,欢迎补充!