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.**斐波那契数
斐波那契数,通常用 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
java
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.**爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
- 输入: 2
- 输出: 2
- 解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
- 输入: 3
- 输出: 3
- 解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
java
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.**使用最小花费爬楼梯
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
提示:
- cost 的长度范围是 [2, 1000]。
- cost[i] 将会是一个整型数据,范围为 [0, 999] 。
java
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.**不同路径
一个机器人位于一个 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
java
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
一个机器人位于一个 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
java
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.**整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
- 输入: 2
- 输出: 1
- 解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
- 输入: 10
- 输出: 36
- 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
- 说明: 你可以假设 n 不小于 2 且不大于 58。
java
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.**不同的二叉搜索树
给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?
二叉搜索树
二叉搜索树(Binary Search Tree,BST),也称为二叉排序树或二叉查找树,是一种特殊的二叉树数据结构。它满足以下性质:
- 节点值特性: 对于树中的每个节点,其左子树中的所有节点的值都小于该节点的值,而其右子树中的所有节点的值都大于该节点的值。
- 有序性: 二叉搜索树的中序遍历会得到一个升序的序列。这一特性使得二叉搜索树在查找、插入和删除操作上具有较高的效率。
java
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 种不同的二叉搜索树。