LeetCode 96 不同的二叉搜索树(动态规划 | 递推 | Catalan数)

题目链接:leetcode96

题面

在这里插入图片描述

题目大意

求 n 个数字构成的二叉搜索树的种类数量

解题思路

与本题等价的题目有:凸多边形划分三角形的数量、一串数字通过栈构成的出栈序列数量、2n 长度的 01 串的种类(限制是前缀 1 的数量不少于 0 )等,最终其实可以归结于 Catalan 数的计数问题。以下主要介绍两种动态规划思想的递推,为了方便讲解,问题不妨假设转化为数字的进栈出栈问题:

递推

我们考虑,对于一串规模为 n 的数字,对于状态 f [ i ] f[i] f[i] 表示把第 i i i 个数字为开头的出栈序列种类数。显然,我们需要把该数字前面的 v a l [ 1... i − 1 ] val[1...i-1] val[1...i1] i − 1 i-1 i1 个数字全部入栈,之后 v a l [ i ] val[i] val[i] 入栈并出栈,然后再把后续的 v a l [ i + 1... n ] val[i+1...n] val[i+1...n] 的数字入栈,最终形成的出栈序列数即为答案。于是,我们可以形成这样一个式子:
f [ i ] = ∑ i = 1 n f [ i − 1 ] ∗ f [ n − i ] f[i] = \sum_{i=1}^{n} f[i-1]*f[n-i] f[i]=i=1nf[i1]f[ni]
特别地,对于 n = 0 的情况,f[0] = 1。相当于一个数字都没有的时候也有一种种类。

对于递推的时候,由于要使用到更小规模的答案,我们可以先把小规模的答案先计算好,也就是递推顺序是从 1 到 n。

时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( n ) O(n) O(n)

动态规划

我们可以考虑按动态规划的思想去思考本题,设 f [ i ] [ j ] f[i][j] f[i][j] 表示未入栈的数字数为 i i i ,待出栈的数字数为 j j j,已出栈的数字数为 n − i − j n-i-j nij 的状态下出栈序列的方案数。显然,最终状态是 f [ 0 ] [ 0 ] f[0][0] f[0][0],其值为 1 ,因为此时出栈序列已经固定下来。同理, f [ 0 ] [ 1... n ] f[0][1...n] f[0][1...n] 的值均为 1 ,因为没有数字未入栈也就表示数字序列已经固定。

问题的关键是如何转移这些状态?显然,我们目标是求得起始状态 f [ n ] [ 0 ] f[n][0] f[n][0] ,也就是我们已知最终状态求起始状态去考虑,于是状态转移方程:
f [ i ] [ j ] = f [ i − 1 ] [ j + 1 ] + f [ i ] [ j − 1 ] f[i][j] = f[i-1][j+1] + f[i][j-1] f[i][j]=f[i1][j+1]+f[i][j1]
其中, f [ i − 1 ] [ j + 1 ] f[i-1][j+1] f[i1][j+1] 表示入栈一个数字, f [ i ] [ j − 1 ] f[i][j-1] f[i][j1] 表示出栈一个数字。

注意的是,i+j 不能大于 n ,也就是保证状态合法;栈中的数量为 0 时不能出栈;栈中的数量为 n 时不能进栈。

时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( n 2 ) O(n^2) O(n2),这边空间可以利用滚动数组进行优化,空间复杂度也可以达到 O ( n ) O(n) O(n)

Catalan 数+递推

定义式

C n + 1 = C 1 C n + C 2 C n − 1 + . . . + C n C 1 C_{n+1}=C_1C_n+C_2C_{n-1}+...+C_nC_1 Cn+1=C1Cn+C2Cn1+...+CnC1

通项公式

f ( n ) = C 2 n n − C 2 n n − 1 . f ( n ) = C 2 n n n + 1 f(n)=C_{2n}^{n}-C_{2n}^{n-1}\\ .\\ f(n)=\frac{C_{2n}^{n}}{n+1} f(n)=C2nnC2nn1.f(n)=n+1C2nn

递推公式

f [ n ] = f [ n − 1 ] ∗ 4 n − 2 n + 1 , 其 中 f [ 0 ] = 1 f[n]=f[n-1]*\frac{4n-2}{n+1},其中f[0]=1 f[n]=f[n1]n+14n2,f[0]=1

本题可以根据递推公式进行求值

时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)

递推优化

我们考虑法一的公式:
f [ i ] = ∑ i = 1 n f [ i − 1 ] ∗ f [ n − i ] f[i] = \sum_{i=1}^{n} f[i-1]*f[n-i] f[i]=i=1nf[i1]f[ni]
不难发现,在求解的过程中, 某个子状态的规模即可能出现在某些数字的左边也可能出现在右边,也就是左右两边有对称的性质,我们在计数的时候就可以把枚举的终点折半,而每次算的时候去乘以 2 。特别地当规模为奇数时,左右两边地规模一样,此时要算它的贡献是子状态的平方数。

时间复杂度 O ( n 3 2 ) O(n^{\frac{3}{2}}) O(n23),空间复杂度 O ( n ) O(n) O(n)

代码实现

动态规划

class Solution {
public:
    int numTrees(int n) {
        int f[n+1][n+1];
        for (int i=0; i<=n; i++) f[0][i] = 1;
        for (int i=1; i<=n; i++) {
            for (int j=0; j<=n; j++) {
                f[i][j] = 0;
                if (i+j>n) break;
                if (j > 0) f[i][j] += f[i][j-1];
                if (j < n) f[i][j] += f[i-1][j+1];
            }
        }
        return f[n][0];
    }
};
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页