当前位置:   article > 正文

卡特兰数_卡特兰数c++最省时间的算法

卡特兰数c++最省时间的算法

欢迎来访

首先看一下卡特兰数。若一个数列 h n h_n hn满足:

h n = ∑ i = 0 n − 1 h i ⋅ h n − 1 − i h_n = \sum_{i=0}^{n-1}h_i \cdot h_{n-1-i} hn=i=0n1hihn1i

则称 h n h_n hn为卡特兰数列。

还有一种形式若:

h n = C 2 n n n + 1 h_n = \frac{C_{2n}^n}{n+1} hn=n+1C2nn

也称 h n h_n hn为卡特兰数列

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

满足条件的01序列

题目链接

分析

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

000111
001011
001101
010101
010011

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

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


图中橙色紫色满足条件。

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

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

所以在一般问题中即为:

C 2 n n − C 2 n n − 1 C_{2n}^n-C_{2n}^{n-1} C2nnC2nn1

在对其进行转化:

C 2 n n − C 2 n n − 1 = ( 2 n ) ! n ! n ! − ( 2 n ) ! ( n − 1 ) ! ( n + 1 ) ! = ( 2 n ) ! ( n + 1 ) − ( 2 n ) ! n n ! ( n + 1 ) ! = ( 2 n ) ! n ! ( n + 1 ) ! = ( 2 n ) ! n ! n ! ( n + 1 ) = C 2 n n 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} C2nnC2nn1=n!n!(2n)!(n1)!(n+1)!(2n)!=n!(n+1)!(2n)!(n+1)(2n)!n=n!(n+1)!(2n)!=n!n!(n+1)(2n)!=n+1C2nn

转化后即为:

C 2 n n n + 1 \frac{C_{2n}^n}{n+1} n+1C2nn

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

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

题目链接

分析

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

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 312 312这种序列的,因为 3 3 3先出栈,就意味着, 3 3 3曾经进栈,既然 3 3 3都进栈了,那就意味着, 1 1 1 2 2 2已经进栈了,此时 2 2 2一定在 1 1 1上面,也就是更接近栈顶,所以 2 2 2一定会先比 1 1 1出栈,也就没有 312 312 312这种序列。

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

C++代码

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

#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

不同的二叉搜索树

题目链接(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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
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];
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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];
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
正解 动态规划

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

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

闫氏DP分析法

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

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

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

参考

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/76163
推荐阅读
  • 相关标签
      

    闽ICP备14008679号