代码随想录 动态规划-基础题目

这篇具有很好参考价值的文章主要介绍了代码随想录 动态规划-基础题目。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

509.斐波那契数 

70.爬楼梯

746.使用最小花费爬楼梯

62.不同路径

63.不同路径||

343.整数拆分

96.不同的二叉树


 

509.斐波那契数 

509. 斐波那契数

简单

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

示例 1:

输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3:

输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

提示:

  • 0 <= n <= 30

确定dp数组以及下标的含义

dp[i]的定义为:第i个数的斐波那契数值是dp[i]

确定递推公式

状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];

dp数组如何初始化

dp[0] = 0;
dp[1] = 1;

确定遍历顺序

从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的

举例推导dp数组

按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:

0 1 1 2 3 5 8 13 21 34 55

如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的。

// 定义一个名为Solution的类  
class Solution {  
    // 定义一个公共方法fib,用于计算斐波那契数列的第n个数  
    public int fib(int n) {  
        // 如果n小于2,直接返回n。这是斐波那契数列的基础情况,即f(0) = 0, f(1) = 1  
        if(n < 2){  
            return n;  
        }  
          
        // 定义一个数组dp,长度为n+1,用于存储斐波那契数列的值  
        int dp[] = new int[n + 1];  
          
        // 初始化斐波那契数列的前两个数  
        dp[0] = 0; // f(0) = 0  
        dp[1] = 1; // f(1) = 1  
          
        // 从第2个数开始,通过循环计算斐波那契数列的剩余值  
        for(int i = 2; i <= n; i++){  
            // 斐波那契数列的定义:f(i) = f(i-1) + f(i-2),这里计算并存储第i个斐波那契数  
            dp[i] = dp[i - 1] + dp[i - 2];  
        }   
          
        // 返回第n个斐波那契数  
        return dp[n];  
    }  
}

70.爬楼梯

70. 爬楼梯

简单

提示

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

提示:

  • 1 <= n <= 45

确定dp数组以及下标的含义

dp[i]: 爬到第i层楼梯,有dp[i]种方法

确定递推公式

首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。

还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。

那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!

所以dp[i] = dp[i - 1] + dp[i - 2] 。

在推导dp[i]的时候,一定要时刻想着dp[i]的定义,否则容易跑偏。

这体现出确定dp数组以及下标的含义的重要性!

dp数组如何初始化

再回顾一下dp[i]的定义:爬到第i层楼梯,有dp[i]种方法。

我dp[1] = 1,dp[2] = 2,这个初始化大家应该都没有争议的。

所以我的原则是:不考虑dp[0]如何初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义。

确定遍历顺序

从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的

举例推导dp数组

举例当n为5的时候,dp table(dp数组)应该是这样的

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

如果代码出问题了,就把dp table 打印出来,看看究竟是不是和自己推导的一样。

// 定义一个名为Solution的类  
class Solution {  
    // 定义一个公共方法climbStairs,用于计算爬楼梯的不同方式的数量  
    public int climbStairs(int n) {  
        // 如果楼梯的阶数小于3,则直接返回阶数本身,因为1阶和2阶的爬楼梯方式数就是阶数本身  
        if(n < 3){  
            return n;  
        }  
          
        // 定义一个数组dp,长度为n+1,用于存储到达每一阶楼梯的不同方式的数量  
        int dp[] = new int[n + 1];  
          
        // 初始化到达第1阶楼梯的方式有1种,即直接跨一步  
        dp[1] = 1;  
          
        // 初始化到达第2阶楼梯的方式有2种,即跨一步或跨两步  
        dp[2] = 2;  
          
        // 从第3阶楼梯开始,通过循环计算到达每一阶楼梯的不同方式的数量  
        for(int i = 3; i <= n; i++){  
            // 到达第i阶楼梯的方式数是到达第i-1阶楼梯的方式数加上到达第i-2阶楼梯的方式数  
            // 因为可以从第i-1阶跨一步上来,也可以从第i-2阶跨两步上来  
            dp[i] = dp[i - 1] + dp[i - 2];  
        }  
          
        // 返回到达第n阶楼梯的不同方式的数量  
        return dp[n];  
    }  
}

