当前位置: 首页 > news >正文

动态规划汇总1

1.动态规划

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。

但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。

所以贪心解决不了动态规划的问题。

动态规划五步曲

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

为什么要先确定递推公式,然后在考虑初始化呢?

因为一些情况是递推公式决定了dp数组要如何初始化!

做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果

然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。

如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。

如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。

代码都已经和题解一模一样了,为什么通过不了呢?

发出这样的问题之前,其实可以自己先思考这三个问题:

  • 这道题目我举例推导状态转移公式了么?
  • 我打印dp数组的日志了么?
  • 打印出来了dp数组和我想的一样么?

如果这灵魂三问自己都做到了,基本上这道题目也就解决了,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历dp数组的顺序。

2.斐波那契数

力扣题目链接(opens new window)

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。

示例 1:

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

示例 2:

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

示例 3:

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

提示:

  • 0 <= n <= 30
public class Fibonacci_number {public int fib2(int n) {//公共方法,接受一个整数参数 n,返回斐波那契数列的第 n 项。if (n <= 1) return n;//如果 n 小于或等于 1,直接返回 n,因为斐波那契数列的前两个数分别是 0 和 1。int[] dp = new int[n + 1];//创建一个长度为 n + 1 的数组 dp,为了存储从 F(0) 到 F(n) 的所有斐波那契数。初始化 dp[0] 为 0,dp[1] 为 1,这是斐波那契数列的前两项。dp[0] = 0;dp[1] = 1;for (int index = 2; index <= n; index++){//for 循环从索引 2 迭代到 n,每次迭代计算下一个斐波那契数,并存储在 dp[index] 中。dp[index] = dp[index - 1] + dp[index - 2];//在每次迭代中,根据斐波那契数列的递推公式 F(n) = F(n - 1) + F(n - 2),计算 dp[index] 的值,即 dp[index] 等于 dp[index - 1] 与 dp[index - 2] 的和。}//通过逐步计算,数组 dp 中会依次存储斐波那契数列的前 n + 1 项的值。return dp[n];//循环结束后,dp[n] 中存储的就是斐波那契数列的第 n 项的值,将其返回。}
}

假设 n = 5

  1. 因为 n = 5 > 1,不满足 if (n <= 1) 的条件,继续执行。
  2. 创建数组 dp,长度为 6n + 1),并初始化 dp[0] = 0dp[1] = 1
  3. 进入 for 循环:
    • 当 index = 2 时:
      • dp[2] = dp[1] + dp[0] = 1 + 0 = 1
    • 当 index = 3 时:
      • dp[3] = dp[2] + dp[1] = 1 + 1 = 2
    • 当 index = 4 时:
      • dp[4] = dp[3] + dp[2] = 2 + 1 = 3
    • 当 index = 5 时:
      • dp[5] = dp[4] + dp[3] = 3 + 2 = 5
  4. 循环结束,返回 dp[5],即 5,这就是斐波那契数列的第 5 项的值。

通过这种动态规划的方法,代码可以高效地计算出斐波那契数列的任意一项。

3.爬楼梯

力扣题目链接(opens new window)

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

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

注意:给定 n 是一个正整数。

示例 1:

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

示例 2:

  • 输入: 3
  • 输出: 3
  • 解释: 有三种方法可以爬到楼顶。
    • 1 阶 + 1 阶 + 1 阶
    • 1 阶 + 2 阶
    • 2 阶 + 1 阶
public class Climbing_Stairs {public int climbStairs1(int n) {//接受一个整数参数 n,表示台阶的数量,返回爬上 n 级台阶的不同方法数。int[] dp = new int[n + 1];//创建一个长度为 n + 1 的数组 dp,用于存储从第0个台阶到第 n 个台阶的爬法数量。dp[0] = 1;//表示在地面上(第0个台阶)只有1种方式(啥都不做)。dp[1] = 1;//表示到达第1个台阶只有1种方式(即向上爬1个台阶)。for (int i = 2; i <= n; i++) {//for 循环,从第 2 级台阶开始迭代到第 n 级台阶。每次迭代计算到达当前台阶的不同方法数,并存储在 dp[i] 中。到达第 i 个台阶的方法数等于到达第 i-1 个台阶的方法数加上到达第 i-2 个台阶的方法数,因为可以从第 i-1 个台阶走1步上来,或者从第 i-2 个台阶走2步上来。dp[i] = dp[i - 1] + dp[i - 2];}return dp[n];//循环结束后,dp[n] 中存储的就是爬上 n 级台阶的不同方法数,将其返回。}
}

假设 n = 3

