动态规划精品课 2024.6.26-24.7.3

一、斐波那契数列模型

0、第N个泰波那契数

image.png

class Solution {
   
    public int tribonacci(int n) {
   
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回结果
        // 处理边界情况
        if (n == 0)
            return 0;

        if (n == 1 || n == 2)
            return 1;
        int[] dp = new int[n + 1];
        dp[0] = 0;
        dp[1] = dp[2] = 1;
        for (int i = 3; i <= n; i++)
            dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
        return dp[n];
    }
}
// 空间优化写法:
class Solution {
   
    public int tribonacci(int n) {
   
        // 可以递归写,也可以使用动态规划
        if (n == 0) return 0;
        if (n == 1) return 1;
        if (n == 2) return 1;
        int ret = 0;
        int a = 0, b = 1, c = 1;
        for(int i=0;i<n-2;i++){
   
            ret = a+b+c;
            a=b;
            b=c;
            c=ret;
        }
        return c;
    }
}
class Solution {
   
    public int tribonacci(int n) {
   
        // 可以递归写,也可以使用动态规划
        if (n == 0)
            return 0;
        if (n == 1)
            return 1;
        if (n == 2)
            return 1;
        return tribonacci(n-1)+tribonacci(n-2)+tribonacci(n-3);
    }
}

**状态表示:**dp[i]表示第i个泰波纳契数列的值。
**状态转移方程:**dp[i] = dp[i-1] + dp[i-2] + dp[i-3];
**初始化:**因为前三个值是固定的,所以直接给dp[0]、dp[1]、dp[2]赋值即可。
**填表顺序:**从左向右填表。
**返回结果:**dp[n]。

**思考:**动态规划的思想和递归有相似之处,都是通过规模更小的子问题来得出问题的答案
动态规划的步骤:

  1. 定义状态表示,创建dp表。(这个需要经验,具体问题具体分析)
  2. 初始化dp表,该题根据题目意思进行初始化
  3. 填表,需要注意填表顺序
  4. 返回结果,根据题目意思来判断,有时候是dp表的最后一个值,有时候是dp表求和或者最值。

1、三步问题

image.png

class Solution {
   
    public int waysToStep(int n) {
   
        if(n==1) return 1;
        if(n==2) return 2;
        if(n==3) return 4;
        int MOD = 1000000007;// 十亿
        int[] dp = new int[n+1];
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 4;
        for(int i=4;i<=n;i++){
   
            // 状态转移方程:
            dp[i] = ((dp[i-1]+dp[i-2])%MOD+dp[i-3])%MOD;
        }
        return dp[n];
    }
}

PS:因为两个数相加有可能大于MOD,所以要每两个数相加都进行模MOD
**状态表示:**dp[i]表示走到第i个台阶有dp[i]种上楼梯的方式
**状态转移方程:**dp[i] = ((dp[i-1]+dp[i-2])%MOD+dp[i-3])%MOD;
**初始化:**由题意可知,前几个值可以直接填写。
**填表顺序:**从左向右填表。
**返回结果:**dp[n]。

3、使用最小花费爬楼梯

image.png

class Solution {
   
    public int minCostClimbingStairs(int[] cost) {
   
        // 爬出数组操作是爬到楼梯顶
        int n = cost.length;
        // dp[i]:爬到第i层的最小花费,dp[n]就是爬到顶部的最小花费
        int[] dp = new int[n+1];
        // dp初始化
        dp[0]=0;
        dp[1]=0;
        for(int i=2;i<=n;i++){
   
            dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[n];
    }
}

分析问题 —> 状态转移方程 —> dp表创建以及初始化 —> 填写dp表
**状态表示:**dp[i]:爬到第i层的最小花费,dp[n]就是爬到顶部的最小花费
**状态转移方程:**dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);爬到第i个台阶有两种方式,去两种方式花费的最小值。
**初始化:**由题意可知,前几个值可以直接填写。
**填表顺序:**从左向右填表。
**返回结果:**dp[n]。

