动态规划 – 不同的二叉搜索树
什么是动态规划?
将一个问题拆分成一个个子问题,直到子问题可以直接解决。然后将子问题答案保存以减少重复计算,并通过子问题答案反推,得出原问题解的一种方式。
核心思想:拆分子问题,记住过往,减少重复计算
解题思路:
- 穷举分析
- 确定边界
- 找出规律,确定最优子结构
- 写出状态转移方程
什么是二叉搜索树?
二叉搜索树BST(Binary Search/ Sort Tree),也称为二叉查找树,二叉排序树,顾名思义是二叉树的特种树,专注于检索,特点是让节点按照数据的一定的规律摆放,从而让搜索某个节点特别的高效。
特点:
- 左子树上所有结点的值均小于等于它的根节点值
- 右子树上所有结点的值均大于等于它的根节点值
利用动态规划解决问题
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入:n = 3
输出:5
示例 2:
输入:n = 1
输出:1
- 穷举分析
对于1-n的数,我们遍历每一个数字,将当前数字定为i;
当i = 1时,生成的二叉搜索树为1
当i = 2时,1作为根节点时,2作为它的右节点;2作为根节点时,1作为它的左节点
当i = 3时:排列出5种不同的二叉搜索树
- 1作为根节点时,2、3作为右节点,同时对于2、3参照i=2时可以排列出两种符合二叉搜索树的节点;
- 2作为根节点时,只能是1作为左节点,3作为右节点
- 3作为根节点时,1、2作为右节点,参照i = 2 时进行排列,可以得出两种结果。
当 i 作为根节点时,将1… (i-1)作为左节点,(i+1) … n作为右节点;
- 确定边界
定义G(n)函数:长度为n的序列能构成的不同二叉搜索树的个数
定义F(i,n):当i为根、序列长为n时能构成的不同二叉搜索树的个数
从穷举分析中得出结论:G(n)=i=∑F(i,n)
边界情况:序列长度为1和序列长度为0 时,G(0) = 1;G(1) = 1
- 找出规律,确定最优子结构
给定序列 1…n,我们选择数字 i 作为根,则根为 i 的所有二叉搜索树的集合是左子树集合和右子树集合的笛卡尔积。
例如:不同二叉搜索树的个数为F(4,9)。其中[1,2,3]作为左节点,[5,6,7,8,9]作为右节点。
- [1,2,3]构建的左子树个数为G(3);[5,6,7,8,9]构建的右子树个数为G(5)
- 因此 F(4,9) = G(3) * G(5)
从上述分析中得出,G(n)的结果只和序列长度有关 因此推出公式 *F(i,n) = G(i-1)G(n-i)
- 写出状态转移方程
将 G(n)=i=∑F(i,n) 与 **F(i,n) = G(i-1)*G(n-i)**相结合,推出状态转移方程:*G(n) = ∑ G(i-1)G(n-i) ( i 属于 1-n)
根据推论出的状态转移方程解题:
/**
* @param {number} n
* @return {number}
*/
var numTrees = function(n) {
//定义一个长度为n+1的数组
const G = new Array(n+1).fill(0);
//定义边界条件
G[0] = 1;
G[1] = 1;
//实现公式条件
//G(2) = G(0)*G(1) + G(1)*G(0) = 2
//G(3) = G(0)*G(2) + G(1)*G(1) + G(2)*G(0) = 5
for(let i = 2; i <= n; i++){
for(let j = 1; j <= i; j++){
G[i] += G[j-1] * G[i-j];
}
}
return G[n];
};