卡特兰数

Posted optimjie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了卡特兰数相关的知识,希望对你有一定的参考价值。

欢迎来访

首先看一下卡特兰数。若一个数列(h_n)满足:

[h_n = sum_{i=0}^{n-1}h_i cdot h_{n-1-i} ]

则称(h_n)为卡特兰数列。

还有一种形式若:

[h_n = frac{C_{2n}^n}{n+1} ]

也称(h_n)为卡特兰数列

那什么样的问题的答案是卡特兰数呢?

满足条件的01序列

题目链接

分析

(n)(3)时,满足条件的(01)序列为:

000111
001011
001101
010101
010011

我们可以将(01)序列转化到一个二维的平面当中,如果是(0)则向右走,反之则向上走,因此我们会走到((n,n))点,那么在平面中任意前缀序列中(0)的个数都不少于(1)代表的意思就是,对于途中的每一个点,横坐标要大于等于纵坐标,即路线不能越过(y=x)这条线。

看一下以(n=6)的例子:


技术图片

图中橙色紫色满足条件。

那满足条件的路线一共有多少呢?直接求不是很好求,所以我们可以转化一下,用所有的路线减去不满足条件的路线,那不满足条件的路线有什么样的特点呢?

这样的路线上必然存在一点的纵坐标是大于横坐标的,即这样的路线一定经过了(y=x+1)这条线,以图中的褐色为例。我们将褐色的路线第一次经过(y=x+1)以后的路线关于(y=x+1)做轴对称,用黑色的线表示。我们可以发现所有不满足的条件的路线都可以通过这种方式将终点变为((n-1,n+1)),在这个例子中即为((5,7))。所有的路线为(C_{12}^6),因为一共有(12)个空,要放(6)(0)所以为(C_{12}^6),同理:不满条件的路线为(C_{12}^5)

所以在一般问题中即为:

[C_{2n}^n-C_{2n}^{n-1} ]

在对其进行转化:

(C_{2n}^n-C_{2n}^{n-1}= frac{(2n)!}{n!n!} - frac{(2n)!}{(n-1)!(n+1)!}= frac{(2n)!(n+1)-(2n)!n}{n!(n+1)!}= frac{(2n)!}{n!(n+1)!} = frac{(2n)!}{n!n!(n+1)} = frac{C_{2n}^n}{n+1})

转化后即为:

[frac{C_{2n}^n}{n+1} ]

正好为开始讲的卡特兰数的第二种方式。

C++代码

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int MOD = 1e9 + 7;

int n;

int qmi(int a, int b, int p) {
    int res = 1;
    while (b) {
        if (b & 1) res = (LL)res * a % MOD;
        b >>= 1;
        a = (LL)a * a % MOD;
    }    
    return res;
}

int main() {

    cin >> n;
    int a = 2 * n, b = n;

    int res = 1;
    // 求:2n * 2n-1 * ... * 2n-n+1
    for (int i = a; i >= a - b + 1; i--)
        res = (LL)res * i % MOD;
      
    // 除以 n! 之所以可以用费马小定理求逆元是因为,MOD为质数,且i不是MOD的倍数
    for (int i = b; i >= 1; i--)
        res = (LL)res * qmi(i, MOD - 2, MOD) % MOD;
    
    res = (LL)res * qmi(n + 1, MOD - 2, MOD) % MOD;

    cout << res << endl;

    return 0;
}

题目链接

分析

以样例为例,一共有五种方式:

1push 2push 3push 3pop 2pop 1pop 序列为321
1push 1pop 2push 2pop 3push 3pop 序列为123
1push 2push 2pop 1pop 3push 3pop 序列为213
1push 1pop 2push 3push 3pop 2pop 序列为132
1push 2push 2pop 3push 3pop 1pop 序列为231

我们发现是没有(312)这种序列的,因为(3)先出栈,就意味着,(3)曾经进栈,既然(3)都进栈了,那就意味着,(1)(2)已经进栈了,此时(2)一定在(1)上面,也就是更接近栈顶,所以(2)一定会先比(1)出栈,也就没有(312)这种序列。

(push)(pop)的过程中必须满足:栈内有元素才能(pop),也就是说任意(push)(pop)序列的前缀中(push)的数量必须大于等于(pop)的数量,这样就与满足条件的(01)序列问题一样了。

