当前位置: 首页 > news >正文

老题新解|组合数问题

《信息学奥赛一本通》第163题:组合数问题

题目描述
给定两个正整数 nnnmmm,请你计算从 nnn 个不同的元素中选择 mmm 个元素的方案数(选择的顺序不重要)。
由于方案数可能很大,请输出方案数对 109+710^9+7109+7 取模的结果,也就是输出方案数除以 109+710^9+7109+7 的余数。
输入格式
一行,包含两个整数 nnnmmm
输出格式
一个整数,表示组合数 CnmC_n^mCnm109+710^9+7109+7 取模的结果。
输入输出样例 #1
输入 #1
5 3
输出 #1
10
说明/提示
样例解释 #1:
555 个元素中选择 333 个,总共有 101010 种不同的方案:

  • (1,2,3)(1,2,3)(1,2,3)
  • (1,2,4)(1,2,4)(1,2,4)
  • (1,2,5)(1,2,5)(1,2,5)
  • (1,3,4)(1,3,4)(1,3,4)
  • (1,3,5)(1,3,5)(1,3,5)
  • (1,4,5)(1,4,5)(1,4,5)
  • (2,3,4)(2,3,4)(2,3,4)
  • (2,3,5)(2,3,5)(2,3,5)
  • (2,4,5)(2,4,5)(2,4,5)
  • (3,4,5)(3,4,5)(3,4,5)
    注意:选择 (1,2,3) 和选择 (2,1,3) 被视为同一种方案。
    数据范围:
    对于 20% 的数据,满足 1≤m≤n≤101\le m\le n\le 101mn10
    对于 100% 的数据,满足 1≤m≤n≤50001\le m\le n\le 50001mn5000

大家好,我是莫小特。
这篇文章给大家带来《信息学奥赛一本通》中的第 163 题:组合数问题。

image.png

一、题目描述

洛谷的题号是:B2164 组合数问题

image.png

二、题意分析

这道题是信息学奥赛一本通练习题的第 163 题。

根据输入格式,输入一行两个整数 m 和 n,数据范围:1≤m≤n≤50001\le m\le n\le 50001mn5000,使用 int 类型即可,注意输入顺序哦,n 在前,m 在后。

int m,n;
cin>>n>>m;

输入完成后,我们来分析题目,题目要求我们求从 n 个不同的元素中选择 m 个元素的方案数,注意,这里只需要求方案数量,具体的方案不需要我们输出,所以我们可以直接利用公式:

Cnm​=n!m!(n−m)!​C_n^m​=\frac {n!}{m!(n−m)!}​ Cnm=m!(nm)!n!

根据题目计算出结果之后,需要计算对 109+710^9+7109+7 求模的结果。

先定义这个数值出来作为常量。

const long long MOD = 1000000007;

此时可以使用阶乘+逆阶乘的方法来计算。

模 p(这里 p=109+7p=10^9+7p=109+7,是素数)下,除法用乘以模逆元替代:

a−1≡ap−2(modp)a^{-1} \equiv a^{p-2} \pmod p a1ap2(modp)

所以得出:

Cnm​≡n!×(m!)−1×((n−m)!)−1(modp))C_n^m​≡n!×(m!)_{−1}×((n−m)!)^{−1}\pmod p) Cnmn!×(m!)1×((nm)!)1(modp))

所以需要定义一个数组,元素个数为 5005。

const int MAXN=5005;

定义一个 long long 类型数组。

long long fac[MAXN],ifac[MAXN];

计算快速幂,二分幂,将指数 b 按二进制分解,遇到 1 就把当前基 a 乘到结果上。

注意:在这段代码中没有 a %= MOD 的初始语句。这里没有问题,因为我们只把 fac[n] 传给 modpow(它已经被 % MOD),但一般化写法建议先 a %= MOD