4、解码方法

image.png

class Solution {
   
    public int numDecodings(String s) {
   
        // 存在前导0时无法解码,直接返回0
        if(s.charAt(0)=='0') return 0;
        int n = s.length();
        // 0、定义状态表示,dp[i]表示以i位置为结尾的子串有多少种解码方式
        // 之所以数组大小定义为n+1,是为了填表的时候防止越界访问
        int[] dp = new int[n + 1];
        // dp[0]表示空串的解码方式有多少种,初始值应该为0或用0-1表示没有意义。但是为了后续填表的正确性dp[0]应该初始化为1.也就是说此处的初始化并没有实际意义,仅仅只是为了填表方便
        
        dp[0] = dp[1] = 1;
        for (int i = 2; i <= n; i++) {
   
            // 当前字符 cur,前置字符 pre
            int pre = s.charAt(i-2)-'0';
            int cur = s.charAt(i-1)-'0';
            // 如果pre和cur组成的数字可以解码
            if(10<=(pre*10+cur) && (pre*10+cur)<=26){
   
                dp[i] += dp[i - 2];
            }
            // 单独的cur进行解码
            if(1<=cur&&cur<=9){
   
                dp[i] += dp[i - 1];
            }
            // 非法情况直接返回0
            if(cur==0 && (pre==0||pre>2)){
   
                return 0;
            }
        }
        return dp[n];
    }
}

这个课是听过一遍的,写起来还算有点记忆。
**状态表示:**dp[i]表示以i位置为结尾的子串有多少种解码方式。
**初始化:**dp[0]表示空串的解码方式有多少种,初始值应该为0或用0-1表示没有意义。但是为了后续填表的正确性dp[0]应该初始化为1.也就是说此处的初始化并没有实际意义,仅仅只是为了填表方便。
**填表顺序:**从左向右
**返回值:**dp[n].

二、路径问题

5、不同路径

image.png

class Solution {
   
    public int uniquePaths(int m, int n) {
   
        // 这是一个排列组合的问题,有公式可以直接秒。
        // 动态规划解法:
        int[][] dp = new int[m+1][n+1];
        // 为什么要这么进行初始化?
        dp[0][1] = 1;
        // 填表顺序:从上到小,从左到右
        for(int i=1;i<=m;i++){
   
            for(int j=1;j<=n;j++){
   
                // 想要走到[i,j]位置,有两种方式,有x中方式走到[i-1,j],有y种方式走到[i,j-1]
                // 那么走到[i,j]的方式就有x+y种。
                dp[i][j] = dp[i-1][j]+dp[i][j-1];
            }
        }
        return dp[m][n];

    }
}

由题意可知,初始化应为:dp[i][1]=dp[1][j]=1。为了编码简单我们创建的dp表比原数组长度大1,
dp[i][1]和dp[1][j]都应该等于1,dp[1][1] = dp[1][0] + dp[0][1];所以仅需将dp[1][0]或者dp[0][1]设置为1即可。

动态规划的核心就是:

  1. 明确dp表的含义;
  2. 确定状态转移方程

细节问题:

  1. dp表是一维的还是二维的?
  2. dp表的大小(一般是要比原数组长度大1)
  3. 如何初始化

初始化的几种情况:1、具有实际意义。2、编码需要,为填表工作服务。

6、不同路径II

image.png

class Solution {
   
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
   

        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;

        int[][] dp = new int[m+1][n+1];
        // 为什么要这么进行初始化?为了dp[1][1]位置填表的正确性。!!
        dp[0][1] = 1;
        for(int i=1;i<=m;i++){
   
            for(int j=1;j<=n;j++){
   
                if(obstacleGrid[i-1][j-1]==1){
   
                    dp[i][j] = 0;
                }else{
   
                    dp[i][j] = dp[i-1][j]+dp[i][j-1];
                }
            }
        }
        return dp[m][n];
    }
}