746.使用最小花费爬楼梯

746. 使用最小花费爬楼梯

简单

提示

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1:

输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。

示例 2:

输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。

提示:

  • 2 <= cost.length <= 1000
  • 0 <= cost[i] <= 999

题目中说 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯” 也就是相当于 跳到 下标 0 或者 下标 1 是不花费体力的, 从 下标 0 下标1 开始跳就要花费体力了。

确定dp数组以及下标的含义

dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]。

确定递推公式

可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。

dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。

dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。

那么究竟是选从dp[i - 1]跳还是从dp[i - 2]跳呢?

一定是选最小的,所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);

dp数组如何初始化

题目描述中明确说了 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。” 也就是说 到达 第 0 个台阶是不花费的,但从 第0 个台阶 往上跳的话,需要花费 cost[0]。

所以初始化 dp[0] = 0,dp[1] = 0;

确定遍历顺序

本题的遍历顺序其实比较简单,简单到很多同学都忽略了思考这一步直接就把代码写出来了。

因为是模拟台阶,而且dp[i]由dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。

举例推导dp数组

拿示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟一下dp数组的状态变化,如下:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

如果大家代码写出来有问题,就把dp数组打印出来,看看和如上推导的是不是一样的。

class Solution {  
    // 公共方法,计算爬楼梯的最小花费  
    public int minCostClimbingStairs(int[] cost) {  
        // 如果楼梯的阶数小于2,则不需要花费,直接返回0  
        if(cost.length < 2){  
            return 0;  
        }  
          
        // 定义一个数组dp,长度为cost.length + 1,用于存储到达每一阶楼梯的最小花费  
        int dp[] = new int[cost.length + 1];  
          
        // 初始化到达第0阶楼梯的花费为0,这通常是为了确保代码逻辑的完整性,实际中没有第0阶楼梯  
        dp[0] = 0;  
          
        // 初始化到达第1阶楼梯的花费为0,因为到达第1阶楼梯不需要跨越任何楼梯,所以花费为0  
        dp[1] = 0;  
          
        // 从第2阶楼梯开始,通过循环计算到达每一阶楼梯的最小花费  
        for(int i = 2; i <= cost.length; i++){  
            // 到达第i阶楼梯的最小花费是选择从第i-1阶跨一步上来或从第i-2阶跨两步上来的较小花费  
            // 加上对应阶数的花费cost[i-1]或cost[i-2]  
            dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);  
        }  
          
        // 返回到达最高阶楼梯(第cost.length阶)的最小花费  
        return dp[cost.length];  
    }  
}

62.不同路径

62. 不同路径

中等

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

输入:m = 3, n = 7
输出:28

示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

输入:m = 7, n = 3
输出:28

示例 4:

输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109

确定dp数组(dp table)以及下标的含义

dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。

确定递推公式

想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。

此时在回顾一下 dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理。

那么很自然,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。

dp数组的初始化

如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。

所以初始化代码为:

for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;

确定遍历顺序

这里要看一下递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。

这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值的。

举例推导dp数组

如图所示:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

class Solution {  
    // 这个函数计算从左上角到右下角的唯一路径数,其中网格的维度是m行n列  
    public static int uniquePaths(int m, int n) {  
          
        // 使用一个二维数组dp来存储到达每个格子的唯一路径数  
        int[][] dp = new int[m][n];  
  
        // 初始化第一列的所有格子,因为从左上角到第一列的任何一个格子都只有一条路径  
        for (int i = 0; i < m; i++) {  
            dp[i][0] = 1;  
        }  
  
        // 初始化第一行的所有格子,因为从左上角到第一行的任何一个格子也只有一条路径  
        for (int i = 0; i < n; i++) {  
            dp[0][i] = 1;  
        }  
  
        // 从第二行第二列开始,计算到达每个格子的唯一路径数  
        // 路径数等于其上方格子的路径数加上其左方格子的路径数  
        for (int i = 1; i < m; i++) {  
            for (int j = 1; j < n; j++) {  
                dp[i][j] = dp[i-1][j] + dp[i][j-1];  
            }  
        }  
  
        // 返回右下角格子的路径数,即从左上角到右下角的唯一路径数  
        return dp[m-1][n-1];  
    }  
}