  1. 创建数组 dp,长度为 4n + 1),并初始化 dp[0] = 1dp[1] = 1
  2. 进入 for 循环:
    • 当 i = 2 时:
      • dp[2] = dp[1] + dp[0] = 1 + 1 = 2。这表示到达第 2 级台阶有 2 种方法:从第 0 级走 2 步上来,或者从第 1 级走 1 步上来。
    • 当 i = 3 时:
      • dp[3] = dp[2] + dp[1] = 2 + 1 = 3。这表示到达第 3 级台阶有 3 种方法:从第 1 级走 2 步上来,或者从第 2 级走 1 步上来。
  3. 循环结束,返回 dp[3],即 3,这就是爬上 3 级台阶的不同方法数。

这种动态规划的方法通过逐步计算每一级台阶的方法数,最终得出爬上 n 级台阶的总方法数,逻辑清晰且计算效率较高。

4.使用最小花费爬楼梯

力扣题目链接(opens new window)

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

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

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

提示:

  • cost 的长度范围是 [2, 1000]。
  • cost[i] 将会是一个整型数据,范围为 [0, 999] 。
public class Climbing_Stairs_with_Minimum_Cost {public int minCostClimbingStairs2(int[] cost) {//接受一个整数数组 cost 作为参数,cost 数组中的每个元素表示爬上对应台阶所需的花费。方法返回爬上楼梯顶部的最小花费。int[] dp = new int[cost.length];//创建一个长度为 cost.length 的数组 dp,用于存储从第0个台阶到第 cost.length - 1 个台阶的最小花费,dp[i] 表示到达第 i 个台阶的最小花费。dp[0] = cost[0];//因为题目设定可以从第 0 个台阶或第 1 个台阶开始)。从起始位置到达第 0 个台阶,只有一种方式,就是直接踏上第 0 个台阶,而踏上这个台阶所需的花费就是 cost[0],所以将 dp[0] 初始化为 cost[0]。表示到达第 0 个台阶的最小花费就是 cost[0]。dp[1] = cost[1];//对于第 1 个台阶,从起始位置到达它也只有一种方式,就是直接踏上第 1 个台阶,所需的花费就是 cost[1]。所以把 dp[1] 初始化为 cost[1]。表示到达第 1 个台阶的最小花费就是 cost[1]。for (int i = 2; i < cost.length; i++) {//从第 2 个台阶开始迭代到第 cost.length - 1 个台阶。每次迭代计算到达当前台阶的最小花费,并存储在 dp[i] 中。到达第 i 个台阶的最小花费等于到达第 i-1 个台阶的最小花费加上当前台阶的花费,或者到达第 i-2 个台阶的最小花费加上当前台阶的花费,取两者的最小值。dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];}return Math.min(dp[cost.length - 1], dp[cost.length - 2]);//最终返回 dp[cost.length - 1] 和 dp[cost.length - 2] 的最小值,因为到达顶部可以是从倒数第二步或倒数第三步爬上来的。}
}

假设输入的 cost 数组为 [10, 15, 20]

  1. 初始化 dp 数组:
    • dp[0] = 10dp[1] = 15
  2. 进入 for 循环:
    • 当 i = 2 时:
      • dp[2] = Math.min(dp[1], dp[0]) + cost[2]
      • dp[2] = Math.min(15, 10) + 20 = 10 + 20 = 30
  3. 循环结束后,计算并返回结果:
    • return Math.min(dp[2], dp[1]) = Math.min(30, 15) = 15

所以,爬上楼梯顶部的最小花费是 15。通过这种动态规划的方法,代码能够有效地计算出爬上楼梯的最小花费。

5.不同路径

力扣题目链接(opens new window)

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

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

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

示例 1:

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

示例 2:

