题目
给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?
示例:
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
题目解读
二叉搜索树
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。(来自百度百科)
动态规划
没看tag和题解前,我自己没想到用动规做,看了题解后豁然开朗,整理一些题解的重点。
- 当一个问题可以分解成规模较小的两个子问题,且子问题的解可以复用时,就要想到使用动态规划。举例,本题中,以当前结点为根的二叉搜索树,它的左子树和右子树也是二叉搜索树,所以子问题的求解方式是一样的,因此应该要想到用dp
- 使用动规时,把状态转移和极限条件写出来,然后从极限条件开始一个个算出每个状态的值
题目思路
官方题解法一讲的已经很好了,我重复一遍。
首先,把当前问题用数学方法表达出来。要求1……n的二叉搜索树种类,我们假设一个函数
G(n)
用来表示长度为n(即序列是 1……n 时)的二叉搜索树种类。
显然这n个数字,都可以作为整个树的根节点,所以我们的根节点有n中选择。当确定了根节点 i 后,因为二叉搜索树的性质决定了它左子树值小于根节点,右子树值大于根节点,所以根结点 i 的左子树必然是由1……i-1 这个序列构成的,右子树则是 i+1 …… n 。而在其左右子树中,也分别可以选定一个根节点,继续进行左右子树的设计。
当 n=0 时,二叉搜索树为空,所以只有一种可能的二叉搜索树;当 n=1 时,只有一个根节点,所以也是一种。
当 n>1 时,以节点 i 作为根节点时,它组成的二叉搜索树的种类,等于它左子树( 1 …… i-1 )的种类乘它右子树( i+1 …… n )的种类。这个很好理解,就比如一个人从A走到C,中途会经过B,他从A到B有三条路可以走,从B到C有四条路可以走,那么从A到C就有 3 x 4 = 12 种行走的方案。这里的情况也是一样的,数学名词把这个叫做笛卡尔积。
由上面的分析,我们可知:
G(n) = G( (i-1) - 1 + 1 ) * G( n- (i+1) +1 ) =G( i-1 ) * G( n-i )( i 可以取 1……n 这 n 个值)
等式右边的 G(i-1) 代表了以 i 为根节点时,左子树的种类,因为左子树序列为 1……i-1 ,总长度是 i-1 ,同样的,G(n- (i+1) ) 代表了右子树的种类。可以看到G的值与序列内容无关,而是与序列长度有关。这也很好理解,假设这题给的序列不是 1……n ,而是另外一个长度为 n 的序列,我们分析时,仍然是把它排序成从小到大的、长度为n的一个序列。这里的重点在于,当你选定根节点 i 后,根节点左右的所有值都小于它,并不需要在意这些值分别都是什么具体的值。
因此,由于当要求长度为 n 的序列的可能情况时,需要对根节点分别取 1……n种的每个值进行分析,每种情况相加就是最后的情况,所以有:
题目代码
class Solution
{
public:
int numTrees(int n)
{
vector<int> ans(n + 1, 1);
for (int len = 2; len <= n; len++)
{
int sum = 0;
for (int i = 1; i <= len; i++)
{
sum += ans[i - 1] * ans[len - i];
}
ans[len] = sum;
}
return ans[n];
}
};