路径上存在障碍物,多加一层判断即可。
为什么要初始化dp[0][1]=1?为了dp[1][1]位置填表的正确性。

7、珠宝的最高价格

image.png

class Solution {
   
    public int jewelleryValue(int[][] frame) {
   
        // 从左上角到右下角的路径和最大值
        int m = frame.length;
        int n = frame[0].length;
        int[][] dp = new int[m+1][n+1];
        for(int i=1;i<=m;i++){
   
            for(int j=1;j<=n;j++){
   
                // 填表规则,两种方式取较大的哪一种
                dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])+frame[i-1][j-1];
            }
        }
        return dp[m][n];
    }
}

dp[i][j]表示从起点到达[i,j]位置珠宝总价值的最大值。
所以有状态转移方程:dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])+frame[i-1][j-1];

8、下降路径最小和

image.png

class Solution {
   
    public int minFallingPathSum(int[][] matrix) {
   
        int m = matrix.length;
        int n = matrix[0].length;
        // 0、如果建一个更大的dp表,在填充dp表的时候就不需要考虑数组越界的问题了
        int[][] dp = new int[m+1][n+2];
        // 1、初始化,周围一圈需要初始化为无穷大
        for(int i=1;i<m+1;i++){
   
            dp[i][0]=Integer.MAX_VALUE;
            dp[i][n+1]=Integer.MAX_VALUE;
        }
        // 2、填写dp表 
        for(int i=1;i<=m;i++){
   
            for(int j=1;j<=n;j++){
   
                int min = Math.min(dp[i-1][j-1],dp[i-1][j]);
                min = Math.min(min,dp[i-1][j+1]);
                dp[i][j] = matrix[i-1][j-1] + min;
            }
        }
        // 3、找最后一行的最小值
        int min = dp[m][n];
        for(int i=1;i<=n-1;i++){
   
            min = Math.min(min,dp[m][i]);
        }
        return min;  
    }
}

图示:

image.png

**状态表示:**dp[i][j]表示下降到[i,j]位置时的最小值,因此返回值应该是最下面一行的最小值
**初始化:**dp建的比较大是为了填表时的越界访问,但是数组的默认值会干扰填表,根据题目意思将数组的第一列和最后一列设置为一个大数即可。
**填表顺序:**从上到下,同一行中填顺序无所谓
**返回值:**最后一行的最小值。

9、最小路径和

image.png

class Solution {
   
    public int minPathSum(int[][] grid) {
   
        // 从左上角到右下角的路径和最大值
        int m = grid.length;
        int n = grid[0].length;
        // dp表建的比较大是为了防止数组越界,省去判断下标合法。
        int[][] dp = new int[m+1][n+1];
        // 为了防止dp边界值影响dp的填充,所以要做初始化
        
        for(int i=0;i<=m;i++) dp[i][0] = Integer.MAX_VALUE;
        for(int i=0;i<=n;i++) dp[0][i] = Integer.MAX_VALUE;
        dp[0][1] = dp[1][0] = 0;
        for(int i=1;i<=m;i++){
   
            for(int j=1;j<=n;j++){
   
                dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])+grid[i-1][j-1];
            }
        }
        return dp[m][n];
    }
}

图示:

image.png

dp表创建的大一点是为了避免填表时越界访问,
为了防止dp边界值影响dp的填充,所以要做初始化

10、地下城游戏 ★★★★★★

image.png

class Solution {
   
