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

概述

熟悉这类题目的同学都知道,事实上这是一类动态规划(dp)问题,在小专题中都暂且只针对这一类问题进行解决了,更全面的动态规划文章以后有机会再努力吧!

简单介绍一句动态规划。动态规划实际是一种分治思想,与分治算法不同的是,分治算法是把原问题分解为若干子问题,自顶向下求解各子问题,合并子问题的解,从而得到原问题的解。动态规划也是把原问题分解为若干子问题,然后自底向上,先求解最小的子问题,把结果存储在表格中,再求解大的子问题时,直接从表格中查询小的子问题的解,避免重复计算,从而提高算法效率

对于本类问题而言,我们采用有限状态自动机的思想来解决,实际就是找到所有状态,并根据状态转移图来列状态转移方程,实际还是dp的思想,下面我们以第一道题为例来介绍。

例题

我们以121.买卖股票的最佳时机为例来进行讲解。题目如下:

题目

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

示例 :

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
分析

首先我们归纳一下题意:每天股票价格在数组prices中,每天可以选择买入、卖出或是什么都不做。只有没有股票时才能买入,只有有股票时才能卖出,而且只能买入(卖出)一次,求最大利润。

下面我们需要明确几件事:

  • 整个股票买卖过程可以进行哪些操作(选择)?每一天都可以买入、卖出、休息(既不买入也不卖出),只不过买入和卖出是有要求的,必须先买入才能卖出。
  • 既然采用自动机来解决,那么过程中有那些状态呢?因为可以进行以上三种操作,因此每一天有两种状态:持有股票不持有股票。今天持有股票是因为之前或今天买入了股票且没有卖出;今天不持有股票是因为之前或今天一直没买股票或者已经卖出了。
  • 最大利润在哪里产生?一定是在最后一天不持有股票状态。(显然把股票卖掉一定比手里拿着股票获利更多)

我们根据是否持有股票这两个状态画出如下图所示的状态转移图。图中状态0表示当前不持有股票,状态1表示当前持有股票

image-20210216105342407

对上图的转移过程的解释如下:当我们不持有股票时,可以通过买入buy变为持有状态;也可以休息rest仍然为不持有状态。当我们持有股票时,可以通过卖出sale变为不持有状态;也可以休息rest仍然为持有状态。

有了状态转移图,我们应该考虑一种数据结构来记录每个时刻(每天)的状态(股票的持有状态)。因此使用一个二维数组来记录,记为dp数组。举例:dp[i][0]表示第i天时不持有股票时的最大利润,dp[i][1]表示第i天时持有股票的最大利润。然后写出状态转移方程,也就是上述两个式子是怎么由其他状态转换来的,怎么求出来的。

dp[i][0]为例,什么情况下第i天才能不持有股票呢?无非就是两种情况:

  • i-1天也是不持有股票,然后第i天也没有买入,此时利润仍然等于第i-1天的最大利润;
  • i-1天持有了股票,但第i天进行卖出了,此时利润等于第i-1天持有股票的利润加上卖出股票的获利。(因为股票只能买卖一次,因此第i-1天持有股票的利润就是之前买股票的花费)

因此dp[i][0]应该取以上两种情况的最大值,即dp[i][0]=max(dp[i-1][0],dp[i-1][0]+prices[i])

同理,以下两种情况下第i天会持有股票:

  • i-1天就已经持有股票,然后第i天没有卖出,此时利润仍然等于第i-1天的最大利润;
  • i-1天不持有股票,但第i天买入了股票,此时利润等于买入股票的价格的相反数。(因为股票只能买卖一次,因此第i天买入股票的利润就是当天买入股票的花费)

因此,dp[i][1]=max(dp[i-1][1],-prices[i])

这样我们就已经写出状态转移方程了,下面要思考初始的状态(初态,显然方程中i不能为0,因此我们把i=0时的情况分析出来,剩下的就从i=1开始直接递推了)。

  • dp[0][0]=0:第1天不持有股票,因此利润是0
  • dp[0][1]=prices[0]:第1天持有股票,即第一天买了股票,利润是第一天股票价格的相反数
参考代码
class Solution {
    public int maxProfit(int[] prices) {
        int [][]dp=new int[prices.length][2];

        dp[0][0]=0;// 第1天不持有股票,利润为0
        dp[0][1]=-prices[0];// 第1天持有股票,利润为第一天股票售价的相反数

        for(int i=1;i<prices.length;i++){
            // 第i天如果没持有股票,对应两种情况:1.前一天就是没有持有股票的状态2.今天把前一天持有的股票给卖了,额外获利今天的股票价格。
            dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
            // 第i天如果持有股票,对应两种情况:1.前一天就是持有股票的状态2.今天刚刚买入股票,额外加上今天股票价格的支出。
            dp[i][1]=Math.max(dp[i-1][1],-prices[i]);
        }

        return dp[prices.length-1][0];// 最大利润产生在【最后一天的不持有股票状态】
    }
}
代码优化

观察上面的代码我们发现,事实上每一天的利润只与上一天的利润有关,最后需要的答案也只是最后一天的不持有股票状态,因此完全可以不用dp数组记录每天的每个状态,只需要用变量来记录上一天的每个状态就可以了,这样能使空间复杂度降为O(1)。核心思想是不变的,只是本题并不需要记录所有状态,只需要保证有足够的数据(前一天的状态数据)来递推出最后一天的情况即可。

即:使用变量dp0来代替dp数组中每天不持有股票的情况dp[i][0]),变量dp1代替dp数组中每天持有股票的情况dp[i][1])。

优化后的代码如下:

class Solution {
    public int maxProfit(int[] prices) {
        //int [][]dp=new int[prices.length][2];
        int dp0=0;
        int dp1=-prices[0];

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

        return dp0;
    }
}

总结

使用自动机的思想,找到股票买卖问题中的三种操作:买入、卖出、休息,两个状态:持有股票、不持有股票,然后画出状态转换图,根据转换图去写出状态转移方程。使用合适的数据结构去记录状态,本题中只有两个变化的状态,天数和持有情况,因此使用二维数组dp记录每天的持有情况就可以了。要额外考虑一下初态的情况,剩下的就交给循环,递推求解即可。明确最后的结果产生在最后一天的不持有股票状态。

后面还会有增加一个状态的变式:即不再限制只能买卖一次,可以买多次、任意次等,我们增加一个状态即可;还会限制买卖间隔时间,我们修改状态转换图即可。以下列出的题目我都会在本专题第二篇—刷题部分去讲解,到时更新了我会把链接放在这里。

Letcode中股票买卖问题(点击链接跳转去看题):

121.买卖股票的最佳时机(简单):即本文例题,最多只买卖一次。

122.买卖股票的最佳时机II(简单):可以多次买卖,不做任何限定。

123.买卖股票的最佳时机III(困难):最多只能买卖两次。

188.买卖股票的最佳时机IV(困难):最多可以买卖k次。k是变化的。

309.最佳买卖股票时机含冷冻期(中等):卖出和买入之间必须至少间隔一天。

714.买卖股票的最佳实际含手续费(中等):可以多次买卖,但每次买入股票都要额外支付手续费。

参考

  1. labuladong的算法小抄。链接

更多文章

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

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

更多文章仍在更新,欢迎收藏本专栏。感谢您的每一个喜欢~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值