题目
这题和不同的二叉搜索树的思路很像 leetcode 96. 不同的二叉搜索树
解题思路:动态规划的递推公式并不是这么好推导的,因为题目是千变万化的;我们可以利用递归的思想,一步一步转换为动态规划,因为动态规划是最接近于最优解的,而递归的过程最符合我们常人的思维,如果我们能在暴力解决题目后,然后再对这种暴力方法进行优化,这样才可以一步步演化成动态规划;
所以我们先来讲解一下如何利用递归暴力解决该题;
首先根据题目排除掉二叉树节点数为偶数个的情况,直接return 0 即可;无论如何也凑不成好二叉树这种结构;
然后找到递归的出口,这也是难点;
当整棵树上只有一个节点的时候,显然是有一种好二叉树结构的 可以记作为:节点个数n为1 , 结果count为1;
当整棵树上有3个节点时,显然也只有一种好二叉树结构,可以记作为:节点个数为n为3,结果count为1;
当整棵树上有 5 个节点时,可以将其分为左子树 3 个节点,右子树 1 个节点 或者左子树 1 个节点,右子树 3 个节点这样两种情况,如果把左子树看作单独的一棵树,左子树上不管1个节点还是3个节点都只有一种好二叉树结构;同理右子树也是一样;显然上面两种情况都各自只有一种好二叉树结构,所以当整棵树上有五个节点时,此时好二叉树的结构只有 2 种;
那么以此类推,当整棵树上的节点个数为n个时,就是将左子树上好二叉树不同结构的个数 ✖ 右子树上好二叉树不同结构的个数;递归的出口有了,同时递归的条件也有了;即将 n 分成 左子树节点数 1 ,对应的右子树节点数(n-1-1)(减第二个1的原因是需要减去头节点);左子树节点数为3,右子树对应节点数为 (n-3-1); 以此类推当左树上节点数为 n-2时,右树节点数 1;
假设整棵树有 7 个节点,那么好二叉树的结构总数应该等于 (左:1 ✖ 右:5) 情况1好二叉树个数+ (左:3 ✖ 右:3)+ 情况2的好二叉树个数+ (左:5 ✖ 右:1)情况三的好二叉树个数;(除去头节点个数,还剩 6 个节点可以分配)
递归的代码:
public static int getTreeNumRecursion(int n) {
if ((n & 1) != 1) {
return 0;
}
// Map<Integer, Integer> map = new HashMap<>();
return process(n);
}
public static int process(int n) {
if (n == 1 || n == 3) {
return 1;
}
int count = 0;
for (int i = 1; i < n; i += 2) {
// 左树上分i个节点,右树上分 n-i-1个节点,再去进行递归;
count += process(i) * process(n - i - 1);
}
return count;
}
递归显然是不足以通关的,因为它的时间复杂度太高了;
那么我们可以采用记忆化搜索的方式来进行优化,减少重复计算;
所谓记忆化搜索,就是使用一个(map集合或者其他集合)记录当前递归的结果,如果后面的递归过程和当前递归相同,那么直接返回当前递归结果即可,不用执行接下来的整个递归过程; 比如说当前左子树上面的节点个数为 5 个,将递归完成后的结果记录在map中后;如果右子树的节点个数为5个时,直接返回map中记录的 5 个节点所产生的结果即可,不用再次执行整个递归过程;
public static int getTreeNumRecursion(int n) {
if ((n & 1) != 1) {
return 0;
}
// 使用map来记录每个递归过程的结果,如果递归条件相同,直接返回记录的递归结果即可;
Map<Integer, Integer> map = new HashMap<>();
return process(n, map);
}
public static int process(int n, Map<Integer, Integer> map) {
// 如果条件满足,就说明了前面已经执行过当前递归过程了,直接返回前面递归的结果即可;
if (map.containsKey(n)) {
return map.get(n);
}
if (n == 1 || n == 3) {
return 1;
}
int count = 0;
for (int i = 1; i < n; i += 2) {
// 左树上分i个节点,右树上分 n-i-1个节点,再去进行递归;
count += process(i, map) * process(n - i - 1, map);
}
// 将当前递归结果记录在map中;
map.put(n, count);
return count;
}
改造动态规划:
public static int getTreeNum(int n) {
if ((n & 1) != 1) {
return 0;
}
// dp数组就相当于记忆化搜索过程中的map集合,用来记录节点个数为i时,总共的方法数有dp[i]种;
int[] dp = new int[n + 1];
// 表示节点为1时,好二叉树结构为1
dp[1] = 1;
// 外层循环实际上是在填满dp数组
for (int i = 3; i <= n; i += 2) {
// 内存循环是在从dp数组中取出值来进行计算
for (int j = 1; j < i; j += 2) {
// 递推公式和递归过程中的递归条件很类似; 都是将节点为i的树分成多种可能性去讨论,再进行累加;
dp[i] += dp[j] * dp[i - j - 1];
dp[i] %= 1000000007;
}
}
return dp[n];
}