  • 输入:m = 2, n = 3
  • 输出:3

解释: 从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向右 -> 向下
  2. 向右 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向右

示例 3:

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

示例 4:

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

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 10^9
public class different_paths {public int uniquePaths2(int m, int n) {//接受两个整数参数m和n,分别表示网格的行数和列数。方法返回从网格左上角到右下角的不同路径数量。int[] dp = new int[n];//创建一个长度为 n 的一维整数数组 dp,dp[j] 表示到达当前行的第 j 个格子的不同路径数量。Arrays.fill(dp, 1);//将dp数组的所有元素初始化为1,这是因为在第一行中,到达任何一个格子都只有一种方式,即从左边的格子直接移动过来(因为只能向右或向下移动)。for (int i = 1; i < m; i ++) {//外层 for 循环:从第二行(索引 i = 1)开始,遍历到第 m - 1 行。对于每一行,我们要计算该行中每个格子的不同路径数量。for (int j = 1; j < n; j ++) {//内层 for 循环:从第二列(索引 j = 1)开始,遍历到第 n - 1 列。对于每一个格子 (i, j)。到达该格子的路径数量 dp[j]dp[j] += dp[j - 1];//由于只能向右或向下移动,到达当前格子 (i, j) 的路径可以来自于上方的格子 (i - 1, j) 或者左方的格子 (i, j - 1)。因为我们是逐行计算的,当计算到第 i 行的格子时,dp[j - 1] 就表示了到达当前行前一列的格子 (i, j - 1) 的路径数量,而 dp[j] 在这之前保存的是上一行同一列的格子 (i - 1, j) 的路径数量。所以,dp[j] += dp[j - 1] 表示将到达当前行前一列的格子的路径数量累加到当前格子的路径数量上,从而得到到达当前格子 (i, j) 的总路径数量。在计算第 i 行的格子时,dp 数组的状态是上一行计算完成后的结果。}}return dp[n - 1];//当所有行和列都计算完毕后,dp[n - 1] 中存储的就是到达右下角格子(即第 m - 1 行,第 n - 1 列)的不同路径数量,将其返回。}
}

假设 m = 3n = 2

  1. 初始化 dp 数组:
    • dp = [1, 1],因为第一行的每个格子都只有一种到达方式。
  2. 进入外层 for 循环:
    • 当 i = 1(第二行):
      • 进入内层 for 循环:
        • 当 j = 1
          • dp[1] += dp[0],此时 dp[0] = 1dp[1] = 1,所以 dp[1] = 1 + 1 = 2
    • 此时 dp = [1, 2]
  3. 外层 for 循环结束,返回 dp[n - 1],即 dp[1] = 2

这表示在一个 3 x 2 的网格中,从左上角到右下角有 2 条不同的路径。通过这种动态规划的方法,代码能够高效地计算出不同路径的数量。

6.不同路径 II

力扣题目链接(opens new window)

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

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

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

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

示例 1:

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

示例 2:

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

提示:

  • m == obstacleGrid.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j] 为 0 或 1
public class different_pathsII {public int uniquePathsWithObstacles2(int[][] obstacleGrid) {//接受一个二维整数数组obstacleGrid作为参数,表示网格,其中1表示障碍物,0表示空地。该方法返回从左上角到右下角的不同路径数量。int m = obstacleGrid.length;//获取网格的行数m和列数n。int n = obstacleGrid[0].length;int[] dp = new int[n];//创建一个长度为 n 的一维整数数组 dp,dp[j] 表示到达当前行的第 j 列格子的不同路径数量。for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {//初始化第一行的 dp 值。从左到右遍历第一行,如果当前格子没有障碍物(obstacleGrid[0][j] == 0),则到达该格子的路径数量为 1,因为从左上角出发到第一行的非障碍物格子只有一种路径。一旦遇到障碍物,后面的格子路径数就不再初始化,保持默认的 0 值。dp[j] = 1;}for (int i = 1; i < m; i++) {//外层 for 循环:从第二行(i = 1)开始,遍历到第 m - 1 行。对于每一行,我们要计算该行中每个格子的不同路径数量。for (int j = 0; j < n; j++) {//内层 for 循环:遍历当前行的每一列(从 j = 0 到 j = n - 1)。if (obstacleGrid[i][j] == 1) {//如果当前格子 (i, j) 有障碍物(obstacleGrid[i][j] == 1),那么到达该格子的路径数量为 0,因为无法通过有障碍物的格子。dp[j] = 0;} else if (j != 0) {//如果当前格子没有障碍物,并且不是当前列的第一个格子(即j != 0),由于只能从上方或左方移动到当前格子,在处理到第 i 行时,dp[j - 1] 表示到达当前行前一列格子 (i, j - 1) 的路径数量。由于可以从 (i, j - 1) 直接移动到 (i, j),所以从 (i, j - 1) 到达 (i, j) 的路径数量就是 dp[j - 1]。dp[j] += dp[j - 1];//通过 dp[j] += dp[j - 1];,就把从左边格子到达当前格子的路径数量累加到了 dp[j] 上。上方格子对 dp[j] 的贡献已经在之前的计算中被包含在 dp[j] 的初始值里了。具体来说,在处理第 i 行的格子时,dp[j] 在执行 dp[j] += dp[j - 1]; 之前的值,就是从第 i - 1 行同一列的格子(即上方格子 (i - 1, j))到达当前格子 (i, j) 的路径数量。}}}return dp[n - 1];//当所有行和列都计算完毕后,dp[n - 1] 中存储的就是到达右下角格子(即第 m - 1 行,第 n - 1 列)的不同路径数量,将其返回。}
}
[[0, 0, 0],[0, 1, 0],[0, 0, 0]
]
  1. 初始化
    • m = 3n = 3dp = [0, 0, 0]
    • 初始化第一行:
      • 因为 obstacleGrid[0][0] == 0dp[0] = 1
      • 因为 obstacleGrid[0][1] == 0dp[1] = 1
      • 因为 obstacleGrid[0][2] == 0dp[2] = 1。此时 dp = [1, 1, 1]
  2. 计算第二行
    • 当 i = 1
      • 当 j = 0obstacleGrid[1][0] == 0dp[0] 保持不变为 1
      • 当 j = 1obstacleGrid[1][1] == 1dp[1] = 0
      • 当 j = 2obstacleGrid[1][2] == 0dp[2] 由于 j!= 0dp[2] += dp[1],此时 dp[2] = 1 + 0 = 1。此时 dp = [1, 0, 1]
  3. 计算第三行
    • 当 i = 2
      • 当 j = 0obstacleGrid[2][0] == 0dp[0] 保持不变为 1
      • 当 j = 1obstacleGrid[2][1] == 0dp[1] += dp[0]dp[1] = 0 + 1 = 1
      • 当 j = 2obstacleGrid[2][2] == 0dp[2] += dp[1]dp[2] = 1 + 1 = 2。此时 dp = [1, 1, 2]
  4. 返回结果
    • 返回 dp[n - 1],即 dp[2] = 2

所以,从左上角到右下角有 2 条不同的路径。

7.整数拆分

力扣题目链接(opens new window)

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

示例 1:

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

示例 2:

  • 输入: 10
  • 输出: 36
  • 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
  • 说明: 你可以假设 n 不小于 2 且不大于 58。
public class Integer_Partition {public int integerBreak1(int n) {//接受一个整数n作为参数,并返回将其拆分后能得到的最大乘积。int[] dp = new int[n+1];//一个长度为n+1的整型数组dp,用于存储从1到n每个整数拆分后能得到的最大乘积。dp[i] 用于存储整数 i 拆分后能得到的最大乘积。dp[2] = 1;//初始化dp[2]为1,因为2是最小的可以拆分的数,且2只能拆分为1+1,乘积为1。for(int i = 3; i <= n; i++) {//从 3 开始遍历到 n,因为 2 已经在前面初始化过了。对于每个 i,我们要计算将其拆分后能得到的最大乘积。for(int j = 1; j <= i-j; j++) {//对于每个 i,内层循环遍历所有可能的拆分数 j,j 的取值范围是从 1 到 i - j。这样可以确保 j 和 i - j 都是正整数,且它们的和为 i。dp[i] = Math.max(dp[i], Math.max(j*(i-j), j*dp[i-j]));//对于每个i,尝试所有可能的拆分方式,计算j*(i-j)和j*dp[i-j],取两者中较大的值,更新dp[i]。j*(i-j)表示将i拆分为j和i-j的乘积,j*dp[i-j]表示将i拆分为j和剩余部分(i-j)的最大乘积。}//例如,假设 i = 4:// 当 j = 1 时,j * (i - j) = 1 * 3 = 3,j * dp[i - j] = 1 * dp[3](假设 dp[3] = 2),则 j * dp[i - j] = 1 * 2 = 2,Math.max(j * (i - j), j * dp[i - j]) = 3。如果 dp[4] 初始值为 0,那么 dp[4] = Math.max(0, 3) = 3。//当 j = 2 时,j * (i - j) = 2 * 2 = 4,j * dp[i - j] = 2 * dp[2](假设 dp[2] = 1),则 j * dp[i - j] = 2 * 1 = 2,Math.max(j * (i - j), j * dp[i - j]) = 4。此时 dp[4] = Math.max(3, 4) = 4。// 所以经过这一系列计算,dp[4] 最终被更新为 4,即整数 4 拆分后能得到的最大乘积。}return dp[n];//循环结束后,dp[n] 中存储的就是将整数 n 拆分后能得到的最大乘积,将其返回。}
}

假设 n = 5

