2021新年算法小专题—2.股票买卖利润刷题(Java)

本篇是股票买卖问题的更多题解,在上一篇文章中我们已经介绍了这一题型,实际上是一类dp问题,我们用自动机的思想去解决,在上一篇中我们以一道限定只买卖一次股票的题目为例进行了讲解,文章链接。下面我们继续完成剩下的题目。

1.可以多次买卖的股票买卖问题

本题链接:122.买卖股票的最佳时机II(简单)

题目

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 :

输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
分析

和上一篇题目的唯一区别就是不再限制只允许买一次,因此我们只需改变一个位置即可。回忆上一篇的题目,我们是这样递推的:

        dp0=Math.max(dp0,dp1+prices[i]);
        dp1=Math.max(dp1,-prices[i]);

dp0代表今天不持有股票获得的最大利润,dp1代表今天持有股票获得的最大利润。我们是这样总结出上面的状态转移方程的:今天不持有股票,对应两种情况:前一天就不持有股票、前一天持有股票但今天卖出了,在这两种情况中取最大值,即max(dp0,dp1+prices[i])。今天持有股票也对应两种情况:前一天就持有股票、前一天不持有股票但是今天买入了,在这两种情况中取最大值,即max(dp1,-prices[i])

在上题中,由于只能买卖一次,因此在今天持有股票的“前一天不持有但今天买入”情况下只需要计算本次买入股票的花费即可,跟之前的状态无关(之前不可能买卖过股票,也就是一直在观望,利润没有发生变化),而本题则不再是这样,在本次购买之前可能已经发生过买卖了,这就是与例题唯一的不同之处。我们只需要更改这个位置为以下代码即可:

	int dp0_tmp=dp0;// 记录上一次不持有股票的情况。
	dp0=Math.max(dp0,dp1+prices[i]);
    dp1=Math.max(dp1,dp0_tmp-prices[i]);
参考代码
class Solution {
    public int maxProfit(int[] prices) {
        
        int dp0=0;
        int dp1=-prices[0];

        for(int i=1;i<prices.length;i++){
            int tmp=dp0;
            dp0=Math.max(dp0,dp1+prices[i]);
            dp1=Math.max(dp1,tmp-prices[i]);
        }

        return dp0;
    }
}

2.限制最多买卖两次的股票买卖问题

本题链接:123.买卖股票的最佳时机III(困难)

题目

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 :

输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
     随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

分析

现在要求只能完成两笔交易了,这就跟以前大不相同了,以前我们要么是只能买一次,要么是可以买无限次,而现在限制某一个具体数字了,我们就不得不把这个次数也考虑进状态转移方程了。我们在原来的dp数组中再加入一个维度,记录当前的已经买卖次数

因为现在k也是一个限制我们买卖股票的因素了,因为当你买卖超过两次后就不能再买入了,所以要把这个状态也算进去,天数、当前允许交易的次数、当前股票的持有状态这三个变化的因素我们都要考虑进去,所以使用**三维数组dp**来记录。(当然,后面我们会优化成几个变量的形式,现在这样说只是为了更好理解)

举例:dp[i][k][0]表示第i天时不持有股票,最多还能进行k次交易,(这里买入并卖出看作一次交易,如果买入了则必然要卖出,所以我们以买入时对k做减一操作,卖出时就不用重复考虑了)

本题中k是从2开始取,因此这个维度只会出现2、1两个值(为0时则代表什么都不能做了,不会对获利产生影响,因此省略不写即可),我们的状态转移方程如下:

    dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i]);
    dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i]);

    dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i]);
    dp[i][1][1] = max(dp[i-1][1][1], -prices[i]);

前面已经说明了,我们在买入股票时做k的更新,因此dp[i][2][1]“今天持有股票,还能最多买两次股票”这个状态下的最大利润,在“前一天就持有股票”“前一天未持有股票,但今天买入了股票(做k-1,k变为1)”这两个状态下利润较大值中产生。

和上一篇介绍的一样,我们到这里就发现当前每个状态事实上只与上一天的状态相关,我们没有必要用数组去记录完整的数据,我们只需要变量做缓存、每次更新递推求解即可。最终代码如下。