    public int calculateMinimumHP(int[][] dungeon) {
   
        int m = dungeon.length;
        int n = dungeon[0].length;

        // 0、创建dp表
        int[][] dp = new int[m+1][n+1];

        // 1、初始化dp表
        for(int i=0;i<m;i++)
            dp[i][n] = Integer.MAX_VALUE;
        for(int i=0;i<n;i++){
   
            dp[m][i] = Integer.MAX_VALUE;
        }
        dp[m-1][n] = dp[m][n-1] = 1;

        // 2、填充dp表,正难则反
        for(int i=m-1;i>=0;i--){
   
            for(int j=n-1;j>=0;j--){
   
                // 从dp[i][j]去往dp[i+1][j]或者dp[i][j+1],选择两者较小的哪一个
                int min = Math.min(dp[i+1][j],dp[i][j+1]);
                // 如果该房间可以增加健康值,且dungeon[i][j]>=min,
                // 那么从进入这个房间并到达终点所需要的能量为1,因为保持能量为正才能进入房间
                if(dungeon[i][j]>=min){
   
                    dp[i][j] = 1;
                }else{
   
                    dp[i][j] = min - dungeon[i][j];
                }
            }
        }
        return dp[0][0];
    }
}
/*
地下城数组:
-2  -3  3
-5 -10  1
10  30 -5

dp表:从后往前填写
7  5  2 ∞
6  11 5 ∞
1  1  6 1
∞  ∞  1 0
*/

正难则反:
dp[i][j]表示:进入[i,j]房间并走出终点所需要的最低健康值,可见健康值最低为1,否则进不去[i,j]房间。健康值为0代表勇士死亡,所以勇士走出终点至少要保持健康值为1.

/*
地下城数组:
-2  -3  3
-5 -10  1
10  30 -5

填充后的dp表:从后往前填写
7  5  2 ∞
6  11 5 ∞
1  1  6 1
∞  ∞  1
*/

三、简单多状态dp问题

11、按摩师

image.png

class Solution {
   
    public int massage(int[] nums) {
   
        int n = nums.length;
        if(n==0) return 0;
        if(n==1) return nums[0];
        int[] dp = new int[n+1];
        dp[0] = 0;
        dp[1] = nums[0];

        for(int i=2;i<=n;i++){
   
            int max = dp[i-2];
            for(int j=0;j<i-2;j++){
   
                max=Math.max(max,dp[j]);
            }
            dp[i] = nums[i-1] + max;
        }
        return dp[n]>dp[n-1]?dp[n]:dp[n-1];
    }
}

dp[i]:代表以nums[i]为结尾的预约序列的最长预约时间。时间复杂度O(N^2)。为了填写当前dp值所需要遍历之前所有写过的dp值。
状态转移方程:dp[i] = nums[i-1] + max;

class Solution {
   
    public int massage(int[] nums) {
   
        int n = nums.length;
        if(n==0) return 0;
        if(n==1) return nums[0];
        int[] dp = new int[n+1];
        dp[0] = 0;
        dp[1] = nums[0];
        for(int i=2;i<=n;i++){
   
            dp[i] = Math.max((nums[i-1] + dp[i-2]),dp[i-1]);
        }
        return dp[n]>dp[n-1]?dp[n]:dp[n-1];
    }
}

dp[i]:代表从nums[0]到nums[i-1]的最长预约时间。时间复杂度O(N)
状态转移方程:dp[i] = Math.max((nums[i-1] + dp[i-2]),dp[i-1]);
dp[i]代表从nums[0]到nums[i-1]的最长预约时间,所以dp[i]有两种状态:选择了第i个预约或者**没选择第i个预约,**dp[i]要选择两种状态下的最大值。


> 本体给出了两种动态规划的解法:第一种是单状态的,也就是dp[i]代表选择第i个预约。 > 第二种是多状态的状态表示,dp[i]表示前i个预约复合题意的最大预约时长,首先这个状态表示是比原问题规模更小的子问题,其次dp[i]有两个状态,那就是最有一个预约有没有被选择两种状态。这两种状态时在填表使用到了,当然也可以定义两个dp表: > p[i]:选择了i位置预约的最大时长。p[i] = q[i-1]+nums[i]; > q[i]:没有选择i位置预约的最大时长。q[i] = p[i-1];
class Solution {
   