  1. 初始化 dp 数组,dp[2] = 1
  2. 当 i = 3
    • 内层循环:
      • 当 j = 1j * (i - j) = 1 * 2 = 2j * dp[i - j] = 1 * dp[2] = 1 * 1 = 1dp[3] = Math.max(dp[3], Math.max(2, 1)) = 2
  3. 当 i = 4
    • 内层循环:
      • 当 j = 1j * (i - j) = 1 * 3 = 3j * dp[i - j] = 1 * dp[3] = 1 * 2 = 2dp[4] = Math.max(dp[4], Math.max(3, 2)) = 3
      • 当 j = 2j * (i - j) = 2 * 2 = 4j * dp[i - j] = 2 * dp[2] = 2 * 1 = 2dp[4] = Math.max(dp[4], Math.max(4, 2)) = 4
  4. 当 i = 5
    • 内层循环:
      • 当 j = 1j * (i - j) = 1 * 4 = 4j * dp[i - j] = 1 * dp[4] = 1 * 4 = 4dp[5] = Math.max(dp[5], Math.max(4, 4)) = 4
      • 当 j = 2j * (i - j) = 2 * 3 = 6j * dp[i - j] = 2 * dp[3] = 2 * 2 = 4dp[5] = Math.max(dp[5], Math.max(6, 4)) = 6

最终,dp[5] = 6,即把 5 拆分后能得到的最大乘积是 6(例如拆分为 2 + 3)。

8.不同的二叉搜索树

力扣题目链接(opens new window)

给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?

二叉搜索树

二叉搜索树(Binary Search Tree,BST),也称为二叉排序树或二叉查找树,是一种特殊的二叉树数据结构。它满足以下性质:

  • 节点值特性: 对于树中的每个节点,其左子树中的所有节点的值都小于该节点的值,而其右子树中的所有节点的值都大于该节点的值。
  • 有序性: 二叉搜索树的中序遍历会得到一个升序的序列。这一特性使得二叉搜索树在查找、插入和删除操作上具有较高的效率。
public class Different_Binary_Search_Trees {public int numTrees(int n) {//接受一个整数n作为参数,表示节点的数量,并返回可以形成的不同二叉搜索树的数量。int[] dp = new int[n + 1];//一个长度为n + 1的整型数组dp,用于存储从0到n个节点时不同二叉搜索树的数量。dp[0] = 1;//初始化dp[0]和dp[1]。对于0个节点,有一种空树的情况,对于1个节点,有一种单节点树的情况。dp[1] = 1;for (int i = 2; i <= n; i++) {//外层 for 循环:从 2 开始遍历到 n,对于每个 i,我们要计算有 i 个节点时不同二叉搜索树的数量。for (int j = 1; j <= i; j++) {//对于每个 i,内层循环遍历从 1 到 i 的所有可能的根节点。这是因为在构建二叉搜索树时,每个节点都可以作为根节点。dp[i] += dp[j - 1] * dp[i - j];//对于每个i,计算以j为根节点时的不同二叉搜索树的数量,并将结果累加到dp[i]。这里dp[j - 1]表示左子树的不同二叉搜索树的数量(因为左子树有 j - 1 个节点),dp[i - j]表示右子树的不同二叉搜索树的数量(因为右子树有 i - j 个节点)。}//根据乘法原理,以 j 为根节点的不同二叉搜索树的数量就是左子树的不同二叉搜索树数量乘以右子树的不同二叉搜索树数量。我们将以每个 j 作为根节点时的不同二叉搜索树的数量累加到 dp[i] 中,这样遍历完所有的 j 后,dp[i] 就得到了有 i 个节点时不同二叉搜索树的总数量。}//乘法原理是一个基本的计数原理。如果完成一件事需要n个步骤,其中,做第一步有m1种不同的方法,做第二步有m2种不同的方法,……,做第n步有mn种不同的方法,那么完成这件事一共有N=m1*m2*....*mn种不同的方法。return dp[n];//循环结束后,dp[n] 中存储的就是有 n 个节点时不同二叉搜索树的数量,将其返回。当以值为 j 的节点作为根节点时,因为左子树节点的值都要小于根节点的值,所以左子树的节点只能从值为 1 到 j - 1 的这些节点中选取,那么左子树就恰好有 j - 1 个节点。在具有 i 个节点的二叉搜索树中,当确定以值为 j 的节点作为根节点时,右子树的节点数量为 i - j 个}
}

假设 n = 3