63.不同路径||

63. 不同路径 II

中等

提示

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 1 和 0 来表示。

示例 1:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

  • m == obstacleGrid.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j] 为 0 或 1

确定dp数组(dp table)以及下标的含义

dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。

确定递推公式

dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。

但这里需要注意一点,因为有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)。

dp数组如何初始化

因为从(0, 0)的位置到(i, 0)的路径只有一条,所以dp[i][0]一定为1,dp[0][j]也同理。

但如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。

如图:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

下标(0, j)的初始化情况同理。

注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理

确定遍历顺序

从递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 中可以看出,一定是从左到右一层一层遍历,这样保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值。

代码如下:

举例推导dp数组

拿示例1来举例如题:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

对应的dp table 如图:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

class Solution {  
    // 这个函数计算从左上角到右下角的唯一路径数,其中网格中可能包含障碍物  
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {  
        // 创建一个二维数组dp来存储到达每个格子的唯一路径数  
        int dp[][] = new int[obstacleGrid.length][obstacleGrid[0].length];  
  
        // 初始化第一行  
        for (int i = 0; i < obstacleGrid[0].length; i++) {  
            // 如果当前格子是障碍物,则路径数为0  
            if (obstacleGrid[0][i] == 1) {  
                dp[0][i] = 0;  
            } else {  
                // 如果不是第一列,则路径数等于前一个格子的路径数  
                if (i > 0) {  
                    dp[0][i] = dp[0][i - 1];  
                } else {  
                    // 如果是第一列且没有障碍物,则路径数为1  
                    dp[0][i] = 1;  
                }  
            }  
        }  
  
        // 初始化第一列  
        for (int i = 0; i < obstacleGrid.length; i++) {  
            // 如果当前格子是障碍物,则路径数为0  
            if (obstacleGrid[i][0] == 1) {  
                dp[i][0] = 0;  
            } else {  
                // 如果不是第一行,则路径数等于上一个格子的路径数  
                if (i > 0) {  
                    dp[i][0] = dp[i - 1][0];  
                } else {  
                    // 如果是第一行且没有障碍物,则路径数为1  
                    dp[i][0] = 1;  
                }  
            }  
        }  
  
        // 从第二行第二列开始计算每个格子的路径数  
        for (int i = 1; i < obstacleGrid.length; i++) {  
            for (int j = 1; j < obstacleGrid[0].length; j++) {  
                // 如果当前格子是障碍物,则路径数为0  
                if (obstacleGrid[i][j] == 1) {  
                    dp[i][j] = 0;  
                } else {  
                    // 否则,路径数等于上方格子的路径数加上左方格子的路径数  
                    dp[i][j] = dp[i][j - 1] + dp[i - 1][j];  
                }  
            }  
        }  
  
        // 返回右下角格子的路径数,即从左上角到右下角的唯一路径数  
        return dp[dp.length - 1][dp[0].length - 1];  
    }  
}

 因为初始化的dp数组中所有的值均为0,所以在遇到障碍的情况下不需要将dp数组赋值为0,直接跳过就可以

class Solution {  
    // 这个函数计算从左上角到右下角的唯一路径数,其中网格中可能包含障碍物  
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {  
        // 创建一个二维数组dp来存储到达每个格子的唯一路径数  
        int dp[][] = new int[obstacleGrid.length][obstacleGrid[0].length];  
  
        // 初始化第一行  
        for (int i = 0; i < obstacleGrid[0].length && obstacleGrid[0][i] == 0; i++) {  
            // 如果当前格子是障碍物,则该格子dp不变(为0),且跳出循环,同一行之后的格子dp也不变    
            //格子左边无障碍物,dp为1
                dp[0][i] = 1;  
        }  
  
        // 初始化第一列  
        for (int i = 0; i < obstacleGrid.length && obstacleGrid[i][0] == 0; i++) {  
            // 如果当前格子是障碍物,则该格子dp不变(为0),且跳出循环,同一列之后的格子dp也不变          
            //格子上边无障碍物,dp为1
            dp[i][0] = 1;   
        }  
  
        // 从第二行第二列开始计算每个格子的路径数  
        for (int i = 1; i < obstacleGrid.length; i++) {  
            for (int j = 1; j < obstacleGrid[0].length; j++) {  
                // 如果当前格子不是障碍物,则路径数为0 ,是障碍物dp不变(为0) 
                if (obstacleGrid[i][j] != 1) {  
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];  
                } 
            }  
        }  
  
