动态规划典型题之“买卖股票”系列

买卖股票系列题目1(简单)、

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。来自leetcode121,链接


思路1:暴力法需要两轮遍历,外层遍历买入,内层遍历卖出,因此时间复杂度O(N^2)。思路2,只需要一层遍历,同时更新最小值以及利润最大值。因此需要两个变量保存最小买入价格和最大利润。时间复杂度降为O(N);

public int maxProfit1(int[] prices) {
        if(prices==null||prices.length<1)
            return 0;
        int minBuy=Integer.MAX_VALUE;
        int maxSell=0;
        for(int i=0;i<prices.length;i++){
            if(prices[i]<minBuy){//如果当前价格小于最小价格
                minBuy=prices[i];
            }
            else{//当前价格大于最小价格
                int get=prices[i]-minBuy;
                maxSell=Math.max(maxSell,get);
            }
        }
        return maxSell;
    }

思路2、动态规划,如果说买卖股票这一系列题目有一个通用解法的话,那就是动态规划。
  dp[i][j]表示第i天之后的最大收益(如果买入还没有卖出,那么收益就是负数),如果j=0,那么表示i天之后是持有股票的,如果j=1,那么表示i天之后是持有现金的(不持有股票的)。因此,可以总结出状态转移方程如下:

dp[i][0]=dp[i-1][0]+0-price[i]
dp[i][1]=dp[i-1][1]+dp[i-1][0]+price[i]

注意:这里注意第一行的0-price[i],因为只能买卖一次,如果第i天是持有股票的,那么要么dp[i-1][0]即前一天就持有股票,要么之前没有买卖过,今天第一次买入股票。只有这两种情况。

因此,代码如下:

 public int maxProfit(int[] prices) {
        if(prices==null||prices.length<1)
            return 0;
        int[][] dp=new int[prices.length][2];
        dp[0][0]=-prices[0];//初始化
        dp[0][1]=0;
        
        for(int i=1;i<prices.length;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]);
            
        }
        return dp[prices.length-1][1];
        }


买卖股票系列题目2(简单)、

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。leetcode122,链接


思路1:相比前面一道题,这里的变化就是可以买卖多次股票,求最大利润是多少。这里使用一种“峰谷”的思想(也不容易想,但是想到了就挺简单)。

这里peak就是峰,valley就是谷,如上图。也就是说最大利润等于所有相邻的“峰”-相邻的“谷”的累加。如果我们试图跳过其中一个峰值来获取更多利润,那么我们最终将失去其中一笔交易中获得的利润,从而导致总利润的降低。
  例如,在上述情况下,如果我们跳过 peaki 和 valleyj 试图通过考虑差异较大的点以获取更多的利润,获得的净利润总是会小与包含它们而获得的净利润,因为 C 总是小于 A+B。因此我们的任务就是找到所有相邻的“峰谷”。

 public int maxProfit1(int[] prices) {
        if(prices==null||prices.length<1)
            return 0;
        int peak=0,valley=0;
        int i=0;
        int res=0;
        while(i<prices.length-1){
            //首先找到谷
            while(i<prices.length-1&&prices[i]>=prices[i+1])
                i++;
            valley=prices[i];
             //然后找到峰
            while(i<prices.length-1&&prices[i]<=prices[i+1])
                i++;
            peak=prices[i];
            res+=peak-valley;
        }
        return res;
    }

时间复杂度O(N).

思路2:就是刚才前面讲的通用解法,dp[i][j]表示第i天之后的最大收益(如果买入还没有卖出,那么收益就是负数),如果j=0,那么表示i天之后是持有股票的,如果j=1,那么表示i天之后是持有现金的(不持有股票的)。因此,可以总结出状态转移方程如下:

dp[i][0]=Math.max( dp[i-1][0],dp[i-1][1]-price[i] )

即:前一天要么已经持有股票了,今天依然持有,或者前一天卖出股票,今天买入股票,两者之间收益较大值

dp[i][1]=Math.max( dp[i-1][1],dp[i-1][0]+price[i] )

即:前一天和今天都不持有股票,或者前一天持有今天卖出的最大收益。

public int maxProfit2(int[] prices) {
        if(prices==null||prices.length<1)
            return 0;
        int[][] dp=new int[prices.length][2];
        //初始化
        dp[0][0]=-prices[0];
        dp[0][1]= 0;
        for(int i=1;i<prices.length;i++){
            dp[i][0]=Math.max( dp[i-1][0],dp[i-1][1]-prices[i]) ;

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

        }
        return dp[prices.length-1][1];
     }

时间复杂度:O(N),这里 N 表示股价数组的长度。空间复杂度O(N).

空间优化,因为这个表实际上是n行2列的,每一行只和上一行有关,因此只需要一维dp,或者说2个变量即可。因此将空间复杂度降为o(1)。