C++代码

由于本题的数据范围很小所以求组合数时直接使用递推式:(C_n^m=C_{n-1}^m+C_{n-1}^{m-1})预处理出所有的组合数。

#include <bits/stdc++.h>

using namespace std;

const int N = 40;

int n;
long long c[N][N];

void init() {
    for (int i = 0; i < N; i++)
        for (int j = 0; j <= i; j++)
            if (!j) c[i][j] = 1;
            else c[i][j] = c[i - 1][j] + c[i - 1][j - 1];
}

int main() {
    
    init();
    cin >> n;
    cout << c[2 * n][n] / (n + 1) << endl;
    
    return 0;
}

不同的二叉搜索树

题目链接(AcWing)
题目链接(LeetCode)

记忆化搜索

这个题刚做的时候是用dfs做的。但是在AcWing上只能过7个测试点,不过在LeetCode上还是能过的。

AcWing代码
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

typedef long long LL;

const int N = 1010, MOD = 1e9 + 7;

int n;
int f[N][N];

LL dfs(int l ,int r) {
    if (l >= r) return 1;
    if (f[l][r] != -1) return f[l][r];
    f[l][r] = 0;
    for (int i = l; i <= r; i++) {
        f[l][r] = (f[l][r] + dfs(l, i - 1) * dfs(i + 1, r)) % MOD;
    }
    return f[l][r];
}

int main() {
    
    memset(f, -1, sizeof f);
    cin >> n;
    cout << dfs(1, n) << endl;
    
    return 0;
}
LeetCode代码

C++

class Solution {
public:
    int numTrees(int n) {
        vector<vector<int>> a(n + 10, vector<int>(n + 10, -1));
        return dfs(1, n, a);
    }
    int dfs(int l, int r, vector<vector<int>> &a) {
        if (l >= r) return 1;
        if (a[l][r] != -1) return a[l][r];
        a[l][r] = 0;
        for (int i = l; i <= r; i++) {
            a[l][r] += dfs(l, i - 1, a) * dfs(i + 1, r, a);
        }
        return a[l][r];
    }
};

Java

class Solution {
    public int numTrees(int n) {
        int[][] a = new int[n + 10][n + 10];
        for (int i = 0; i <= n; i++)
            for (int j = 0; j <= n; j++)
                a[i][j] = -1;
        return dfs(1, n, a);
    }
    public int dfs(int l, int r, int[][] a) {
        if (l >= r) return 1;
        if (a[l][r] != -1) return a[l][r];
        a[l][r] = 0;
        for (int i = l; i <= r; i++) {
            a[l][r] += dfs(l, i - 1, a) * dfs(i + 1, r, a);
        }
        return a[l][r];
    }
}
正解 动态规划

这个问题与有(n)个相同的节点构成的不同形态的二叉树是等价的。

因为相同的节点构成的不同形态的二叉树的数量是固定的,而又因为本题是(BST)所以对于每一种形态上放整数(1)~(n)是唯一确定的。

闫氏DP分析法

  • 状态表示:(f[n])表示有(n)个节点组成的不同(BTS)的数量
  • 状态计算:根据左儿子节点的数量划分,从(0) ~ (n-1),则右儿子节点的数量为(n-1) ~ (0)。对于每一种情况满足乘法原理,所以(f[n] = sum_{i=0}^{n-1}f[i] cdot f[n-1-i])

到这里就会发现本题答案也是卡特兰数。

C++代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 1010, MOD = 1e9 + 7;

int n;
int f[N];

int main() {
    cin >> n;
    f[0] = 1; // 这个边界不能写成0
    for (int i = 1; i <= n; i++) 
        for (int j = 0; j < i; j++) 
            f[i] = (f[i] + (LL)f[j] * f[i - 1 - j]) % MOD;
        
    cout << f[n] << endl;
    
    return 0;
}

参考

AcWing算法基础
AcWing笔试面试
大话数据结构

以上是关于卡特兰数的主要内容,如果未能解决你的问题,请参考以下文章

卡特兰数-Catalan数

Golang 实现卡特兰数

谁有卡特兰数的证明过程?

卡特兰数总结

卡特兰数

Catalan number (卡特兰数)