96. Unique Binary Search Trees(不同的二叉搜索树)三种解法(C++ & 注释)

96. Unique Binary Search Trees(不同的二叉搜索树)

  • 1. 题目描述
  • 2. 暴力破解(Brute Force)
    • 2.1 解题思路
    • 2.2 实例代码
  • 3. 动态规划(Dynamic Programming)
    • 3.1 解题思路
    • 3.2 实例代码
  • 3. 卡特兰数(Catalan Number)
    • 3.1 解题思路
    • 3.2 实例代码
  • 4. 参考资料

1. 题目描述

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

示例:
96. Unique Binary Search Trees(不同的二叉搜索树)三种解法(C++ & 注释)_第1张图片
题目链接:中文题目;英文题目

2. 暴力破解(Brute Force)

2.1 解题思路

虽然暴力破解会超时,但是后面动态规划的思路基础是基于这里的暴力破解,所以建议不明白暴力破解的同学认真看看这里的方法。

BST分为三个部分:1)root节点;2)小于root的节点;3)大于root的节点;所以给定节点[1, 2, 3, …, n -1, n],选取任意一个节点为root节点,那么整个树的结构可以概括为:
96. Unique Binary Search Trees(不同的二叉搜索树)三种解法(C++ & 注释)_第2张图片
当固定root后,剩余的树节点划分为两个部分。之后继续对剩余两个部分执行上述的步骤,直到某个部分只剩下1个节点或空节点则停止。那么n个节点形成的BST总个数则依次为:1~n分别为root的情况的总和。到这里,我们理解了递归的大部分思路,现在还剩下一个问题:为什么某个节点的BST总数关系是:左节点总数 * 右节点总数 ?回到上面的例子,来看看左右节点所能形成的BST总数:
96. Unique Binary Search Trees(不同的二叉搜索树)三种解法(C++ & 注释)_第3张图片
我们发现1个节点只能形成1种BST,两个节点可以形成2种BST,那么3为root的情况下,所能形成的BST总数为左右两边个数的乘积,即A分别和B、C进行组合;此部分对应代码:2.2.1 暴力破解(Brute Force, Time Limit Exceeded)

通过上面的分析,我们发现节点所能构成的BST总数与具体的节点无关,而与多少个节点有关。换句话说,[3,4]和[5,6]构成的BST总数都是2,以此类推,只要构成树的节点数相同,那么所能构建的树的总数也相同。所以基于这点就能很自然的想到用记忆方法,存储之前计算过的值,降低时间复杂度。此部分对应代码:2.2.2 记忆(Memorization)

2.2 实例代码

2.2.1 暴力破解(Brute Force, Time Limit Exceeded)

class Solution {
    int recursionMethod(int left, int right) {
        if (left >= right) return 1; // 只有一个节点或空节点都只能构成一种树

        int ans = 0;
        for (int i = left; i <= right; i++)
            ans += recursionMethod(left, i - 1) * recursionMethod(i + 1, right);

        return ans;
    }

public:
    int numTrees(int n) {
        return recursionMethod(1, n);
    }
};

2.2.2 记忆(Memorization)

class Solution {
    unordered_map<int, int> memorization;

    int recursionMethod(int left, int right) {
        if (memorization.count(right - left)) return memorization[right - left];
        if (left >= right) return 1;

        int ans = 0;
        for (int i = left; i <= right; i++)
            ans += recursionMethod(left, i - 1) * recursionMethod(i + 1, right);

        memorization[right - left] = ans;
        return ans;
    }

public:
    int numTrees(int n) {
        return recursionMethod(1, n);
    }
};

3. 动态规划(Dynamic Programming)

3.1 解题思路

在 2. 暴力破解(Brute Force)一章的分析中,我们得到了第一个重要的关系式:
96. Unique Binary Search Trees(不同的二叉搜索树)三种解法(C++ & 注释)_第4张图片
现在假设G(n)表示从1 ~ n所能形成的BST总数;F(i, n)表示以某个节点(i)为root,在剩下的[1, …, i - 1]和[i + 1, …, n]个节点中所能形成BST总数;那么结合暴力破解的思路,能递推出G(n)和F(i, n)的关系式:

Formula1:
G(n) = F(1, n) + F(2, n) + F(3, n) + … + F(n - 1, n) + F(n, n)

Formula1等价暴力解法的for循环;对于F(i, n),可以利用上图的关系进行分解,也就是递归中的“左节点总数 * 右节点总数”可以等价为:

Formula2:
F(i, n) = G([1, …, i - 1]) * G([i + 1, …, n])

G([1, …, i - 1]) 表示从1 ~ i - 1所能形成的BST总数;同理,G([i + 1, …, n])表示i + 1 ~ n所能形成的BST总数,我们又知道节点所能构成的BST总数与具体的节点无关,而与多少个节点有关,那么求两个G式子的节点数;或者使用G(n)的定义,Formula2可以改写成:

Formula3:
F(i, n) = G(i - 1 - 1 + 1) * G(n - (i + 1) + 1) = G(i - 1) * G(n - i)

根据Formula3,Formula1可以改写成:

Formula4:
G(n) = G(0) * G(n - 1) + G(1) * G(n - 2) + … + G(n - 1) * G(0)

得到Formula4,我们就得到了本题的DP递推公式。现在题目要我们求G(n),又已知G(0) = G(1) = 1,那么我们可求得所有的DP递推公式如下:

G(1) = G(0) * G(0);
G(2) = G(0) * G(1) + G(1) * G(0) ;
G(2) = G(0) * G(2) + G(2) * G(1) + G(2) * G(0)

G(n) = G(0) * G(n - 1) + G(1) * G(n - 2) + … + G(n - 1) * G(0)

所以我们从G(1)开始计算,算到G(n)即可得到答案。

3.2 实例代码

class Solution {
public:
    int numTrees(int n) {
        vector<int> memo(n + 1, 0);
        memo[0] = memo[1] = 1;

        for (int i = 2; i <= n; i++) // G(1), G(2), G(3) ...... G(n - 1), G(n), Jump G(1) over
            for (int j = 1; j <= i; j++) // G(n) = G(0) * G(n - 1) + G(1) * G(n - 2) ...... G(n - 1) * G(0)
                memo[i] += memo[j - 1] * memo[i - j]; // F(i, n) = G(i - 1) * G(n - i), 1 <= i <= n 
                
        return memo[n];
    }
};

3. 卡特兰数(Catalan Number)

3.1 解题思路

这道题还有用数学计算的方法,暂时看不懂,等会面有时间研究明白再更新这个方法,有兴趣的童鞋可以先参考下面两篇文章:

  1. A very simple and straight ans based on Math,Catalan Number ,O(N) times,O(1)space
  2. Exercises on Catalan and Related Numbers

3.2 实例代码

4. 参考资料

  1. Python Top-down DP | More straightforward | From brute force to DP
  2. Javascript and C++ solutions
  3. Simple Recursion Java Solution with Explanation
  4. Fantastic Clean Java DP Solution with Detail Explaination
  5. DP Solution in 6 lines with explanation. F(i, n) = G(i-1) * G(n-i)
  6. A very simple and straight ans based on Math,Catalan Number ,O(N) times,O(1)space

你可能感兴趣的:(LeetCode-Medium)