LeetCode动态规划(四)之打家劫舍&买卖股票

前言

对于动态规划来说,有两个比较有意思的专题,就是打家劫舍和买卖股票,也可以说是经典问题

打家劫舍

1.lc198 打家劫舍

lc198链接

描述:

如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例:

输入:[2,7,9,3,1]
输出:12

Solution:
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]
递推公式的精髓在于,站在某一个房子处考虑你是偷还是偷,如果偷,那前一个房子就不能被考虑;如果偷,那金额的最大就来自于偷前一个房屋积累的。

public int rob(int[] nums) {
        int[] dp = new int[nums.length];

        if(nums.length==1) return nums[0];

        dp[0] = nums[0];
        dp[1] = Math.max(nums[0],nums[1]);

        for(int i=2;i<nums.length;i++)
        {
            dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1]); 
        }
        return dp[nums.length-1];
    }

2.lc213 打家劫舍II

lc213 链接

描述:
在上一题基础上增加约束 房屋围成一圈
示例:
输入:nums = [2,3,2] 输出:3(不能2+2,因为是相邻的)

Solution:
把这个序列拆成两段,第一段不包括尾元素,第二段不包括首元素

 public int rob(int[] nums) {
        if(nums.length==1){
        return nums[0];
    }
    int [] dp1 = new int[nums.length];
    int [] dp2 = new int[nums.length];
    dp1[0] = nums[0];
    dp1[1] = nums[0];
    dp2[1] = nums[1];
    for(int i =2; i<nums.length; i++){
        dp1[i] = Math.max(dp1[i-2]+nums[i],dp1[i-1]);
        dp2[i] = Math.max(dp2[i-2]+nums[i],dp2[i-1]);
       }
  return Math.max(dp1[nums.length-2],dp2[nums.length-1]);
  }

3.lc337 打家劫舍 III

lc337链接

描述:二叉树偷窃
示例:
输入: root = [3,2,3,null,3,null,1] 输出: 7

Solution:

买卖股票

0.模板套路

买卖股票一共有6题,可以用一个统一的框架去做(https://labuladong.github.io)

整个买卖股票涉及的【状态】最多有三个(所以根据题意要不要用三维dp)

  • 第一个是天数
  • 第二个是允许交易的最大次数
  • 第三个是当前的持有状态(可以设 1 表示持有,0 表示没有持有)
dp[i][k][0 or 1] 0 <= i <= n - 1, 1 <= k <= K
n 为天数,大 K 为交易数的上限,01 代表是否持有股票。
此问题共 n × K × 2 种状态,全部穷举就能搞定。
遍历:
for 0 <= i < n:
     for 1 <= k <= K:
        for s in {0, 1}:
            dp[i][k][s] = max(buy, sell, rest)

举个例子:比如说 dp[3][2][1] 的含义就是:今天是第三天,我现在手上持有着股票,至今最多进行 2 次交易
最后要求的答案是:dp[n - 1][K][0](即第n天,交易K次,卖出状态下的利润)

然后状态转移方程分为两步,分别是此时不持有股票的最大利润怎么推导,和此时持有股票的最大利润怎么推导来。

// 今天没有持有= 昨天没有持有+昨天持有(今天卖了,所以利润增加)
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
// 今天持有= 昨天持有+ 昨天没持有(今天买了,利润减少,同时交易次数减1)
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

// base case 情况
dp[0][……][0] = 0 (第一天没持有,利润为0)
dp[0][……][1] = -prices[0] (一开始就持有,所以第一天就买入,所以利润为负)
dp[……][0][0] = 0 (交易次数是0次,即不能交易,所以利润为0//dp[...][0][1] = -infinity

1.lc121 买卖股票的最佳时机

lc121链接

描述:

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

示例:

输入:[7,1,5,3,6,4]  输出:5

Solution:
利用上面的统一框架,这里没有限制交易次数,所以用一个二维dp就可以了

 public int maxProfit (int[] prices) {
        // write code here
        int[][] dp = new int[prices.length][2];
         // dp初始化
         dp[0][1] = -prices[0];
         dp[0][0] = 0;

         for(int i=1;i<prices.length;i++)
         {
             // 0表示未持有
             dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
             // 1表示持有(注意这里如果前一天未持有则利润肯定为0)
             dp[i][1] = Math.max(-prices[i],dp[i-1][1]);
            
         }
         return dp[prices.length-1][0];
    }
 }

2.lc122 买卖股票的最佳时机II

lc22链接

相比较上题,不限制交易次数(即k为正无穷)

示例:

输入:prices = [7,1,5,3,6,4]
输出:7(5-1+6-3)

Solution:
按照统一模板,有k这个状态,但是此时的k又是无穷状态(所以k与k-1可以看成一样的,也就是可以不用三维dp)

与上一题相比只需要修改状态转移方程

// 0表示未持有
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
// 1表示持有(这里相比于上题的区别是第i天持有,第i-1天未持有但是可以有利润)
dp[i][1] = Math.max(dp[i-1][0]-prices[i],dp[i-1][1]);

3.lc714 买卖股票的最佳时机含手续费

lc714链接

描述:

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

示例:
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8

Solution:
与上题相似的思路,k依然是无穷,只不过多了个手续费。只需稍微修改状态转移和初始化状态:

 dp[0][1] = -prices[0]-fee;
 dp[0][0] = 0;

  for(int i=1;i<prices.length;i++)
  {
      // 0表示未持有
      dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
      // 1表示持有
      dp[i][1] = Math.max(dp[i-1][0]-prices[i]-fee,dp[i-1][1]);
  }

4.lc309. 最佳买卖股票时机含冷冻期

lc309链接

描述:

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

示例:

输入: prices = [1,2,3,0,2]
输出: 3

Solution:
与前两题相比,k依然是无穷,但多了个冷冻期,冷冻期为1天。 所以第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。

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

       for(int i=2;i<prices.length;i++)
       {
           // 0表示未持有
           dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
           // 1表示持有
           dp[i][1] = Math.max(dp[i-2][0]-prices[i],dp[i-1][1]);
          
       }
       return dp[prices.length-1][0];
}

5. lc123 买卖股票的最佳时机 III

lc123链接

描述:

你最多可以完成 两笔 交易

示例:

输入:prices = [3,3,5,0,0,3,1,4]
输出:6

Solution1:
k=2的情况,引入3维dp。因为k比较小,可以枚举状态

 public int maxProfit(int[] prices) {
     int len = prices.length;
     // dp[i][k][s]
     int[][][] dp = new int[len][3][2];
     dp[0][1][0] = 0;
     dp[0][1][1] = -prices[0];
     dp[0][2][0] = 0;
     dp[0][2][1] = -prices[0];

     for(int i=1;i<len;i++)
     {
         // 今天没有持有= 昨天没有持有+昨天持有(今天卖了,所以利润增加)
         // dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
         // 今天持有= 昨天持有+ 昨天没持有(今天买了,利润减少,同时交易次数减1)
         // dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

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

    }

Solution2:
不仅穷举天数状态,还要枚举交易次数状态。所以两个for循环

在这里插入代码片

6. lc188 买卖股票的最佳时机 IV

lc188链接

至多k笔交易
输入:k = 2, prices = [2,4,1]
输出:2

Solution:
大boss,三种状态都要穷举,参考上面的解法2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值