动态规划汇总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])。
但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。
所以贪心解决不了动态规划的问题。
动态规划五步曲
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导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
:
- 因为
n = 5 > 1
,不满足if (n <= 1)
的条件,继续执行。 - 创建数组
dp
,长度为6
(n + 1
),并初始化dp[0] = 0
,dp[1] = 1
。 - 进入
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
。
- 当
- 循环结束,返回
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
:
- 创建数组
dp
,长度为4
(n + 1
),并初始化dp[0] = 1
,dp[1] = 1
。 - 进入
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 步上来。
- 当
- 循环结束,返回
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]
:
- 初始化
dp
数组:dp[0] = 10
,dp[1] = 15
。
- 进入
for
循环:- 当
i = 2
时:dp[2] = Math.min(dp[1], dp[0]) + cost[2]
dp[2] = Math.min(15, 10) + 20 = 10 + 20 = 30
。
- 当
- 循环结束后,计算并返回结果:
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 条路径可以到达右下角。
- 向右 -> 向右 -> 向下
- 向右 -> 向下 -> 向右
- 向下 -> 向右 -> 向右
示例 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 = 3
,n = 2
:
- 初始化
dp
数组:dp = [1, 1]
,因为第一行的每个格子都只有一种到达方式。
- 进入外层
for
循环:- 当
i = 1
(第二行):- 进入内层
for
循环:- 当
j = 1
:dp[1] += dp[0]
,此时dp[0] = 1
,dp[1] = 1
,所以dp[1] = 1 + 1 = 2
。
- 当
- 进入内层
- 此时
dp = [1, 2]
。
- 当
- 外层
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 条不同的路径:
- 向右 -> 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右 -> 向右
示例 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]
]
- 初始化:
m = 3
,n = 3
,dp = [0, 0, 0]
。- 初始化第一行:
- 因为
obstacleGrid[0][0] == 0
,dp[0] = 1
。 - 因为
obstacleGrid[0][1] == 0
,dp[1] = 1
。 - 因为
obstacleGrid[0][2] == 0
,dp[2] = 1
。此时dp = [1, 1, 1]
。
- 因为
- 计算第二行:
- 当
i = 1
:- 当
j = 0
,obstacleGrid[1][0] == 0
,dp[0]
保持不变为1
。 - 当
j = 1
,obstacleGrid[1][1] == 1
,dp[1] = 0
。 - 当
j = 2
,obstacleGrid[1][2] == 0
,dp[2]
由于j!= 0
,dp[2] += dp[1]
,此时dp[2] = 1 + 0 = 1
。此时dp = [1, 0, 1]
。
- 当
- 当
- 计算第三行:
- 当
i = 2
:- 当
j = 0
,obstacleGrid[2][0] == 0
,dp[0]
保持不变为1
。 - 当
j = 1
,obstacleGrid[2][1] == 0
,dp[1] += dp[0]
,dp[1] = 0 + 1 = 1
。 - 当
j = 2
,obstacleGrid[2][2] == 0
,dp[2] += dp[1]
,dp[2] = 1 + 1 = 2
。此时dp = [1, 1, 2]
。
- 当
- 当
- 返回结果:
- 返回
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
:
- 初始化
dp
数组,dp[2] = 1
。 - 当
i = 3
:- 内层循环:
- 当
j = 1
,j * (i - j) = 1 * 2 = 2
,j * dp[i - j] = 1 * dp[2] = 1 * 1 = 1
,dp[3] = Math.max(dp[3], Math.max(2, 1)) = 2
。
- 当
- 内层循环:
- 当
i = 4
:- 内层循环:
- 当
j = 1
,j * (i - j) = 1 * 3 = 3
,j * dp[i - j] = 1 * dp[3] = 1 * 2 = 2
,dp[4] = Math.max(dp[4], Math.max(3, 2)) = 3
。 - 当
j = 2
,j * (i - j) = 2 * 2 = 4
,j * dp[i - j] = 2 * dp[2] = 2 * 1 = 2
,dp[4] = Math.max(dp[4], Math.max(4, 2)) = 4
。
- 当
- 内层循环:
- 当
i = 5
:- 内层循环:
- 当
j = 1
,j * (i - j) = 1 * 4 = 4
,j * dp[i - j] = 1 * dp[4] = 1 * 4 = 4
,dp[5] = Math.max(dp[5], Math.max(4, 4)) = 4
。 - 当
j = 2
,j * (i - j) = 2 * 3 = 6
,j * dp[i - j] = 2 * dp[3] = 2 * 2 = 4
,dp[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
:
- 初始化
dp
数组,dp[0] = 1
,dp[1] = 1
。 - 当
i = 2
:- 内层循环:
- 当
j = 1
,dp[1] = 1
,dp[0] = 1
,dp[2] += dp[0] * dp[1] = 1 * 1 = 1
。 - 当
j = 2
,dp[1] = 1
,dp[0] = 1
,dp[2] += dp[1] * dp[0] = 1 * 1 = 1
,所以dp[2] = 2
。
- 当
- 内层循环:
- 当
i = 3
:- 内层循环:
- 当
j = 1
,dp[0] = 1
,dp[2] = 2
,dp[3] += dp[0] * dp[2] = 1 * 2 = 2
。 - 当
j = 2
,dp[1] = 1
,dp[1] = 1
,dp[3] += dp[1] * dp[1] = 1 * 1 = 1
。 - 当
j = 3
,dp[2] = 2
,dp[0] = 1
,dp[3] += dp[2] * dp[0] = 2 * 1 = 2
,所以dp[3] = 5
。
- 当
- 内层循环:
最终,dp[3] = 5
,即有 3 个节点时,可以形成 5 种不同的二叉搜索树。
相关文章:

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

【计算机网络】lab5 ARP协议
🌈 个人主页:十二月的猫-CSDN博客 🔥 系列专栏: 🏀计算机网络_十二月的猫的博客-CSDN博客 💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前言 2.…...

分布式缓存redis
分布式缓存redis 1 redis单机(单节点)部署缺点 (1)数据丢失问题:redis是内存存储,服务重启可能会丢失数据 (2)并发能力问题:redis单节点(单机)部…...

【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网关的应用
在现代工业自动化领域,开疆智能CCLINKIE转ModbusTCP网关扮演着至关重要的角色,尤其是在立体仓库的应用中。立体仓库系统通过高度集成的自动化设备和先进的信息技术,实现了物料存储和管理的高效率。CCLINKIE转ModbusTCP网关作为连接不同工业通…...
ASP.NET Core与GraphQL集成
一、引言:探索 C# 与ASP.NET Core、GraphQL 的协同魅力 在当今数字化浪潮中,Web 开发领域不断演进,新技术层出不穷。C# 作为.NET 平台上的中流砥柱,凭借其强大的功能与优雅的语法,成为众多开发者构建各类应用程序的得…...
Zabbix 从入门到精通
一、Zabbix 简介 1.1 什么是 Zabbix Zabbix 是一个基于 Web 界面的提供分布式系统监视以及网络监视功能的企业级开源解决方案。它能监控各种网络参数,保证服务器系统的安全运营;并提供灵活的通知机制以让系统管理员快速定位 / 解决存在的各种问题。 1…...
文生图模型的技术原理、训练方案与微调方案
文生图模型的技术原理、训练方案与微调方案 引言 文生图(Text-to-Image)模型是一类能够根据文本描述生成对应图像的深度学习模型。近年来,随着生成对抗网络(GANs)和扩散模型(Diffusion Models)等技术的进步,文生图模型在图像生成领域取得了显著的进展。本文将详细介绍…...

3_CSS3 渐变 --[CSS3 进阶之路]
CSS3 引入了渐变(gradients),它允许在两个或多个指定的颜色之间显示平滑的过渡。CSS3 支持两种类型的渐变: 线性渐变(Linear Gradients):颜色沿着一条线性路径变化,可以是水平、垂直…...
国内主流的Spring微服务方案指南
构建一个完整的 Spring 微服务方案涉及多个关键组件的集成与配置,包括服务注册与发现、配置管理、API 网关、负载均衡、服务调用、熔断与限流、消息中间件、分布式追踪、服务网格、容器编排以及数据库与缓存等。以下将结合前述内容,详细介绍一个完整的中…...
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 中,合理的错误处理机制不仅能够提升用户体验,还能帮助开发者快速定位问题;而有效的日志管理能够帮助团队监控应用运行状态,及时发现和解决问题。 1. 常见错误…...

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

【论文笔记】多个大规模数据集上的SOTA绝对位姿回归方法:Reloc3r
abstract 视觉定位旨在确定查询图像相对于姿势图像数据库的相机姿势。 近年来,直接回归相机姿势的深度神经网络由于其快速推理能力而受到欢迎。 然而,现有方法很难很好地推广到新场景或提供准确的相机姿态估计。 为了解决这些问题,我们提出了…...

springMVC---常用注解
目录 一、创建项目 1.依赖 2.web.xml 3.spring-mvc.xml 二、RequestParam注解 1.作用 2.属性 3.代码 DeptController类 启动tomcat 三、RequestBody注解 1.作用 2.属性 3.代码 (1)DeptController类 (2)index.jsp (3)启动tomcat 四、P…...
青龙面板脚本开发指南:高效自动化任务的实现
青龙面板脚本开发指南:高效自动化任务的实现 青龙面板(Qinglong Panel)是一款强大的任务管理平台,支持多种语言的脚本开发和执行。通过在青龙面板中编写和管理脚本,用户可以轻松实现自动化任务,提高工作效…...
深入详解DICOM医学影像定位线相关知识:理解定位线的概念、定位线的作用以及定位线显示和计算原理
DICOM医学影像中的定位线(Localization Line) 在医学影像学中,DICOM是用于存储和交换医学影像的标准格式。定位线(Localization Line)在医学影像的显示和分析中起着重要作用,它帮助医生和医学专业人员在影像中精确地标定重要的解剖结构、区域或特征,辅助进行定位、治疗计…...

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

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

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

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...

TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...

Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业
6月9日,国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解,“超级…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
C++ 基础特性深度解析
目录 引言 一、命名空间(namespace) C 中的命名空间 与 C 语言的对比 二、缺省参数 C 中的缺省参数 与 C 语言的对比 三、引用(reference) C 中的引用 与 C 语言的对比 四、inline(内联函数…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...

【C++特殊工具与技术】优化内存分配(一):C++中的内存分配
目录 一、C 内存的基本概念 1.1 内存的物理与逻辑结构 1.2 C 程序的内存区域划分 二、栈内存分配 2.1 栈内存的特点 2.2 栈内存分配示例 三、堆内存分配 3.1 new和delete操作符 4.2 内存泄漏与悬空指针问题 4.3 new和delete的重载 四、智能指针…...
CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝
目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为:一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...