        // 返回右下角格子的路径数,即从左上角到右下角的唯一路径数  
        return dp[dp.length - 1][dp[0].length - 1];  
    }  
}

343.整数拆分

343. 整数拆分

中等

提示

给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积 。

示例 1:

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

提示:

  • 2 <= n <= 58

确定dp数组(dp table)以及下标的含义

dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。

dp[i]的定义将贯彻整个解题过程,下面哪一步想不懂了,就想想dp[i]究竟表示的是啥!

确定递推公式

可以想 dp[i]最大乘积是怎么得到的呢?

其实可以从1遍历j,然后有两种渠道得到dp[i].

一个是j * (i - j) 直接相乘。

一个是j * dp[i - j],相当于是拆分(i - j),对这个拆分不理解的话,可以回想dp数组的定义。

那有同学问了,j怎么就不拆分呢?

j是从1开始遍历,拆分j的情况,在遍历j的过程中其实都计算过了。那么从1遍历j,比较(i - j) * j和dp[i - j] * j 取最大的。递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));

也可以这么理解,j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘。

如果定义dp[i - j] * dp[j] 也是默认将一个数强制拆成4份以及4份以上了。

所以递推公式:dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});

那么在取最大值的时候,为什么还要比较dp[i]呢?

因为在递推公式推导的过程中,每次计算dp[i],取最大的而已。

dp的初始化

严格从dp[i]的定义来说,dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。

拆分0和拆分1的最大乘积是多少?

这是无解的。

这里我只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1,这个没有任何异议!

确定遍历顺序

确定遍历顺序,先来看看递归公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));

dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。

所以遍历顺序为:

for (int i = 3; i <= n ; i++) {
    for (int j = 1; j < i - 1; j++) {
        dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
    }
}

注意 枚举j的时候,是从1开始的。从0开始的话,那么让拆分一个数拆个0,求最大乘积就没有意义了。

j的结束条件是 j < i - 1 ,其实 j < i 也是可以的,不过可以节省一步,例如让j = i - 1,的话,其实在 j = 1的时候,这一步就已经拆出来了,重复计算,所以 j < i - 1

至于 i是从3开始,这样dp[i - j]就是dp[2]正好可以通过我们初始化的数值求出来。

举例推导dp数组

举例当n为10的时候,dp数组里的数值,如下:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

class Solution {  
    public int integerBreak(int n) {  
        // 创建一个动态规划数组,dp[i] 表示将正整数 i 拆分后得到的最大乘积  
        int[] dp = new int[n + 1];  
          
        // 初始化基础情况,将正整数2拆分只有一个拆分方式即2本身,因此乘积为1  
        dp[2] = 1;  
          
        // 从3开始遍历到n,计算每个数的最大乘积  
        for (int i = 3; i <= n; i++) {  
            // 对于每个数i,遍历所有可能的拆分方式j  
            for (int j = 1; j <= i; j++) {  
                // 计算当前拆分方式的乘积,有两种可能:  
                // 1. 直接将j和(i-j)相乘  
                // 2. 将j与剩下的数(i-j)拆分后的最大乘积相乘  
                // 取两种可能中的较大值更新dp[i]  
                dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));  
            }  
        }  
          
        // 返回正整数n拆分后的最大乘积  
        return dp[n];  
    }  
}

96.不同的二叉树

96. 不同的二叉搜索树

相关标签

相关企业

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例 1:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

输入:n = 3
输出:5

示例 2:

输入:n = 1
输出:1

