乐趣区

关于后端:月度刷题计划同款从区间-DP-到卡特兰数

题目形容

这是 LeetCode 上的 [96. 不同的二叉搜寻树](),难度为 中等

Tag :「树」、「二叉搜寻树」、「动静布局」、「区间 DP」、「数学」、「卡特兰数」

给你一个整数 n,求恰由 n 个节点组成且节点值从 1n 互不雷同的 二叉搜寻树 有多少种?

返回满足题意的二叉搜寻树的种数。

示例 1:

 输出:n = 3

输入:5

示例 2:

 输出:n = 1

输入:1

提醒:

  • $1 <= n <= 19$

区间 DP

沿用 95. 不同的二叉搜寻树 II 的基本思路,只不过本题不是求具体计划,而是求个数。

除了能用 95. 不同的二叉搜寻树 II 提到的「卡特兰数」间接求解 $n$ 个节点的以外,本题还能通过惯例「区间 DP」的形式进行求解。

求数量应用 DP,求所有具体计划应用爆搜,是极其常见的一题多问搭配。

定义 $f[l][r]$ 为应用数值范畴在 $[l, r]$ 之间的节点,所能构建的 BST 个数

不失一般性思考 $f[l][r]$ 该如何求解,仍用 $[l, r]$ 中的根节点 i 为何值,作为切入点进行思考。

依据「BST 定义」及「乘法原理」可知:$[l, i – 1]$ 相干节点形成的 BST 子树只能在 i 的右边,而 $[i + 1, r]$ 相干节点形成的 BST 子树只能在 i 的左边。所有的左右子树互相独立,因而以 i 为根节点的 BST 数量为 $f[l][i – 1] \times f[i + 1][r]$,而 i 共有 $r – l + 1$ 个取值($i \in [l, r]$)。

即有:

$$
f[l][r] = \sum_{i = l}^{r} (f[l][i – 1] \times f[i + 1][r])
$$

不难发现,求解区间 $[l, r]$ 的 BST 数量 $f[l][r]$ 依赖于比其小的区间 $[l, i – 1]$ 和 $[i + 1, r]$,这疏导咱们应用「区间 DP」的形式进行递推。

不理解区间 DP 的同学,可看 前置 🧀

一些细节:因为咱们 i 的取值可能会取到区间中的最值 lr,为了可能该状况下,$f[l][i – 1]$ 和 $f[i + 1][r]$ 可能顺利参加转移,起始咱们须要先对所有满足 $i >= j$ 的 $f[i][j]$ 初始化为 1

Java 代码:

class Solution {public int numTrees(int n) {int[][] f = new int[n + 10][n + 10];
        for (int i = 0; i <= n + 1; i++) {for (int j = 0; j <= n + 1; j++) {if (i >= j) f[i][j] = 1;
            }
        }
        for (int len = 2; len <= n; len++) {for (int l = 1; l + len - 1 <= n; l++) {
                int r = l + len - 1;
                for (int i = l; i <= r; i++) {f[l][r] += f[l][i - 1] * f[i + 1][r];
                }
            }
        }
        return f[1][n];
    }
}

C++ 代码:

class Solution {
public:
    int numTrees(int n) {vector<vector<int>> f(n + 2, vector<int>(n + 2, 0));
        for (int i = 0; i <= n + 1; i++) {for (int j = 0; j <= n + 1; j++) {if (i >= j) f[i][j] = 1;
            }
        }
        for (int len = 2; len <= n; len++) {for (int l = 1; l + len - 1 <= n; l++) {
                int r = l + len - 1;
                for (int i = l; i <= r; i++) {f[l][r] += f[l][i - 1] * f[i + 1][r];
                }
            }
        }
        return f[1][n];
    }
};

Python 代码:

class Solution(object):
    def numTrees(self, n):
        f = [[0] * (n + 2) for _ in range(n + 2)]
        for i in range(n + 2):
            for j in range(n + 2):
                if i >= j:
                    f[i][j] = 1
        for length in range(2, n + 1):
            for l in range(1, n - length + 2):
                r = l + length - 1
                for i in range(l, r + 1):
                    f[l][r] += f[l][i - 1] * f[i + 1][r]
        return f[1][n]

TypeScript 代码:

function numTrees(n: number): number {const f = new Array(n + 2).fill(0).map(() => new Array(n + 2).fill(0));
    for (let i = 0; i <= n + 1; i++) {for (let j = 0; j <= n + 1; j++) {if (i >= j) f[i][j] = 1;
        }
    }
    for (let len = 2; len <= n; len++) {for (let l = 1; l + len - 1 <= n; l++) {
            const r = l + len - 1;
            for (let i = l; i <= r; i++) {f[l][r] += f[l][i - 1] * f[i + 1][r];
            }
        }
    }
    return f[1][n];
};
  • 工夫复杂度:$O(n^3)$
  • 空间复杂度:$O(n^2)$

