【图解算法】经典而规整的动态规划——买卖股票的最佳时机

>_<

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

你要怎样交♂易才能获得最多的利润?

 
 

A. 不限买卖次数

一共两个状态【不持股】【持股
dp[i][0]的含义是 第i天不持股的最大资金
dp[i][1]的含义是 第i天持股的最大资金
 
这里的不持股[0],持股[1]表示是该天的状态而非动作——dp[i][0]的含义是第i天处于不持股的状态,而不是说第i天发生卖股票这个动作 —— 理解这句话是解决所有股票问题的关键
 
在这里插入图片描述

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        int len = prices.length;
        int[][] dp = new int[len][2];
        // 初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        // 状态转移
        for(int i = 1; i < len; 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[len - 1][0];
    }
}

 
 

B. 限制一次买卖

还是一共两个状态【不持股】【持股】
但注意,由于只能买卖一次,两种状态不能像上题那样随意转化——只能从【持股】转化为【不持股】,而不能反向转化
 
在这里插入图片描述

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        int len = prices.length;
        int[][] dp = new int[len][2];
        // 初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        // 状态转移
        for(int i = 1; i < len; 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], -prices[i]);
        }
        return dp[len - 1][0];
    }
}

 
 

C. 限制两次买卖

法一:多状态
 
由于买卖次数的限制,一共有5种状态:
dp[i][0]的状态是 什么都不做(固定为0)
dp[i][1]的状态是 第1次持股
dp[i][2]的状态是 第1次不持股
dp[i][3]的状态是 第2次持股
dp[i][4]的状态是 第2次不持股
 
这时的状态转移也不是第一个例题那样的两个状态双向转化,而是一个单向的过程
 
在这里插入图片描述

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        int len = prices.length;
        int[][] dp = new int[len][5];
        // 初始化
        dp[0][1] = -prices[0];
        dp[0][3] = -prices[0];
        // 状态转移
        for(int i = 1; i < len; 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]);
            dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
        }
        return dp[len - 1][4];
    }
}

法二:规整的三维动态规划
 
dp[i][j][1]的含义是:处于第i天,第j次交易,持股的状态
dp[i][j][0]的含义是:处于第i天,第j次交易,不持股的状态
 
从图中直观观察出:
(1) 不管是第几次交易(j=whatever),持股(k=1)初值取负,不持股(k=0)初值取0——初始化时会用到
(2) 当从不持股跳到持股状态时,所处的交易次数一定也跳了1;当从持股状态跳到不持股状态时,一定没有跳出此次交易中——写dp转移时会用到
 
在这里插入图片描述

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        int len = prices.length;
        int[][][] dp = new int[len][3][2];
        // 初始化
        dp[0][1][1] = -prices[0];
        dp[0][2][1] = -prices[0];
        // 状态转移
        for(int i = 1; i < len; i++) {
    
    
            for(int j = 1; j < 3; j++) {
    
    
                dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
                dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
            }
        }
        return dp[len - 1][2][0];
    }
}

 
 

扫描二维码关注公众号,回复: 12934508 查看本文章

D. 限制k次买卖

此时企图使用【多状态】的思路表示出所有状态是不可能的,因为交易的次数不能写死——本题只能使用三维dp
 
其实…这就是上一题的第二种写法。记得看上题的图理解

class Solution {
    
    
    public int maxProfit(int k, int[] prices) {
    
    
        int len = prices.length;
        int[][][] dp = new int[len][k + 1][2];
        // 初始化
        for(int j = 1; j < k + 1; j++) {
    
    
            dp[0][j][0] = 0;
            dp[0][j][1] = -prices[0];
        }
        // 状态转移
        for(int i = 1; i < len; i++) {
    
    
            for(int j = 1; j < k + 1; j++) {
    
    
                dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
                dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
            }
        }
        return dp[len - 1][k][0];
    }
}

 
 

E. 手续费

最简单母题的变式,在卖出股票时收取手续费即可

class Solution {
    
    
    public int maxProfit(int[] prices, int fee) {
    
    
        int len = prices.length;
        int[][] dp = new int[len][2];
        // 初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        // 状态转移
        for(int i = 1; i < len; 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[len - 1][0];
    }
}

 
 

F. 冷冻期

如果你真正理解了【多状态】解法,那么本题也很简单——一共3个状态
 
dp[i][0]表示 不持股(非冷冻期)
dp[i][1]表示 持股
dp[i][2]表示 不持股(冷冻期)
 
关键点在于(1)弄清楚这三种状态的转化关系(2)最后处于冷冻期/非冷冻期都有可能
 
在这里插入图片描述

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        int len = prices.length;
        int[][] dp = new int[len][3];
        // 初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        // 状态转移
        for(int i = 1; i < len; i++) {
    
    
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = dp[i - 1][1] + prices[i];
        }
        return Math.max(dp[len - 1][0], dp[len - 1][2]);
    }
}

 
 
 
 

理解与补充

  1. 想一想,为什么「不限制买卖次数」是2个状态的交叉转化,「限制2次买卖」是5个状态的单向转化呢?其实,「不限制买卖次数」的交叉转化可以看成无限个状态的单向转化(不做任何事,第一次1,第一次0,第二次1,第二次0,第三次1,第三次0…)
  2. 对于最简单的母题「不限制买卖次数」,还有一种更高效的思路—— 贪心。即利润的叠加,只要股票价格相对于前一天有所上涨,就累加上这个利润,最终结果即为全局最优的总利润int diff = prices[i] - prices[i - 1]; if(diff > 0) { res += diff; }
  3. 在「限制2次买卖」的代码中,返回值都是dp表最右下角的值,含义为【最后一天、第二次交易、不持股状态】,但是因为可能只交易了1次,最终结果也可能是【最后一天、第一次交易、不持股状态】呀?其实,在初始化时,【第一天、第一次交易、持股状态】【第一天、第二次交易、持股状态】的初始值都是-prices[0],而且第一、二次交易向下转化的代码是一样的——也就是说,当交易次数不满2次时,“第二次交易”本身没有意义,是对“第一次交易”的完全复制 ——所以我们放心地将dp右下角作为返回值
  4. 在多个变式中,都使用了 多状态 的方法,我们发现,找出所有状态往往不难,可找出状态的转化关系却不简单,对此有什么技巧呢?对于某一个状态,思考它可以由哪些状态转化而来
  5. 上面的dp都可以做滚动数组优化——直接在原先代码上删去一个维度,很简单,不再给出代码

 
 
 
 

 
 
 
 

 
 
 
 

 
 
 
 

E N D END END

B Y A L O L I C O N BY A LOLICON BYALOLICON

猜你喜欢

转载自blog.csdn.net/m0_46202073/article/details/112395067
今日推荐