动态规划——简单多状态 dp 问题

1. 按摩师

题目链接:面试题 17.16. 按摩师

思路一:单一状态表示

题目解析:
在数组nums中,可以选择从0位置或者1位置开始,每次可选择跳2步或者3步,最后结束点在数组倒数第一个或者倒数第二个

算法流程:

  1. 状态表示:dp[i]表示到达i位置时,预约的最大时间
  2. 状态转移方程:dp[i] = max(dp[i-2][i-3]) + nums[i]
  3. 初始化:dp表在前面多创建3个虚拟节点,都初始化为0,防止数组越界,注意这时nums下标与dp表下标的映射关系改变,因此,状态方程为dp[i] = max(dp[i-2][i-3]) + nums[i-3]
  4. 填表顺序:从左往右
  5. 返回值:max(dp[m+1], dp[m+2])

实现代码

class Solution {
    public int massage(int[] nums) {
        //1.创建dp表
        int m = nums.length;
        int[] dp = new int[m+3];
    
        //2.初始化
        dp[0] = dp[1] = dp[2] = 0;

        //3.填表
        for(int i = 3; i < m+3; i++) {
            dp[i] = Math.max(dp[i-2], dp[i-3]) + nums[i-3];
        }

        //4.返回值
        return Math.max(dp[m+2], dp[m+1]);
    }
}

思路二:多状态表示

算法流程:

  1. 状态表示:思路一中dp[i]表示到达i位置时,预约的最大时间。其实,这个状态还可以细化为两个状态:选择i位置的值,不选择i位置的值。因此,f[i]表示到达i位置时,选择nums[i],此时的最长预约时长;g[i]表示到达i位置时,不选择nums[i],此时的最长预约时长
  2. 状态转移方程:对于f[i],到达i位置选择nums[i],因此i-1位置必没有选(也就是g[i-1]),所以f[i] = g[i-1] + nums[i];对于g[i],到达i位置不选择nums[i],因此i-1位置可选(即f[i-1]),也可不选(即g[i-1]),取两者的最大值,即g[i] = max(f[i-1], g[i-1])
  3. 初始化:f[0] = nums[0],g[0] = 0
  4. 填表顺序:从左往右
  5. 返回值:max(f[n-1], g[n-1])

实现代码:

class Solution {
    public int massage(int[] nums) {
        //1.创建dp表
        //2.初始化
        //3.填表
        //4.返回值
        int n = nums.length;
        int[] f = new int[n];
        int[] g = new int[n];

        if(n == 0) return 0;//处理边界情况
        f[0] = nums[0];
        for(int i = 1; i < n; i++) {
            f[i] = g[i-1] + nums[i];
            g[i] = Math.max(f[i-1], g[i-1]);
        }

        return Math.max(f[n-1], g[n-1]);
    }
}

2. 打家劫舍II

题目链接:213. 打家劫舍 II

解题思路:
上一道题是单排的模式,这道题是环形的模式,即首尾相连的。但是我们可以把这个环形问题转换为两个单排问题

  • 偷[0, n-2]区间的房子
  • 偷[1, n-1]区间的房子

最后返回两个单排问题所得结果的最大值即可,每个单排问题和上一个问题是一样的

实现代码:

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n == 1) return nums[0];//处理边界情况
        return Math.max(rob1(nums, 0, n - 2), rob1(nums, 1, n - 1));
    }
    private int rob1(int[] nums, int left, int right) {
        //1.创建dp表
        //2.初始化
        //3.填表
        //4.返回值
        int n = nums.length;
        int[] f = new int[n];
        int[] g = new int[n];

        f[left] = nums[left];
        for(int i = left + 1; i <= right; i++) {
            f[i] = g[i-1] + nums[i];
            g[i] = Math.max(g[i-1], f[i-1]);
        }
        return Math.max(f[right], g[right]);
    }
}

3. 删除并获得点数

题目链接: 740. 删除并获得点数

算法思路:
注意题目描述,选择数字x后,就不能选择x+1 和 x-1了,这是不是很像打家劫舍问题,选择了i位置的金额,就不能选择i+1和i-1位置的金额了。因此,我们可以试着把这道题的解题方式往打家劫舍去靠

对于[1,2,3,4,5,6]这种连续且数字唯一的序列,数字值可以直接对应于打家劫舍的位置值,可以直接使用打家劫舍去做

对于[2,4,5,6,1]这种不连续或乱序的序列,就不能直接去用打家劫舍去做了,我们可以去创建一个数组[0,1,2,0,4,5,6],使其数字值对应于打家劫舍的位置值,原数组中不存在的数字值用0代替,再去用打家劫舍去做

