组合数计算

组合数计算

计算组合数的题目及其变体还是比较常见的,这里记录一下几种经典解法。
我们以一个典型问题为例进行讨论: LeetCode 62. Unique Paths,问的是 mxn 的矩阵从左上走到右下有几种走法,每次只能向下走或者向右走。

动态规划

第一种解法是DP,采用递推公式 (C(m, n)=C(m-1, n) + C(m-1, n-1))(即,杨辉三角)来计算。
很显然,到达每一个格子的走法,是他的左边和上边邻居的走法之和。这样我们可以使用一个二维数组来对应记录每一个位置的走法数,空间开销 O(m*n)。进一步,由于我们是一行一行进行计算,我们可以把空间压缩到 O(min(m,n))

最终,时间开销 O(m*n),空间开销 O(min(m,n))
参考解法:

/*
 * @lc app=leetcode id=62 lang=cpp
 *
 * [62] Unique Paths
 */

// @lc code=start
class Solution {
public:
    int uniquePaths(int m, int n) {
        if (m < n) swap(m, n);
        vector<int> dp(n+1, 0);
        // dp[1][1] = 1;
        dp[1] = 1;
        for (int i=1; i<=m; i++) {
            for (int j=1; j<=n; j++) {
                // dp[i][j] = dp[i-1][j] + dp[i][j-1];
                dp[j] += dp[j-1];
            }
        }
        return dp[n];
    } //  AC, DP
};
// @lc code=end

组合数公式

我们注意到,由于题目限制,实际上路径总长是 m-1+n-1,需要从中选出 m-1 步向下。
第二种解法是直接使用组合数公式 (C(n, k) = n! / (k! * (n-k)!)) 进行计算,但是这里有一个陷阱是整数溢出问题。
一种解决溢出的办法是使用浮点数,甚至有人提出了使用对数降低计算复杂度的方案;但是这是一个整数问题,我们想使用纯整数来解决。
注意到计算公式的分子分母各自都是 n 个数的乘积,所需乘法次数相同。我们把两遍的计算分为两段,分别长为 kn-k,其中一段是完全相同的 1*2*3*... 可以直接消除,我们选择消较长的一段。不妨设剩下的较短一段是长度为k的那一段,则 (C(n, k) = (n*(n-1)*(n-2)*...*(n-k+1)) / (1*2*3*...*k))。这里的问题仍然是溢出如何解决,但是进展是分子分母都已经是单个自然数序列了。
我们可以在一轮循环中解决这个计算,即变形为 (C(n, k) = n/1 * (n-1)/2 * (n-2)/3 * ... * (n-k+1)/k)。也就是乘法和除法交叉计算,保证不会出现小数。
这篇博客 所言,有数论中的定理可以保证这一点,但是博客本身并没有提供这个定理。
我们这里简单说明一下为什么每次都能整除,不会出现小数。考虑数学归纳,对于第一次操作 n/1 肯定是能整除的;然后我们假设前面 k-1 次都整除了,考察第 k 次能不能整除。当然能整除,因为分母是连续的 k 个自然数,必然有一个恰好是 k 的倍数,所以其乘积也能被 k 整除。(这里我们的说明其实不够充分,因为我们没有证明,k的时候分母乘积中用到的因子没有被前面k-1个数用掉。)

这种解法,时间复杂度 O(min(m,n)),空间复杂度 O(1)
参考代码:

/*
 * @lc app=leetcode id=62 lang=cpp
 *
 * [62] Unique Paths
 */

// @lc code=start
class Solution {
public:
    int64_t C(int n, int k) {
        int64_t res = 1;
        if (k > n - k) k = n - k;
        for (int i = n, j = 1; j <= k; i--, j++) {
            res *= i;
            res /= j;
        }
        return res;
    }
    int uniquePaths(int m, int n) {
        return (int) C(m-1 + n-1, m-1);
    } // AC, 组合数C(m+n, m)
};
// @lc code=end

其他解法

其他方案包括 质因数分解
以及进一步降低了复杂度的 Lucas定理
还有人提出了 二进制辅助法 的解决思路。

拓展阅读

OI Wiki | 排列组合
OI Wiki | 卡特兰数
OI Wiki | 卢卡斯定理
Morning-Glory | 卡特兰(Catalan)数入门详解

原文地址:https://www.cnblogs.com/zhcpku/p/14347792.html