long long modpow(long long a, long long b) {long long res = 1;while (b) {if (b & 1) res = res * a % MOD;  // 当 b 的当前最低位为 1 时,累乘一个 aa = a * a % MOD;                 // a 翻倍(平方)b >>= 1;                         // b 右移一位(相当于除以 2)}return res;
}

从 1 到 n 逐步累乘并取模,得到 0!n! 的模值。
这样做避免了重复计算阶乘,若题目中需要多次查询组合数非常高效。

fac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;

第一步:用费马小定理计算 ifac[n] = (n!)^{-1}(只需一次快速幂)。

第二步:利用关系 (i-1)!^{-1} = i!^{-1} * i 推出 ifac[i-1] = ifac[i] * i % MOD,从 n 反推到 0,不需要对每个 i 单独做幂运算,效率高且代码简洁。

  • 思考:这样我们得到完整的 (i!)^{-1} 表,方便直接索引 ifac[m]ifac[n-m]
ifac[n] = modpow(fac[n], MOD - 2);     // (n!)^{-1}
for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;

最后输出,直接套公式 n! * (m!)^{-1} * ((n-m)!)^{-1},每步取模以避免溢出。

long long ans = fac[n] * ifac[m] % MOD * ifac[n - m] % MOD;
cout << ans << '\n';

按照样例输入对数据进行验证。

image.png

符合样例输出,到网站提交测评。

image.png

测试通过!

三、完整代码

该题的完整代码如下:

#include <bits/stdc++.h>
using namespace std;const long long MOD = 1000000007;
const int MAXN = 5005;// 快速幂:求 a^b % MOD
long long modpow(long long a, long long b) {long long res = 1;while (b) {if (b & 1) res = res * a % MOD;a = a * a % MOD;b >>= 1;}return res;
}int main() {int n, m;cin >> n >> m;   // 输入顺序:n 在前,m 在后static long long fac[MAXN], ifac[MAXN];fac[0] = 1;for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;ifac[n] = modpow(fac[n], MOD - 2);     // 费马小定理求逆元for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;long long ans = fac[n] * ifac[m] % MOD * ifac[n - m] % MOD;cout << ans << '\n';return 0;
}

四、总结

常见易错点:
1、忘记处理 m=0m=n 的情况

虽然通用公式也能处理,但在实现中容易越界(例如索引 ifac[n-m]),确认 0! = 1 已包含在 fac/ifac 数组里即可。结果应为 1

2、模数性质误用(非素数模)

本题模 MOD = 10^9+7 是素数,可以用费马小定理计算逆元 a^{MOD-2}。如果换成非素数模,不能用费马小定理,需要用扩展欧几里得或其他方法。

3、快速幂初始值或取模位置错误导致溢出

modpow 内部应先对 a %= MOD(若上层可能传入未取模的值),循环中每次乘法都要 % MOD。否则中间结果可能溢出 long long(尽管 C++ 64 位通常够用,但保持规范更安全)。

4、数组大小不足或越界

n 最大 5000,数组定义要至少 MAXN = 5005(或 n+1)。fac[0..n]ifac[0..n] 都会被访问。不要定义成太小。

5、把除法当作普通除法而非模逆

直接写 fac[n] / (fac[m]*fac[n-m]) % MOD 是错误的。除法在模意义下需要乘以逆元。

6、类型不够 / 溢出

中间乘法 fac[i-1]*i 应在 long long 下进行并取模。使用 int 会溢出。

7、重复计算逆元导致超时(低效做法)

不要对每个 i 都调用二分幂求逆元(O(n log MOD) 次方),可以先算 ifac[n],再递推 ifac[i-1] = ifac[i]*i%MOD(仅一次快速幂,整体 O(n)),这是常用优化。

---end---

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注我哦!
如果有更好的方法也可以在评论区评论哦,我都会看哒~

我们下集见~

http://www.dtcms.com/a/461583.html

相关文章:

  • Java 工具类详解:Arrays、Collections、Objects 一篇通关
  • Cucumber自学导航
  • docker案例
  • 网站如何做提现功能上海市城乡和住房建设厅网站
  • 南宁 网站建设 公司老吕爱分享 wordpress
  • python 矩阵中寻找就接近的目标值 (矩阵-中等)含源码(八)
  • 嵌入式Linux:线程中信号处理
  • docker启动容器慢,很慢,特别慢的坑
  • C#基础14-非泛型集合
  • 【22.1-决策树的构建1】
  • asp制作网站wordpress使用端口
  • 【机器学习】(一)实用入门指南——如何快速搭建自己的模型
  • 【数值分析】插值法实验
  • 地方门户网站的前途搜索引擎大全全搜网
  • 如何给oracle新建架构(schema)
  • 天地数码携手一半科技PLM 赋能应对全球市场,升级热转印色带研发能力
  • 构筑智能防线:大视码垛机如何重新定义工业安全新标准
  • iPhone17实体卡槽消失?eSIM 普及下的安全挑战与应对
  • 什么RPA可以生成EXE
  • 网站开发设计jw100交换链接的作用
  • 企业推广网站建设报价吉林网站建站系统平台
  • 热壁MOCVD有助于GaN-on-AlN HEMT
  • 网站app微信三合一怎么看网站后台什么语言做的
  • 【深度学习新浪潮】大模型推理实战:模型切分核心技术(上)—— 张量并行原理+国内开源案例+踩坑点
  • 高效SQLite操作:基于C++模板元编程的自动化封装
  • uniApp App内嵌H5打开内部链接,返回手势(左滑右滑页面)会直接关闭H5项目
  • 文字排版网站网站建设的宣传词
  • K8s学习笔记(十七) pod优雅终止流程
  • Redis-基础介绍
  • Redis常用数据库及单线程模式