    public int massage(int[] nums) {
   
        int n = nums.length;
        if(n==0) return 0;
        int[] p = new int[n];
        int[] q = new int[n];
        p[0] = nums[0];
        q[0] = 0;
        for(int i=1;i<n;i++){
   
            p[i] = q[i-1]+nums[i];
            q[i] = Math.max(p[i-1],q[i-1]);
        }
        return Math.max(p[n-1],q[n-1]);
    }
}

// 大师,我悟了
// 对于多状态的题目就是为每一个状态都创建一个dp表,不过这题的两个dp表可以合成一个,
// 应该没那么好合并,只是这题巧了

12、打家劫舍II

image.png

class Solution {
   
    public int rob(int[] nums) {
   
        int n = nums.length;
        if(n==1) return nums[0];
        // 0、忽略最后一家进行打家劫舍
        int[] dp = new int[n];
        dp[0] = 0;
        dp[1] = nums[0];
        for(int i=2;i<n;i++){
   
            dp[i] = Math.max((nums[i-1] + dp[i-2]),dp[i-1]);
        }
        // 1、忽略第一家进行打家劫舍
        int[] dp1 = new int[n+1];
        dp1[0] = 0;
        dp1[1] = 0;
        dp1[2] = nums[1];
        for(int i=3;i<=n;i++){
   
            dp1[i] = Math.max((nums[i-1] + dp1[i-2]),dp1[i-1]);
        }
        // 2、 	返回两个dp表最后一个值的较大值
        return dp[n-1]>dp1[n]?dp[n-1]:dp1[n];
    }
}

核心思路:转化成两个线性的"打家劫舍"(按摩师问题)
核心思路:转化成两个线性的多状态dp问题

13、删除并获得点数 ★★★★★★

image.png

这题也是打家劫舍问题:

class Solution {
   
    public int deleteAndEarn(int[] nums) {
   
        int n = 10001;
        // 1. 预处理
        int[] arr = new int[n];
        for (int x : nums)
            arr[x] += x;
        // 2. dp
        // 创建 dp 表
        // f[i]:选择了第i个数时子问题的最大点数
        // g[i]:没选择第i个数时子问题的最大点数
        int[] f = new int[n];
        int[] g = new int[n];

        // 初始化
        f[0] = arr[0];// f[0]代表选择了nums[0],所以初始化为arr[0];
        g[0] = 0;// g[0]代表没选择nums[0],所以初始化为0;
        // 填表
        for (int i = 1; i < n; i++) {
   
            f[i] = g[i - 1] + arr[i];
            g[i] = Math.max(f[i - 1], g[i - 1]);
        }
        // 返回值
        return Math.max(f[n - 1], g[n - 1]);
    }
}

题目限制nums[i]的取值范围,所以很轻松的将该问题转化为了打家劫舍问题,不然没那么轻松!!!

  • 1 <= nums[i] <= 104

经验:有时候需要将问题进行预处理或者转化才能变成常规的动态规划问题

14、粉刷房子

image.png

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

正确性在那里???,这题有贪心

image.png
image.png

多状态:i号房子粉刷的颜色,一共有三种颜色可选,那么就有三个状态表示
创建三个dp表,三张表相互依赖同步填写。

15、买卖股票的最佳时期含冷冻期

image.png
image.png
image.png

// 第i天处于买入状态有两种可能,此时的收益去两种可能的最大值
dp[0][i]=Math.max(dp[0][i-1],dp[1][i-1]-prices[i]);
// 第i天处于可交易状态有两种可能,此时的收益去两种可能的最大值
dp[1][i]=Math.max(dp[1][i-1],dp[2][i-1]);
// 第i天处于冷冻期状态只有一种可能,及前一天是买入状态并抛售股票
dp[2][i]=dp[0][i-1]+prices[i];

image.png
image.png

class Solution {
   
    public int maxProfit(int[
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值