参考代码
class Solution {
    public int maxProfit(int[] prices) {
        // 初始值
        int dp2_0=0;// 当前不持有股票,利润是0
        int dp1_0=0;
        int dp2_1=-prices[0];// 当前持有了股票,利润是-prices[0]
        int dp1_1=-prices[0];
        
        for(int i=1;i<prices.length;i++){

            dp2_0=Math.max(dp2_0,dp2_1+prices[i]);// 可以rest或卖出(卖出后加股票价格的利润)
            dp2_1=Math.max(dp2_1,dp1_0-prices[i]);// 可以rest或买入(买入k-1,减股票价格的花费)

            dp1_0=Math.max(dp1_0,dp1_1+prices[i]);// 可以rest或卖出(卖出后加股票价格的利润)
            dp1_1=Math.max(dp1_1,-prices[i]);// 可以rest或买入(注意这时k=1说明只能买入这一次,因此既然本次要买入,在本次购买前不可能发生过交易,所以本次利润直接等于股票价格的花费)
        }
        return dp2_0;
    }
}

3.可以买卖k次的股票买卖问题

本题链接:188.买卖股票的最佳时机IV(困难)

题目

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。(0 <= k <= 100

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例:

输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
分析

这是这个系列中最难的一道了,在本题中我们基于上一道题k=2的情况,拓展到**k为任意值**的情况,因为k可以有很多值,使得我们不能通过变量代替数组了,而且也要考虑以下k本身的合理范围,如果题目的prices数组长度只有10,即十天,k的值为100,我们显然根本不能交易这么多次,更不能为此开辟一个100的数组空间,所以我们要做一些限制,然后要针对每个k值都进行一次dp数组的递推。

像上面所说,我们先来思考一下k的合理性如何判断。对于一个长度为lenprices交易天数数组,每次交易(买入、卖出)需要花费两天时间,每天最多只能进行买入、卖出中的一种,因此对于len天来说,最多只能完成len/2笔交易,如果给的k值大于len/2,那么就相当于无限次买卖了,退化成本篇第一题的情况了。对这种情况我们单独按第一题那样处理。

对于其他情况我们就只好乖乖的使用dp数组了:(因为我们的k这个维度是从下标1开始用的,因此new数组时我们把长度指定为k+1

int[][][] dp = new int[n][k + 1][2];

因为k的值是任意的,我们不能直接把k=1、2这样列举出来了,使用for循环来遍历k为不同值的情况:

for (int k = max_k; k >= 1; k--) {
       dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
       dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); }

k从给出的值(为区分,用max_k代表给出的值,即最大允许的交易次数)递减循环,根据每天(第i天)的不同已经买卖次数k进行递推。

当然我们也要处理一下第一天的初始状态,无论k为何值,第一天未持有股票一定利润为0持有股票一定利润为第一天股票价格的相反数。即:dp[i][k][0] =0; dp[i][k][1] =-prices[i];

最值产生在最后一天最多允许k次交易后的不持有股票状态。为什么不是持有股票状态?前面已经说过了,如果你手里有股票,在最后一天也不抛售出去就无法赚到钱,肯定是要卖出去的。

参考代码
class Solution {
    public int maxProfit(int max_k, int[] prices) {

        int len;
        if((len=prices.length)==0){
            return 0;
        }
        // k值大于len/2,相当于无限制
        if(max_k>len/2){
            int dp0=0;
            int dp1=-prices[0];
            for(int i=1;i<prices.length;i++){
                int tmp=dp0;
                dp0=Math.max(dp0,dp1+prices[i]);
                dp1=Math.max(dp1,dp0-prices[i]);
            }
            return dp0;
        }
		// 其他情况,使用三维数组记录天数、允许交易次数、持有状态
        int[][][] dp = new int[len][max_k + 1][2];
        
        for (int k = max_k; k >= 1; k--) {
            if (i == 0) { // 第一天时的初始值
                dp[i][k][0] =0;
                dp[i][k][1] =-prices[i];
                continue;
            }
            dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);     
        }
        return dp[len - 1][max_k][0];
    }
}

4.含冷冻期的股票买卖问题

本题链接:309.最佳买卖股票时机含冷冻期(中等)

题目

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:

输入: [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
分析

这个题比较简单,相当于是在无限制交易的基础上多加了一个“冷冻期”的概念,即卖出股票后必须休息一天才能买入,我们只需根据这个特点对状态转移方程进行一下修改即可。回想我们前面第一题的状态转移方程:

int dp0_tmp=dp0;// 记录上一次不持有股票的情况。
dp0=Math.max(dp0,dp1+prices[i]);
dp1=Math.max(dp1,dp0_tmp-prices[i]);

dp1的解释是:如果当天持有股票,可以是前一天就持有股票,今天休息了;也可以是一天没有股票,今天买入了。而在本题中,买入要和卖出间隔一天,因此第二句话要改为两天没有股票,今天买入了,即max(dp1,tmp-prices[i])(用tmp代表前二天的最大利润,当前是第i天,前二天即i-2)。然后在循环前指定一下第i-2天的初值:0,每次在循环中更新为dp0_tmp的值,这样就把前二天这个事给表示出来了。

可能说的有点乱,稍微总结一下:本题关键就是把第i-2天即tmp的最大利润正确的更新。借助dp0_tmp每次记录dp0的旧值(得到第i-1天的最大利润), tmp每次记录dp0_tmp的旧值(得到第i-2天的最大利润)。

参考代码
class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length==0){
            return 0;
        }
        int dp0=0,dp1=-prices[0],tmp=0;
        // dp0代表第i天不持有股票,dp1代表第i天持有股票,tmp代表第i-2天不持有股票
        // 关键则是把tmp正确的更新 借助dp0_tmp每次记录dp0的旧值(第i-1天) tmp每次记录dp0_tmp的旧值(第i-2天)
        for(int i=1;i<prices.length;i++){
            int dp0_tmp=dp0;
            dp0=Math.max(dp0,dp1+prices[i]);
            dp1=Math.max(dp1,tmp-prices[i]);
            tmp=dp0_tmp;
        }
        return dp0;
    }
}

5.含手续费的股票买卖问题

本题链接:714.买卖股票的最佳实际含手续费(中等)

题目

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 :

输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:  
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
分析

仔细阅读题目我们其实发现,这道题和不限制交易次数没有任何转移方程上的变化,只是说每次交易多交纳一些费用,我们可以等价的看成每笔买入费用增加了fee,或者是每次卖出时少卖fee,这里选择第一种。即每次买入时都需要花费prices[i]+fee

参考代码
class Solution {
    public int maxProfit(int[] prices, int fee) {

        int dp0=0;
        int dp1=-prices[0]-fee;// 多交手续费
        for(int i=1;i<prices.length;i++){

            int dp0_tmp=dp0;
            dp0=Math.max(dp0,dp1+prices[i]);
            dp1=Math.max(dp1,dp0_tmp-prices[i]-fee);// 多交手续费
        }
        return dp0;
    }
}

刷题总结

到此我们已经刷完了力扣股票买卖问题了,这类dp问题的核心其实就是找到状态、然后写出状态转移方程进行递推求解。最后再总结一下我们的dp数组的含义:

dp[i][k][0]:第i天,当前最多允许交易k,现在手上没有股票;

dp[i][k][1]:第i天,当前最多允许交易k,现在手上持有股票;

k1或无穷大时,我们可以直接省略掉k,当k2时也可以把k=1k=2简单列举出来,这些情况都可以省去数组的空间,使用不同名称的变量即可,以第二题为例:

dp2_0=Math.max(dp2_0,dp2_1+prices[i])
dp2_1=Math.max(dp2_1,dp1_0-prices[i])

dp1_0=Math.max(dp1_0,dp1_1+prices[i])
dp1_1=Math.max(dp1_1,-prices[i])

而当k为任意值时就必须使用for循环去在每一天的循环中更新dp数组了。如第三题:

for (int k = max_k; k >= 1; k--) {
       dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
       dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); }

找好状态转移方程后,我们还要明确题目要求的最大利润在哪里产生。既然我们用自动机状态的方式去解决了,最值一定在最后一天的某个状态(终态)下产生,而且最后卖出股票才能获得股票具有的价值,因此持有状态应该是不持有。即dp[prices.length-1][k][0]


更多文章:

  1. 新年算法小专题1.滑动窗口(Java)

  2. 新年算法小专题1.滑动窗口刷题(Java)

  3. 新年算法小专题2.股票买卖(Java)

你的喜欢是我创作的动力,喜欢请关注,感谢每一个喜欢~
如有问题欢迎进行交流~
水平所限,如有错误请海涵,欢迎指正~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值