提示:

  • 1 <= n <= 19

了解了二叉搜索树之后,我们应该先举几个例子,画画图,看看有没有什么规律,如图:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

n为1的时候有一棵树,n为2有两棵树,这个是很直观的。

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

来看看n为3的时候,有哪几种情况。

当1为头结点的时候,其右子树有两个节点,看这两个节点的布局,是不是和 n 为2的时候两棵树的布局是一样的啊!

(可能有同学问了,这布局不一样啊,节点数值都不一样。别忘了我们就是求不同树的数量,并不用把搜索树都列出来,所以不用关心其具体数值的差异)

当3为头结点的时候,其左子树有两个节点,看这两个节点的布局,是不是和n为2的时候两棵树的布局也是一样的啊!

当2为头结点的时候,其左右子树都只有一个节点,布局是不是和n为1的时候只有一棵树的布局也是一样的啊!

发现到这里,其实我们就找到了重叠子问题了,其实也就是发现可以通过dp[1] 和 dp[2] 来推导出来dp[3]的某种方式。

思考到这里,这道题目就有眉目了。

dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量

元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量

元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量

元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量

有2个元素的搜索树数量就是dp[2]。

有1个元素的搜索树数量就是dp[1]。

有0个元素的搜索树数量就是dp[0]。

所以dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]

如图所示:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

此时我们已经找到递推关系了,那么可以用动规五部曲再系统分析一遍。

确定dp数组(dp table)以及下标的含义

dp[i] : 1到i为节点组成的二叉搜索树的个数为dp[i]

也可以理解是i个不同元素节点组成的二叉搜索树的个数为dp[i] ,都是一样的。

确定递推公式

在上面的分析中,其实已经看出其递推关系, dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]

j相当于是头结点的元素,从1遍历到i为止。

所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量

dp数组如何初始化

初始化,只需要初始化dp[0]就可以了,推导的基础,都是dp[0]。

那么dp[0]应该是多少呢?

从定义上来讲,空节点也是一棵二叉树,也是一棵二叉搜索树,这是可以说得通的。

从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。

所以初始化dp[0] = 1

确定遍历顺序

首先一定是遍历节点数,从递归公式:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态。

那么遍历i里面每一个数作为头结点的状态,用j来遍历。

代码如下:

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= i; j++) {
        dp[i] += dp[j - 1] * dp[i - j];
    }
}

举例推导dp数组

n为5时候的dp数组状态如图:

代码随想录 动态规划-基础题目,代码随想录,动态规划,算法,贪心算法,数据结构,java,leetcode

当然如果自己画图举例的话,基本举例到n为3就可以了,n为4的时候,画图已经比较麻烦了。

我这里列到了n为5的情况,是为了方便大家 debug代码的时候,把dp数组打出来,看看哪里有问题文章来源地址https://www.toymoban.com/news/detail-841170.html

class Solution {  
    public int numTrees(int n) {  
        // 创建一个动态规划数组dp,dp[i]表示由1到i这些整数能构成的不同二叉搜索树的个数  
        int[] dp = new int[n + 1];  
        // 初始化dp数组,空树和只有一个节点的树都只有1种情况  
        dp[0] = 1;  
        dp[1] = 1;  
          
        // 从2开始遍历到n,计算每个数作为根节点时的不同二叉搜索树的个数  
        for(int i = 2; i <= n; i++){  
            // 对于每个i,枚举1到i之间的每个数j作为根节点  
            for(int j = 1; j <= i; j++){  
                // 当j作为根节点时,左子树由1到j-1构成,右子树由j+1到i构成  
                // 左子树和右子树分别可以构成dp[j-1]和dp[i-j]个不同的二叉搜索树  
                // 因此,以j为根节点的不同二叉搜索树的个数为左子树个数乘以右子树个数  
                // 累加所有以不同数为根节点的情况,得到dp[i]  
                dp[i] += dp[j - 1] * dp[i - j];  
            }  
        }  
        // 返回由1到n这些整数能构成的不同二叉搜索树的个数  
        return dp[n];  
    }  
}

