组合数计算

华为笔试题里出现了组合数计算的问题,在此记录一下解法:

C_n^k的核心公式是C_n^k=C_{n-1}^{k-1}+C_{n-1}^k

这个公式可以从直观上很好地理解:从n个物品里选k个的方法数,等于 选第n个物品,在前n-1个物品里选k-1个的方法数 + 不选第n个物品,在前n-1个物品里选k个的方法数。也可以从组合数的计算公式出发证明。

那么求组合数的问题就变成了动态规划问题:

\left\{\begin{matrix} cnk[i][0] = 1, i \geq 0 \\ cnk[i][j] = cnk[i-1][j-1] + cnk[i-1][j], i \geq j \geq 1 \end{matrix}\right.

这样计算可以有效避免求组合数时乘法溢出。

如果只需要求固定n的C_n^k,那么可以进一步优化空间到一维: cnk[j]=(cnk[j-1] + cnk[j])\%mod,注意 j 要从右向左更新。

    int cnk[MAXN] = {0};    
    cnk[0] = cnk[1] = 1;
    for(int i = 2; i <= k; i++)
    {
        for(int j = i; j >= 1; j--)
        {
            cnk[j] = (cnk[j-1] + cnk[j]) % MOD;
        }
    }

两道例题:

1. 计算系数

题目大意:给定正整数a, b, k, n, m。求出 (ax+by)^k 中, x^ny^m的系数,结果对10007取模。

 根据二项式定理, (ax+by)^k=\sum _{t=0}^kC_k^ta^{k-t}x^{k-t}b^ty^t,对比一下就知道,x^ny^m的系数是 C_k^{k-n}a^nb^{k-n}

其中,a^nb^{k-n}用快速幂去求,组合数用上面的递推公式去求。

#include <bits/stdc++.h>
using namespace std;

using ll = long long;
int MOD = 10007;
ll cnk[1001] = {0};
int a, b, k, n, m;

ll multi(ll x, ll y)
{
    return (x % MOD) * (y % MOD) % MOD;
}

ll fastpow(ll x, int exp)
{
    if(exp == 0 || x == 1) return 1;
    if(exp & 1) return (x % MOD * (fastpow(x, exp - 1) % MOD)) % MOD;
    else
    {
        ll t = fastpow(x, exp / 2);
        return (t % MOD) * (t % MOD) % MOD;
    }
}

int main()
{
    scanf("%d%d%d%d%d", &a, &b, &k, &n, &m);
    cnk[0] = cnk[1] = 1;
    for(int i = 2; i <= k; i++)
    {
        for(int j = i; j >= 1; j--)
        {
            cnk[j] = (cnk[j-1] + cnk[j]) % MOD;
        }
    }
    printf("%lld", multi(multi(cnk[k-n], fastpow(a, n)), fastpow(b, k-n)));
    return 0;
}

2. 5502. 将子数组重新排序得到同一个二叉查找树的方案数

给你一个数组 nums 表示 1 到 n 的一个排列。我们按照元素在 nums 中的顺序依次插入一个初始为空的二叉查找树(BST)。请你统计将 nums 重新排序后,统计满足如下条件的方案数:重排后得到的二叉查找树与 nums 原本数字顺序得到的二叉查找树相同。

比方说,给你 nums = [2,1,3],我们得到一棵 2 为根,1 为左孩子,3 为右孩子的树。数组 [2,3,1] 也能得到相同的 BST,但 [3,2,1] 会得到一棵不同的 BST 。

请你返回重排 nums 后,与原数组 nums 得到相同二叉查找树的方案数。

由于答案可能会很大,请将结果对 10^9 + 7 取余数。

给定数组[2,1,3],那么根节点是2,不能动,它的左子树节点集合是 l, 右子树节点集合是r,那么可以形成相同的树的排列总数就是 形成与l相同的树的排列总数 * 形成与 r 相同的排列总数 *  C(l的节点数 + r的节点数, l的节点数)。最后一个组合数的意思是,除了根节点的位置不能动之外,剩下的位置中,可以任意选出一些位置来放置l中的节点。

class Solution {
private:
    long long cnk[1001][1001] = {0};
    int MOD = 1e9 + 7;
    int helper(const vector<int>& nums)
    {
        if(nums.size() <= 1) return 1;
        vector<int> l, r;
        for(int i = 1; i < nums.size(); i++)
        {
            if(nums[i] < nums[0]) l.push_back(nums[i]);
            else r.push_back(nums[i]);
        }
        long long ans = cnk[l.size() + r.size()][l.size()];
        ans = (ans * helper(l)) % MOD;
        ans = (ans * helper(r)) % MOD;
        return ans;
    }
public:
    int numOfWays(vector<int>& nums) {
        int n = nums.size();
        cnk[1][0] = cnk[1][1] = 1;
        for(int i = 2; i <= n; i++) cnk[i][0] = 1;
        for(int i = 2; i <= n; i++)
        {
            for(int j = 1; j <= i; j++)
            {
                cnk[i][j] = (cnk[i-1][j-1] + cnk[i-1][j]) % MOD;
            }
        }
        return helper(nums) - 1;
    }
};

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值