题目链接: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...i−1] 这
i
−
1
i-1
i−1 个数字全部入栈,之后
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=1∑nf[i−1]∗f[n−i]
特别地,对于 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 n−i−j 的状态下出栈序列的方案数。显然,最终状态是 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[i−1][j+1]+f[i][j−1]
其中,
f
[
i
−
1
]
[
j
+
1
]
f[i-1][j+1]
f[i−1][j+1] 表示入栈一个数字,
f
[
i
]
[
j
−
1
]
f[i][j-1]
f[i][j−1] 表示出栈一个数字。
注意的是,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+C2Cn−1+...+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)=C2nn−C2nn−1.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[n−1]∗n+14n−2,其中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=1∑nf[i−1]∗f[n−i]
不难发现,在求解的过程中, 某个子状态的规模即可能出现在某些数字的左边也可能出现在右边,也就是左右两边有对称的性质,我们在计数的时候就可以把枚举的终点折半,而每次算的时候去乘以 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];
}
};