到了这里,关于代码随想录 动态规划-基础题目的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 代码随想录第41天 | 动态规划part03

    ● 343. 整数拆分 ● 96.不同的二叉搜索树 题目一 343. 整数拆分 给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。 示例 : 输入: 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。 说明: 你可以假设 n 不小于 2 且不大于 5

    2024年01月24日
    浏览(52)
  • 动态规划01背包问题-代码随想录-刷题笔记

    有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。 每件物品只能用一次 ,求解将哪些物品装入背包里物品价值总和最大。 二维dp数组01背包 确定dp数组以及下标的含义 是使用二维数组,即 dp[i][j] 表示从下标为[0-i]的物品里任意取,

    2024年02月07日
    浏览(58)
  • 代码随想录 动态规划-子序列问题-子序列(连续)

    目录 674.最长连续递增序列  718.最长重复子数组 53.最大子数组和  674. 最长连续递增序列 简单 给定一个未经排序的整数数组,找到最长且  连续递增的子序列 ,并返回该序列的长度。 连续递增的子序列  可以由两个下标  l  和  r ( l r )确定,如果对于每个  l = i r ,都

    2024年04月09日
    浏览(50)
  • 代码随想录Day41:动态规划Part3

    讲解前: 毫无头绪 讲解后: 这道题的动态思路一开始很不容易想出来,虽然dp数组的定义如果知道是动态规划的话估摸着可以想出来那就是很straight forward dp定义:一维数组dp[i], i 代表整数的值,dp[i] 代表将整数 i 拆分的话可以获得的最大乘积 然后呢就是定义递归推导式了,

    2024年04月27日
    浏览(42)
  • 【每日刷题】动态规划-代码随想录动规-8、9

    题目链接 dp数组含义 :dp[i]表示拆分i的最大乘积 递推公式 :dp[i]= max(j*(i-j), j*dp[i-j], dp[i]) 解释:从1遍历j,有两种渠道得到dp[i]. 一个是j * (i - j) 直接相乘。 一个是j * dp[i - j],相当于是拆分(i - j) 为何不拆分j:j是从1开始遍历,拆分j的情况,在遍历j的过程中其实都计算过了

    2024年02月02日
    浏览(49)
  • 代码随想录 动态规划 判断子序列,不同的子序列

    392. 判断子序列 给定字符串  s  和  t  ,判断  s  是否为  t  的子序列。 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如, \\\"ace\\\" 是 \\\"abcde\\\" 的一个子序列,而 \\\"aec\\\" 不是)。 思路: 1. 使用哈希统计两个序

    2024年02月07日
    浏览(45)
  • 代码随想录 day38 第九章 动态规划part01

    ●  理论基础 ●  509. 斐波那契数 ●  70. 爬楼梯 ●  746. 使用最小花费爬楼梯 理论基础 解决动态规划必须要想清楚的点 dp数组以及下标的含义 递推公式 dp数组如何初始化 遍历顺序 打印数组 检查结果 关联 leetcode 509. 斐波那契数 思路 动规五部曲 dp数组以及下标的含义

    2024年04月17日
    浏览(50)
  • 【代码随想录】Day 49 动态规划10 (买卖股票Ⅰ、Ⅱ)

    https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/ dp[i]表示在第i天时,卖/不卖股票能获得的最大利润: 1、卖股票:dp[i] = prices[i] -minPrice(i天以前的最低价格) 2、不卖股票:dp[i] = dp[i-1](因为不卖股票,所以状态和前一天保持一致) ∴dp[i] = max(dp[i-1], prices[i] - minPrice); https

    2024年02月09日
    浏览(46)
  • 【每日刷题】动态规划-代码随想录动规-11、12、13

    问题背景 : 有若干个物品对应各自的体积和价值,有一个容量确定的背包,有选择的将物品装进背包里,求可放进背包的最大价值。 思路: 定义dp数组: dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。 dp[i][j]递推公式: 不放物品

    2024年02月22日
    浏览(51)
  • Day39 代码随想录(1刷) 动态规划 0-1背包

    题目描述 小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。  小明的行李空间

    2024年04月23日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包