public int maxProfit(int[] prices) {
        if(prices==null||prices.length<1)
            return 0;
        int have=-prices[0];//一开始持有股票、相当于上面dp的第一列
        int no=0;//一开始不持有股票,相当于上面dp的第二列
        int preHave=have;//用于保存
        int preNo=no;
        for(int i=1;i<prices.length;i++){
            have=Math.max(preHave,preNo-prices[i]);
            no=Math.max(preNo,preHave+prices[i]);
            preHave=have;
            preNo=no;
        }
        return no;

      }

买卖股票系列题目3、(困难)

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


思路:这道题目和前面的区别就是:限制只能完成2笔交易。因此将原来的二维dp变成三维dp,里面增加一个k表示交易的次数

dp[i][k][0]=Math.max(dp[i-1][k-1][1]-price[i],dp[i-1][k][0])


dp[i][k][1]=Math.max( dp[i-1][k][1],dp[i-1][k][0]+price[i] )

还是和前面的一样,第三位的1表示这一天之后是持有股票
如果第三位是0,表示这一天之后不持有股票
然后因为多了一维,所以初始化的代码也多了一些:

public int maxProfit(int[] prices) {
        if(prices==null||prices.length<1)
            return 0;
        int[][][] dp=new int[prices.length][3][2];//第一个2表示两次交易,第二个2表示两种状态,持有股票或者不持有股票
        dp[0][0][1]=0;//初始化
        dp[0][0][0]=Integer.MIN_VALUE;//这种情况不可能,没交易但是持有股票
        dp[0][1][0]=-prices[0];//交易一次,持有股票
        dp[0][1][1]=0;//交易一次,不持有股票
        dp[0][2][0]=-prices[0];//交易两次,持有股票
        dp[0][2][1]=0;//交易两次,不持有股票
        
        for(int i=1;i<prices.length;i++){
           // dp[i][0][1]=0;//按道理应该写,但是写和没写答案是一样的
           // dp[i][0][0]=Integer.MIN_VALUE;
            for(int j=1;j<=2;j++){
                dp[i][j][0]=Math.max( dp[i-1][j-1][1]-prices[i],dp[i-1][j][0]);
                dp[i][j][1]=Math.max( dp[i-1][j][1],dp[i-1][j][0]+prices[i] );
            }
        }
        return dp[prices.length-1][2][1];   

时间复杂度O(N*M),N表示天数,M表示限制的交易次数,这里为2。


买卖股票系列题目4、(困难)

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。leetcode188,链接


思路:这道题目的要求更进一步了,题目的变化是,买卖的次数是作为参数指定的k,而不是之前确定的。但是使用动态规划依然可以非常套路的解出来。
依然像上面一样初始化第一天的所有情况,然后双重循环更新所有dp元素。但是需要注意的是,如果k>price.length/2,其实就是随便交易几次的情况(因为price.length/2是最多的交易次数),因此可以重用前面不限制交易次数(也就是前面的系列题目2)的代码。

public int maxProfit(int k, int[] prices) {

        if(prices==null||prices.length<1)
            return 0;
        int[][][] dp=new int[prices.length][k+1][2];//第一个2表示两次交易,第二个2表示两种状态,持有股票或者不持有股票
        if(k>prices.length/2){
            return maxProfit1(prices);//不限制交易次数的代码
        }
        for(int i=0;i<=k;i++){//初始化第一天的数据
            dp[0][i][0]=-prices[0];
            dp[0][i][1]=0;
        }
        for(int i=1;i<prices.length;i++){
           // dp[i][0][1]=0;
           // dp[i][0][0]=Integer.MIN_VALUE;
            for(int j=1;j<=k;j++){
                dp[i][j][0]=Math.max( dp[i-1][j-1][1]-prices[i],dp[i-1][j][0]);
                dp[i][j][1]=Math.max( dp[i-1][j][1],dp[i-1][j][0]+prices[i] );
            }
        }
        return dp[prices.length-1][k][1];
    }

买卖股票系列题目5、(中等)

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

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

思路:这里的关键点就是冷冻期如何理解,其实很简单,冷冻期就是不能卖出的第二天立即买入股票,因此如果说买入是第i天,那么第i-2天必须得是不持有股票的。即状态传递方程式下面这样:

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

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

但是这里注意i-2一开始是越界的,i=1是1-2=-1,是越界的。所以使用一个变量保存i-2的值。

 public int maxProfit(int[] prices) {
        if(prices==null||prices.length<1)
            return 0;
        int[][] dp=new int[prices.length][2];
        dp[0][0]=-prices[0];//初始化
        dp[0][1]=0;
        int dpPre=0;
        for(int i=1;i<prices.length;i++){
            int tmp=dp[i-1][1];
            dp[i][0]=Math.max(dp[i-1][0],dpPre-prices[i]);
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
            dpPre=tmp;
        }
        return dp[prices.length-1][1];
        }

时间复杂度O(N)。


总结

关于股票这一系列的题目做下来感觉还是挺难的,但是一旦掌握了套路之后至少能确保做出来,虽然说计算效率其实不是很高,但是对于面试来说完全够用了,毕竟关键是能不能做出来,效率再高,现场想不起来,做不出来都是“扯淡”。

今天就这样吧。再会。

你可能感兴趣的:(算法)