  1. 初始化 dp 数组,dp[0] = 1dp[1] = 1
  2. 当 i = 2
    • 内层循环:
      • 当 j = 1dp[1] = 1dp[0] = 1dp[2] += dp[0] * dp[1] = 1 * 1 = 1
      • 当 j = 2dp[1] = 1dp[0] = 1dp[2] += dp[1] * dp[0] = 1 * 1 = 1,所以 dp[2] = 2
  3. 当 i = 3
    • 内层循环:
      • 当 j = 1dp[0] = 1dp[2] = 2dp[3] += dp[0] * dp[2] = 1 * 2 = 2
      • 当 j = 2dp[1] = 1dp[1] = 1dp[3] += dp[1] * dp[1] = 1 * 1 = 1
      • 当 j = 3dp[2] = 2dp[0] = 1dp[3] += dp[2] * dp[0] = 2 * 1 = 2,所以 dp[3] = 5

最终,dp[3] = 5,即有 3 个节点时,可以形成 5 种不同的二叉搜索树。

相关文章:

动态规划汇总1

1.动态规划 动态规划&#xff0c;英文&#xff1a;Dynamic Programming&#xff0c;简称DP&#xff0c;如果某一问题有很多重叠子问题&#xff0c;使用动态规划是最有效的。 所以动态规划中每一个状态一定是由上一个状态推导出来的&#xff0c;这一点就区分于贪心&#xff0c…...

【计算机网络】lab5 ARP协议

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;计算机网络_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前言 2.…...

分布式缓存redis

分布式缓存redis 1 redis单机&#xff08;单节点&#xff09;部署缺点 &#xff08;1&#xff09;数据丢失问题&#xff1a;redis是内存存储&#xff0c;服务重启可能会丢失数据 &#xff08;2&#xff09;并发能力问题&#xff1a;redis单节点&#xff08;单机&#xff09;部…...

【Rust】数据类型

目录 思维导图 1. 数据类型概述 1.1 标量类型 1.1.1 整数类型 1.1.2 浮点数类型 1.1.3 布尔类型 1.1.4 字符类型 1.2 复合类型 1.2.1 元组类型 1.2.2 数组类型 2. 类型注解与类型推断 3. 整数溢出处理 4. 数字运算 5. 示例 思维导图 1. 数据类型概述 Rust是一种静…...

在现代工业自动化领域CClinkIE转ModbusTCP网关的应用

在现代工业自动化领域&#xff0c;开疆智能CCLINKIE转ModbusTCP网关扮演着至关重要的角色&#xff0c;尤其是在立体仓库的应用中。立体仓库系统通过高度集成的自动化设备和先进的信息技术&#xff0c;实现了物料存储和管理的高效率。CCLINKIE转ModbusTCP网关作为连接不同工业通…...

ASP.NET Core与GraphQL集成

一、引言&#xff1a;探索 C# 与ASP.NET Core、GraphQL 的协同魅力 在当今数字化浪潮中&#xff0c;Web 开发领域不断演进&#xff0c;新技术层出不穷。C# 作为.NET 平台上的中流砥柱&#xff0c;凭借其强大的功能与优雅的语法&#xff0c;成为众多开发者构建各类应用程序的得…...

Zabbix 从入门到精通

一、Zabbix 简介 1.1 什么是 Zabbix Zabbix 是一个基于 Web 界面的提供分布式系统监视以及网络监视功能的企业级开源解决方案。它能监控各种网络参数&#xff0c;保证服务器系统的安全运营&#xff1b;并提供灵活的通知机制以让系统管理员快速定位 / 解决存在的各种问题。 1…...

文生图模型的技术原理、训练方案与微调方案

文生图模型的技术原理、训练方案与微调方案 引言 文生图(Text-to-Image)模型是一类能够根据文本描述生成对应图像的深度学习模型。近年来,随着生成对抗网络(GANs)和扩散模型(Diffusion Models)等技术的进步,文生图模型在图像生成领域取得了显著的进展。本文将详细介绍…...

3_CSS3 渐变 --[CSS3 进阶之路]

CSS3 引入了渐变&#xff08;gradients&#xff09;&#xff0c;它允许在两个或多个指定的颜色之间显示平滑的过渡。CSS3 支持两种类型的渐变&#xff1a; 线性渐变&#xff08;Linear Gradients&#xff09;&#xff1a;颜色沿着一条线性路径变化&#xff0c;可以是水平、垂直…...

国内主流的Spring微服务方案指南

构建一个完整的 Spring 微服务方案涉及多个关键组件的集成与配置&#xff0c;包括服务注册与发现、配置管理、API 网关、负载均衡、服务调用、熔断与限流、消息中间件、分布式追踪、服务网格、容器编排以及数据库与缓存等。以下将结合前述内容&#xff0c;详细介绍一个完整的中…...

docker更换镜像源脚本

Ubuntu / Debian 系统下的脚本 sudo curl -fsSL http://luyuanbo79.iepose.cn/wenjian/docker%20jingxiangyuan/Ubuntu-Debian.sh | sh CentOS / RHEL 系统下的脚本 sudo curl -fsSL\n\nhttp://luyuanbo79.iepose.cn/wenjian/docker%20jingxiangyuan/CentOS%20%20RHEL.sh | …...

Java Web开发进阶——错误处理与日志管理

错误处理和日志管理是任何生产环境中不可或缺的一部分。在 Spring Boot 中&#xff0c;合理的错误处理机制不仅能够提升用户体验&#xff0c;还能帮助开发者快速定位问题&#xff1b;而有效的日志管理能够帮助团队监控应用运行状态&#xff0c;及时发现和解决问题。 1. 常见错误…...

计算机网络 笔记 网络层1

网络层功能概述 主要的任务是把分组从源端传输到目的端&#xff0c;为分组交换网上的不同主句提供通信服务&#xff0c;网络层的传输单位是数据报。 主要的功能&#xff1b; 1&#xff0c;路由选择&#xff1a;路由选择指网络层根据特定算法&#xff0c;为数据包从源节点到目…...

【论文笔记】多个大规模数据集上的SOTA绝对位姿回归方法:Reloc3r

abstract 视觉定位旨在确定查询图像相对于姿势图像数据库的相机姿势。 近年来&#xff0c;直接回归相机姿势的深度神经网络由于其快速推理能力而受到欢迎。 然而&#xff0c;现有方法很难很好地推广到新场景或提供准确的相机姿态估计。 为了解决这些问题&#xff0c;我们提出了…...

springMVC---常用注解

目录 一、创建项目 1.依赖 2.web.xml 3.spring-mvc.xml 二、RequestParam注解 1.作用 2.属性 3.代码 DeptController类 启动tomcat 三、RequestBody注解 1.作用 2.属性 3.代码 (1&#xff09;DeptController类 (2&#xff09;index.jsp (3)启动tomcat 四、P…...

青龙面板脚本开发指南:高效自动化任务的实现

青龙面板脚本开发指南&#xff1a;高效自动化任务的实现 青龙面板&#xff08;Qinglong Panel&#xff09;是一款强大的任务管理平台&#xff0c;支持多种语言的脚本开发和执行。通过在青龙面板中编写和管理脚本&#xff0c;用户可以轻松实现自动化任务&#xff0c;提高工作效…...

深入详解DICOM医学影像定位线相关知识:理解定位线的概念、定位线的作用以及定位线显示和计算原理

DICOM医学影像中的定位线(Localization Line) 在医学影像学中,DICOM是用于存储和交换医学影像的标准格式。定位线(Localization Line)在医学影像的显示和分析中起着重要作用,它帮助医生和医学专业人员在影像中精确地标定重要的解剖结构、区域或特征,辅助进行定位、治疗计…...

网络应用技术 实验七:实现无线局域网

一、实验简介 在 eNSP 中构建无线局域网&#xff0c;并实现全网移动终端互相通信。 二、实验目的 1 、理解无线局域网的工作原理&#xff1b; 2 、熟悉无线局域网的规划与构建过程&#xff1b; 3 、掌握无线局域网的配置方法&#xff1b; 三、实验学时 2 学时 四、实…...

kubeneters-循序渐进Cilium网络(一)

文章目录 概要传统网络不同的网络&#xff08;或子网&#xff09;之间通信Kubernetes 中的网络在同一栋大楼内的公寓之间通信跨大楼的通信总结 概要 本文通过“封包追踪”方法&#xff0c;深入解析 Kubernetes 网络通信过程。基于 eBPF 的 Cilium 工具&#xff0c;直观展示了数…...

elasticsearch中IK分词器

1、什么是IK分词器 ElasticSearch 几种常用分词器如下&#xff1a; 分词器分词方式StandardAnalyzer单字分词CJKAnalyzer二分法IKAnalyzer词库分词 分词∶即把一段中文或者别的划分成一个个的关键字&#xff0c;我们在搜索时候会把自己的信息进行分词&#xff0c;会把数据库…...

React Native 开发环境搭建(全平台详解)

React Native 开发环境搭建&#xff08;全平台详解&#xff09; 在开始使用 React Native 开发移动应用之前&#xff0c;正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南&#xff0c;涵盖 macOS 和 Windows 平台的配置步骤&#xff0c;如何在 Android 和 iOS…...

【论文笔记】若干矿井粉尘检测算法概述

总的来说&#xff0c;传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度&#xff0c;通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

《基于Apache Flink的流处理》笔记

思维导图 1-3 章 4-7章 8-11 章 参考资料 源码&#xff1a; https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...

【开发技术】.Net使用FFmpeg视频特定帧上绘制内容

目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法&#xff0c;当前调用一个医疗行业的AI识别算法后返回…...

管理学院权限管理系统开发总结

文章目录 &#x1f393; 管理学院权限管理系统开发总结 - 现代化Web应用实践之路&#x1f4dd; 项目概述&#x1f3d7;️ 技术架构设计后端技术栈前端技术栈 &#x1f4a1; 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 &#x1f5c4;️ 数据库设…...

初探Service服务发现机制

1.Service简介 Service是将运行在一组Pod上的应用程序发布为网络服务的抽象方法。 主要功能&#xff1a;服务发现和负载均衡。 Service类型的包括ClusterIP类型、NodePort类型、LoadBalancer类型、ExternalName类型 2.Endpoints简介 Endpoints是一种Kubernetes资源&#xf…...

uniapp 字符包含的相关方法

在uniapp中&#xff0c;如果你想检查一个字符串是否包含另一个子字符串&#xff0c;你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的&#xff0c;但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...

通过MicroSip配置自己的freeswitch服务器进行调试记录

之前用docker安装的freeswitch的&#xff0c;启动是正常的&#xff0c; 但用下面的Microsip连接不上 主要原因有可能一下几个 1、通过下面命令可以看 [rootlocalhost default]# docker exec -it freeswitch fs_cli -x "sofia status profile internal"Name …...

保姆级【快数学会Android端“动画“】+ 实现补间动画和逐帧动画!!!

目录 补间动画 1.创建资源文件夹 2.设置文件夹类型 3.创建.xml文件 4.样式设计 5.动画设置 6.动画的实现 内容拓展 7.在原基础上继续添加.xml文件 8.xml代码编写 (1)rotate_anim (2)scale_anim (3)translate_anim 9.MainActivity.java代码汇总 10.效果展示 逐帧…...

Axure 下拉框联动

实现选省、选完省之后选对应省份下的市区...