目录
一、题解部分
1.1题目
1.2铺垫
1.3.题解:
二、法一:递归实现
1.输入数据,创建动态数组
2.断言dp指针,并给它赋值
3.打印结果并调用函数
3.1注意:
4.实现函数binarytree
4.1先将动态数组dp[i]中特殊的值给出来,比如i=1,i=0时
4.2然后循环遍历节点的数量为i时,根节点j的不同取值情况,并递归求出不同的dp[i]
4.3返回dp[n]的值
4.4函数binarytree的完整代码
5.完整C语言代码
三、法二:递推求解(从前往后)
1.循环实现
2.方法二完整C语言代码
四、总结
这里我们将题目给复制过来,方便大家浏览
也可以点击下面的友情链接,进入牛客网看题目点击此处跳转
DP5 有多少个不同的二叉搜索树
给定一个由节点值从 1 到 n 的 n 个节点。请问有多少种不同的方法用这 n 个节点构成互不相同的二叉搜索树。
数据范围:1≤n≤19
仅一行输入一个正整数 n ,表示节点的数量。
输出组成不同二叉搜索树的方法树
在写这道题之前,我们首先要理解这样一个问题,即什么是二叉树?这里我们附上相关链接点这里直接跳转-->二叉树的概念,在了解完二叉树的概念之后,二叉树有着这样一个性质即:1.每棵子树头节点的值都比各自左子树上所有节点值要大,也都比各自右子树上所有节点值要小,2.根节点值可以从1到n可以任意取值,这里我们给出了节点值从1到6的二叉搜索树的其中两种情况,希望可以帮助你更好的理解二叉树的两个性质
1.定义dp[i]表示节点数目为i时组成不同二叉树的方法数,注意节点元素值是从1开始的
2.由于二叉树左子树比根节点小,右子树比根节点要大,所以根节点不同也会出现不同种类的二叉搜索树,我们用j来表示当前根节点值,其中j的取值范围为(0
3.我们现在可以知道一个任意的节点数为i的二叉搜索树有j种方式,这些方式相互独立所以是相加,这里涉及高中数学的排列组合相加与相乘原理,我们给出相关链接: 点击直接跳转 --> 排列组合之相加相乘原理的区别
4.单看其中任意一种方式假设此时根节点的节点值已经确定就是j,节点的数量为i(0两个步骤
1.排好j的左子树 2.排好j的右子树
想得到根节点值为j时排这个节点数目为i的二叉树的方法数,只需要把这两个子二叉树的方法数相乘就可
5.左子树:节点元素为:1~(j-1)共j-1个节点 排这个子二叉树方法数为dp[j-1]
6.右子树:节点元素为:(j+1)~i 共i-j个节点 排这个子二叉树方法数为dp[i-j]
这个地方要注意 二叉树的方法数与节点元素的具体值无关,只与节点的数量有关
7.表达式:dp[i] = sum(dp[j-1] + dp[i-j]),sum指把所有的不同的根节点情况的方法数相加的结果
int n = 0;
scanf("%d", &n);
int* dp = (int*)malloc((n + 1) * sizeof(int));//申请一个动态数组dp[i]来表示节点数为i时表示的不同二叉树搜索树的种数
assert(dp);//断言,确保已经申请成功,防止出现空指针的情况
memset(dp, 0, (n + 1) * sizeof(int));//初始化数组dp为0
printf("%d\n", binarytree(n, dp));
为了使我们main函数部分变得更简洁,我们将创建一个叫binarytree的函数,它的功能也很单一,即求节点的数量为n时,其表示的不同二叉搜索树的方法数,并返回给我们的main函数。
p[0] = 1;//只有左子树或者右子树的情况,自然另一边的情况唯一,方法数为1。
p[1] = 1;
注意,i == 1时很好理解,此时节点的数量为1,自然能表示的二叉搜索树的种类就只有一种,关键在于这个i == 0的情况是该如何理解,想一想,假设此时我想求一个节点数为i,根节点为j所能表示的二叉搜索树的种类,按照我们之前在题解中的观点,我想得到其中一个二叉搜索树,要分为两步,先排左子树,再排右子树,再将这两个子树相应节点数组成的二叉搜索树的方法树相乘即可,但现在如果根节点值j取到一个特殊值,只有比它大的节点或者比它小的节点了,那该怎么办呢?很明显,这如果是一道数学题,你应该马上会回答出来,节点数量不为0的那一段子树所组成的二叉搜索树的种树就是我们节点数为i,根节值点为j所能表示的二叉搜索树的方法数了!!!那是不是就等价于dp[0] = 1了呢,很显然,是这样的。
for (int j = 1; j <= n; j++)//循环遍历所有不同根节点的情况
{
/*1.谁不知道就去递归求谁然后用动态数组保存起来,
避免重复的去求*/
/*2.这里要注意的一点是,节点元素i~i+3 组成二叉树的方法数目与节点元素1~4组成二叉树的方法种树相同*/
/*3. 节点元素为i~j d节点数目为(i-j+1)*/
if (p[j - 1] > 0 && p[n - j] > 0)
p[n] += p[j - 1] * p[n - j];
else if (p[j - 1] > 0)
p[n] += p[j - 1] * binarytree(n - j, p);
else if (p[n - j] > 0)
p[n] += binarytree(j - 1, p) * p[n - j];
else p[n] += binarytree(j - 1, p) * binarytree(n - j);
}
注意由于我们已经初始化数组dp为0,那么就可以通过dp[i]是否等于0来知道其是否已知,如果未知就通过递归,从后往前,第一次大的递归一直递归到n == 1 或者n == 2就会返回值(因为我们递归开始之前就给p[0]和p[1]赋了值),之后的递归会越来越快,因为已知数变得越来越多,从而一步一步算出我们想要的那个值。
return p[n];//返回需要的值
int binarytree(int n, int* p)
{
p[0] = 1;//只有左子树或者右子树的情况,自然另一边的情况唯一,方法数为1。
p[1] = 1;
if(n > 1)
{
for (int j = 1; j <= n; j++)//循环遍历所有不同根节点的情况
{
/*1.谁不知道就去递归求谁然后用动态数组保存起来,
避免重复的去求*/
/*2.这里要注意的一点是,节点元素i~i+3 组成二叉树的方法数目与节点元素1~4组成二叉树的方法种树相同*/
/*3. 节点元素为i~j d节点数目为(i-j+1)*/
if (p[j - 1] > 0 && p[n - j] > 0)
p[n] += p[j - 1] * p[n - j];
else if (p[j - 1] > 0)
p[n] += p[j - 1] * binarytree(n - j, p);
else if (p[n - j] > 0)
p[n] += binarytree(j - 1, p) * p[n - j];
else p[n] += binarytree(j - 1, p) * binarytree(n - j);
}
}
return p[n];//返回需要的值
}
注意n==1要单独考虑,因为我们已经在递归的开始就给dp[1]赋上了值,不需要进入循环再算一遍
//法一:动态规划之递归
#include
#include
#include
#include
int binarytree(int n, int* p)
{
p[0] = 1;//只有左子树或者右子树的情况,自然另一边的情况唯一,方法数为1。
p[1] = 1;
if(n > 1)
{
for (int j = 1; j <= n; j++)//循环遍历所有不同根节点的情况
{
/*1.谁不知道就去递归求谁然后用动态数组保存起来,
避免重复的去求*/
/*2.这里要注意的一点是,节点元素i~i+3 组成二叉树的方法数目与节点元素1~4组成二叉树的方法种树
相同*/
/*3. 节点元素为i~j d节点数目为(i-j+1)*/
if (p[j - 1] > 0 && p[n - j] > 0)
p[n] += p[j - 1] * p[n - j];
else if (p[j - 1] > 0)
p[n] += p[j - 1] * binarytree(n - j, p);
else if (p[n - j] > 0)
p[n] += binarytree(j - 1, p) * p[n - j];
else p[n] += binarytree(j - 1, p) * binarytree(n - j);
}
}
return p[n];//返回需要的值
}
int main()
{
int n = 0;
scanf("%d", &n);
int* dp = (int*)malloc((n + 1) * sizeof(int));//申请一个动态数组dp[i]来表示节点数为i时的能表示的不同二叉树的种数
assert(dp);//断言,确保已经申请成功
memset(dp, 0, (n + 1) * sizeof(int));//初始化数组dp为0
printf("%d\n", binarytree(n, dp));
return 0;
}
注意这个方法二与方法一只有算法实现部分有所区别,所以前面的雷同部分我们直接省略
从前往后递推求dp[n],只需注意i从2开始计算即可,因为i=1和i=0已经已知了。
for (int i = 2; i <= n; i++)//循环递推一步一步得出dp[n],注意n == 1要分开讨论,防止出错
for (int j = 1; j <= i; j++)//1~i谁都可以成为根节点,每一种情况都不同,需要都加起来
{
dp[i] += dp[j - 1] * dp[i - j];
}//每一次第二个循环结束都可以算出一个值,即节点数为i(i > 1)组成的不同二叉树的方法树
我们直接给出完整代码
//法二:循环解法(递推)
#include
#include
#include
#include
int main() {
int n = 0;
scanf("%d", &n);
int* dp = (int*)malloc((n + 1) * sizeof(int));//申请动态数组存储节点数为i时能组成的不同二叉树的方法数
assert(dp);//确保dp已经申请成功
memset(dp, 0, (n + 1) * sizeof(int));初始化dp的值为0
p[0] = 1;//只有左子树或者右子树的情况,自然另一边的情况唯一,方法数为1。
p[1] = 1;
for (int i = 2; i <= n; i++)//循环递推一步一步得出dp[n],注意n == 1要分开讨论,防止出错
for (int j = 1; j <= i; j++)//1~i谁都可以成为根节点,每一种情况都不同,需要都加起来
{
dp[i] += dp[j - 1] * dp[i - j];
}//每一次第二个循环结束都可以算出一个值,即节点数为i(i > 1)组成的不同二叉树的方法树
printf("%d", dp[n]);
return 0;
}
抛开时间复杂度和空间复杂度不谈,方法二的代码复杂度更得我青睐,这个题目与跳台阶和斐波拉契数列问题的不同之处是点击此处跳转斐波拉契(这里我们给出友情链接,有兴趣的朋友可以看一下),递推求解的时候每一次dp不再简单的只和最近的几个值有关,而是由于根节点j的不同,每一个值都不只用到一次,所以要把他们都用动态数组存起来,便于我们的使用,额外空间o(n)不能节省,最后谢谢大家的观看,欢迎评论区与我互动交流,共同进步!!!