对于[1,1,2,2,3,4,5,5,5,6]这种每个数字具有多个的情况,又该怎么办呢?当我们需要选择一个数字时,不如把所有的数字全部选上,将他们的和放入arr数组中该数字下标的位置,即[0,2,4,3,4,15,6],再去用打家劫舍去做

实现代码:

class Solution {
    public int deleteAndEarn(int[] nums) {
        //1. 预处理
        int n = 10001;
        int[] arr = new int[n];
        for(int x : nums) arr[x] += x;

        //2.创建dp表
        int[] f = new int[n];
        int[] g = new int[n];

        //3.初始化
        f[0] = arr[0];

        //4.填表
        for(int i = 1; i < n; i++) {
            f[i] = g[i-1] + arr[i];
            g[i] = Math.max(f[i-1], g[i-1]);
        }

        //5.返回值
        return Math.max(f[n-1], g[n-1]);
    }
}

注: 本来是要根据nums中的最大值去创建arr数组的大小,但是题目中给出了nums数组里元素的取值范围,于是就取10001作为arr数组的大小,这样就不需要将数组nums排序,找出最大值了,也是一种解题的小技巧

4. 粉刷房子

题目链接:LCR 091. 粉刷房子

算法流程:

  1. 状态表示:创建一个二维dp表,dp[i][0],dp[i][1],dp[i][2]分别表示粉刷到第i个房子时,将其粉刷成红色,蓝色,绿色时,此时的最小花费
  2. 状态转移方程:dp[i][0] = Math.min(dp[i-1][1], dp[i-1][2]) + costs[i-1][0];
    dp[i][1] = Math.min(dp[i-1][0], dp[i-1][2]) + costs[i-1][1];
    dp[i][2] = Math.min(dp[i-1][0], dp[i-1][1]) + costs[i-1][2];
  3. 初始化:创建表时,可以多创建一列(第0列),初始化为0
  4. 填表顺序:三个表同时从左往右填
  5. 返回值:返回三个表的最后一个节点的最小值,即dp[n][0],dp[n][1],dp[n][2]三者的最小值

实现代码:

class Solution {
    public int minCost(int[][] costs) {
        //1. 建表
        //2.初始化
        //3.填表
        //4.返回值
        int n = costs.length;
        int[][] dp = new int[n+1][3];

        for(int i = 1; i < n+1; i++) {
            dp[i][0] = Math.min(dp[i-1][1], dp[i-1][2]) + costs[i-1][0];
            dp[i][1] = Math.min(dp[i-1][0], dp[i-1][2]) + costs[i-1][1];
            dp[i][2] = Math.min(dp[i-1][0], dp[i-1][1]) + costs[i-1][2];
        }

        return Math.min(Math.min(dp[n][0], dp[n][1]), dp[n][2]);
    }
}

5.买卖股票的最佳时机含冷冻期

题目链接:309. 买卖股票的最佳时机含冷冻期

算法流程:

  1. 状态表示:dp[i]表示当第i天结束时,所得的最大利润。而dp可以划分为几个状态:(一)第i天结束时,处于手中有股票的状态,即后面一天可进行卖出操作。(二)第i天结束时,处于冷冻期的状态即当天卖出股票,后一天不能进行买入操作。(三)第i天结束时,处于手中无股票的状态,即后面一天可进行买入操作。注意:手中有股票的状态到达手中无股票的状态,必须经过冷冻期状态
    在这里插入图片描述
    因此,用dp[i][0]表示第i天结束后,处于手中有股票的状态,所得的最大利润,dp[i][1]表示第i天结束后,处于手中无股票的状态,所得的最大利润,dp[i][2]表示第i天结束后,处于冷冻期的状态,所得的最大利润
  2. 状态转移方程:
  • dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - price[i])
  • dp[i][1] = max(dp[i - 1][1], dp[i-1][2])
  • dp[i][2] = dp[i-1][0] + price[i]
  1. 初始化:
  • dp[0][0]:第1天结束后,手中有股票,因此第一天买入了股票,因此利润为-price[0]
  • dp[0][1]:第1天结束后,手中还是没有股票,因此第一天什么都没干,因此利润为0
  • dp[0][2]:第1天结束后,处于冷冻期,因此第一天买入后又卖出了,因此利润为0
  1. 填表顺序:三个表同时从左往右填
  2. 返回值:最大利润不可能是最后一天结束后手里还有股票,因此最后返回max(dp[n-1][1], dp[n-1][2])。(返回三者的最大值也是可以的)

实现代码:

class Solution {
    public int maxProfit(int[] prices) {
        //1.创建dp表
        //2.初始化
        //3.填表
        //4.返回值

        int n =prices.length;
        int[][] dp = new int[n][3];
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        dp[0][2] = 0;
        for(int i = 1; i < n; 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][2]);
            dp[i][2] = dp[i-1][0] + prices[i];
        }
        return Math.max(dp[n-1][1], dp[n-1][2]);
    }
}

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

题目链接:714. 买卖股票的最佳时机含手续费

算法流程:

  1. 状态表示:p[i][0]表示第i天结束后,处于手中有股票的状态,所得的最大利润;dp[i][1]表示第i天结束后,处于手中无股票的状态,所得的最大利润

画出状态机
在这里插入图片描述
我们不妨将买入股票并卖出股票要收取的手续费,放在买入时收取一次,当卖出时不收取即可

  1. 状态转移方程:dp[i][0] = max(dp[i-1][0], dp[i-1][1] - price[i] - fee); dp[i][1] = max(dp[i][0] + price[i], dp[i][1])
  2. 初始化:dp[0][0] = -price[0] - fee; dp[0][1] = 0;
  3. 填表顺序:两张表同时从左往右填
  4. 返回值:dp[n-1][1]

实现代码:

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int n = prices.length;
        int[][] dp = new int[n][2];
        dp[0][0] = -prices[0]-fee;
        for(int i = 1; i < n; 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][0]+prices[i], dp[i-1][1]);
        }
        return dp[n-1][1];
    }
}

7. 买卖股票的最佳时机 III

题目链接:123. 买卖股票的最佳时机 III

算法流程:

  1. 状态表示:f[i][j]表示第i天结束后,完成了j笔交易,这时手中有股票,此时的最大利润;g[i][j]表示第i天结束后,完成了j笔交易,这时手中无股票,此时的最大利润

画出状态机:
在这里插入图片描述

  1. 状态转移方程:f[i][j] = max(f[i-1][j], g[i-1][j]-price[i]); g[i][j] = max(g[i-1][j], f[i-1][j-1]+price[i])

  2. 初始化:需要初始化f的第一行和第一列,初始化g的第一行
    对于f,除了可以多创建一行和一列外,还有一个技巧:只初始化第一行,当j-1存在时,才进行状态转移方程计算
    在这里插入图片描述
    但是,如果将负无穷设置为int类型的最小值时,g[i-1][j]-price[i]会超出int类型的范围,从而变成一个很大的一个值,即溢出,因此我们可以规定无穷大为0x3f3f3f3f,正好是int类型最大值的一半,足够小即可

  3. 填表顺序:从左往右,从上往下,两张表同时填

  4. 返回值:g[n-1][0],g[n-1][1],g[n-1][2]三者的最大值

代码实现:

class Solution {
    public int maxProfit(int[] prices) {
        //1.创建dp表
        //2.初始化
        //3.填表
        //4.返回值
        int n = prices.length;
        int[][] f = new int[n][3];
        int[][] g = new int[n][3];

        int INF = 0x3f3f3f3f; //无穷大
        for(int j = 0; j < 3; j++) f[0][j] = g[0][j] = -INF;
        f[0][0] = -prices[0];
        g[0][0] = 0;

        for(int i = 1; i < n; i++) {
            for(int j = 0; j < 3; j++) {
                f[i][j] = Math.max(f[i-1][j], g[i-1][j] - prices[i]);
                g[i][j] = g[i-1][j];
                if(j-1 >= 0) {
                    g[i][j] = Math.max(g[i-1][j], f[i-1][j-1] + prices[i]);
                }
            }
        }

        return Math.max(Math.max(g[n-1][0], g[n-1][1]), g[n-1][2]);
    }
}

8. 买卖股票的最佳时机 IV

思路和上题是一致的,故不再赘述

代码如下:

class Solution {
    public int maxProfit(int k, int[] prices) {
        //1. 创建dp表
        //2. 初始化
        //3. 填表
        //4. 返回值
        int INF = 0x3f3f3f3f;
        int n = prices.length;
        int[][] f = new int[n][k+1];
        int[][] g = new int[n][k+1];
        for(int j = 0; j <= k; j++) f[0][j] = g[0][j] = -INF;
        f[0][0] = -prices[0];
        g[0][0] = 0;

        for(int i = 1; i < n; i++) {
            for(int j = 0; j <= k; j++) {
                f[i][j] = Math.max(f[i-1][j], g[i-1][j]-prices[i]);
                g[i][j] = g[i-1][j];
                if(j-1 >= 0) g[i][j] = Math.max(g[i-1][j], f[i-1][j-1] + prices[i]);
            }
        }
        
        int ret = 0;
        for(int j = 0; j <= k; j++) {
            if(ret < g[n-1][j]) ret = g[n-1][j];
        }

        return ret;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值