区间 DP(优化)

求解完应用 $[1, n]$ 共 $n$ 个间断数所能形成的 BST 个数后,再来思考一个问题:应用 $[L, R]$ 共 $n = R – L + 1$ 个间断数,所能形成的 BST 个数又是多少。

答案是一样的。

由 $n$ 个间断数形成的 BST 个数仅与数值个数有关系,与数值大小自身并无关系

因为可知,咱们上述的「区间 DP」必然进行了大量反复计算,例如 $f[1][3]$ 和 $f[2][4]$ 同为大小为 $3$ 的区间,却被计算了两次。

调整咱们的状态定义: 定义 $f[k]$ 为思考间断数个数为 $k$ 时,所能形成的 BST 的个数

不失一般性思考 $f[i]$ 如何计算,仍用 $[1, i]$ 中哪个数值作为根节点进行思考。假如应用数值 $j$ 作为根节点,则有 $f[j – 1] \times f[i – j]$ 个 BST 可奉献到 $f[i]$,而 $j$ 共有 $i$ 个取值($j \in [1, i]$)。

即有:

$$
f[i] = \sum_{j = 1}^{i}(f[j – 1] \times f[i – j])
$$

同时有初始化 $f[0] = 1$,含意为没有任何间断数时,只有“空树”一种非法计划。

Java 代码:

class Solution {public int numTrees(int n) {int[] f = new int[n + 10];
        f[0] = 1;
        for (int i = 1; i <= n; i++) {for (int j = 1; j <= i; j++) {f[i] += f[j - 1] * f[i - j];
            }
        }
        return f[n];
    }
}

C++ 代码:

class Solution {
public:
    int numTrees(int n) {vector<int> f(n + 10, 0);
        f[0] = 1;
        for (int i = 1; i <= n; i++) {for (int j = 1; j <= i; j++) {f[i] += f[j - 1] * f[i - j];
            }
        }
        return f[n];
    }
};

Python 代码:

class Solution:
    def numTrees(self, n: int) -> int:
        f = [0] * (n + 10)
        f[0] = 1
        for i in range(1, n + 1):
            for j in range(1, i + 1):
                f[i] += f[j - 1] * f[i - j]
        return f[n]

TypeScript 代码:

function numTrees(n: number): number {const f = new Array(n + 10).fill(0);
    f[0] = 1;
    for (let i = 1; i <= n; i++) {for (let j = 1; j <= i; j++) {f[i] += f[j - 1] * f[i - j];
        }
    }
    return f[n];
};
  • 工夫复杂度:$O(n^2)$
  • 空间复杂度:$O(n)$

数学 – 卡特兰数

在「区间 DP(优化)」中的递推过程,正是“卡特兰数”的 $O(n^2)$ 递推过程。通过对惯例「区间 DP」的优化,咱们得证 95. 不同的二叉搜寻树 II 中「给定 $n$ 个节点所能形成的 BST 的个数为卡特兰数」这一论断。

对于准确求卡特兰数,存在工夫复杂度为 $O(n)$ 的通项公式做法,公式为 $C_{n+1} = \frac{C_n \cdot (4n + 2)}{n + 2}$。

Java 代码:

class Solution {public int numTrees(int n) {if (n <= 1) return 1;
        long ans = 1;
        for (int i = 0; i < n; i++) ans = ans * (4 * i + 2) / (i + 2);
        return (int)ans;
    }
}

C++ 代码:

class Solution {
public:
    int numTrees(int n) {if (n <= 1) return 1;
        long long ans = 1;
        for (int i = 0; i < n; i++) ans = ans * (4 * i + 2) / (i + 2);
        return (int)ans;
    }
};

Python 代码:

class Solution:
    def numTrees(self, n: int) -> int:
        if n <= 1:
            return 1
        ans = 1
        for i in range(n):
            ans = ans * (4 * i + 2) // (i + 2)
        return ans

TypeScript 代码:

function numTrees(n: number): number {if (n <= 1) return 1;
    let ans = 1;
    for (let i = 0; i < n; i++) ans = ans * (4 * i + 2) / (i + 2);
    return ans;
};
  • 工夫复杂度:$O(n)$
  • 空间复杂度:$O(1)$

最初

这是咱们「刷穿 LeetCode」系列文章的第 No.96 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,局部是有锁题,咱们将先把所有不带锁的题目刷完。

在这个系列文章外面,除了解说解题思路以外,还会尽可能给出最为简洁的代码。如果波及通解还会相应的代码模板。

为了不便各位同学可能电脑上进行调试和提交代码,我建设了相干的仓库:https://github.com/SharingSource/LogicStack-LeetCode。

在仓库地址里,你能够看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其余优选题解。

更多更全更热门的「口试 / 面试」相干材料可拜访排版精美的 合集新基地 🎉🎉

退出移动版