zoukankan      html  css  js  c++  java
  • 组合数计算

    组合数计算

    计算组合数的题目及其变体还是比较常见的,这里记录一下几种经典解法。
    我们以一个典型问题为例进行讨论: 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)数入门详解

  • 相关阅读:
    算法常识——二叉堆
    关于c++ 感想
    算法常识——树的遍历
    算法常识——非线性结构
    算法常识——基础的数据结构
    算法常识——结构与复杂度
    重温网络编程——常识(三)
    重温网络编程——协议(二)
    重温网络编程(一)
    RemoteViews 整理
  • 原文地址:https://www.cnblogs.com/zhcpku/p/14347792.html
Copyright © 2011-2022 走看看