腾讯音乐笔试题——好二叉树的个数

58 阅读3分钟

题目

image.png

这题和不同的二叉搜索树的思路很像 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];
}