C++求解组合数的具体实现

2023-05-16

文章目录

  • 前言
  • 问题起因
  • 组合公式
  • 公式变形
  • 递推公式
    • 递归实现
    • 备忘递归
    • 动态规划
    • 压缩DP
    • 其他优化
  • 总结
  • 补充
    • 反向递归
    • 正向递推

前言

很少写关于具体算法的总结笔记,因为很难把一个算法从头到尾的叙述清晰并且完整,容易造成误解。这次想总结一下组合数的具体实现,原因是最近总是碰见组合数,所以决定来写写,免得每次从头推导公式耽误时间。排列组合经常会作为一个问题解决方案中一部分,通常是求某个问题有多少个解,达到某种状态有多少种操作方式等等。

问题起因

今天下午解一道简单题,难度简直刷新了我的认知,其中需要用到组合数,但这仅仅是解题的一小部分,没办法,从头推导的,简单优化下,写出了如下代码:

int C(int a, int b)
{
    int ans = 1;
    for (int i = a; i > a - b; i--) ans *= i;
    for (int i = b; i > 1; i--) ans /= i;
    return ans;
}

因为时间紧迫,范围也比较小,同时可以控制 ab 的大小,所以临时写下的这段代码可以运行,不然这段代码会出现各种错误的。

组合公式

既然是想做总结,还是从头来看看组合公式,根据原始公式实现算法,并尝试优化它,当熟悉这个套路之后,就可以直接拿来用了,可以节省不少时间,组合公式的常见表示方式如下:

C n m = n ! m ! ( n − m ) ! = C n n − m , ( n ≥ m ≥ 0 ) C^m_n = \frac{n!}{m!(n-m)!} = C^{n-m}_n,(n \geq m \geq 0) Cnm=m!(nm)!n!=Cnnm,(nm0)

这个公式写出来清晰多了,n!表示n的阶乘,计算方式为 n*(n-1)*(n-2)*(n-3)*…*3*2*1, 相信很多人都清楚,我们只要把这个数据公式翻译成代码就可以了:

int C2(int n, int m)
{
    int a = 1, b = 1, c = 1;
    for (int i = n; i >= 1; --i) a *= i;
    for (int i = m; i >= 1; --i) b *= i;
    for (int i = n-m; i >= 1; --i) c *= i;
    return a/(b*c);
}

代码比较简单,依次计算公式中三个数的阶乘,然后再做乘除法就可以了,但是你有没有思考过一个问题,int 类型的整数最大能表示的阶乘是多少?是12!,它的值是 479,001,600,它是 int 表示范围内最大的阶乘数,看来这种实现方式局限性很大,如果 n 大于12就没有办法计算了。

公式变形

实际上根据阶乘的定义,n! 和 (n-m)! 是可以约分的,将这两个式子约分后,公式可以化简为:

C n m = n ! m ! ( n − m ) ! = n ( n − 1 ) ( n − 2 ) . . . ( n − m + 1 ) ) m ! , ( n ≥ m ≥ 0 ) C^m_n = \frac{n!}{m!(n-m)!} = \frac{n(n-1)(n-2)...(n-m+1))}{m!},(n \geq m \geq 0) Cnm=m!(nm)!n!=m!n(n1)(n2)...(nm+1)),(nm0)

公式写成这样之后可以少计算一个阶乘,并且计算的范围也会缩小,代码实现和一开始展示的代码思想是一样的:

int C3(int n, int m)
{
    int a = 1, b = 1;
    for (int i = n; i > n - m; --i) a *= i;
    for (int i = m; i >= 1; i--) b *= i;
    return a/b;
}

这段代码虽然经过了化简,但是当 n 和 m 非常接近的时候,分子还是接近于 n!,所以表示的范围还是比较小。

递推公式

直接给出的公式经过化简后还是受制于计算阶乘的范围,得想个办法看看能不能绕过阶乘计算,方法总是有的,并且前辈们已经给我们整理好了,我们总是站在巨人的肩膀上,下面就是递推公式:

{ C n m = 1 , ( m = 0 或 m = n ) C n m = C n − 1 m + C n − 1 m − 1 , ( n > m > 0 ) \begin{cases} {C^m_n} = 1,\qquad\qquad\qquad (m=0 或 m=n) \\ {C^m_n} = {C^m_{n-1}} + {C^{m-1}_{n-1}},\qquad(n > m > 0) \end{cases} {Cnm=1,(m=0m=n)Cnm=Cn1m+Cn1m1,(n>m>0)

递归实现

有了上面的分段函数表示,就满足了递归的条件,既有递归调用缩小规模,也有递归出口,这样实现起来很简单,代码如下:

int C4(int n, int m)
{
    if (n == m || m == 0) return 1;
    return C4(n-1, m) + C4(n-1, m-1);
}

这两行代码是不是很秀?不过使用递归常常会出现一问题,那就是相同子问题多次计算,导致效率低下,这个计算组合数的方式同样存在重复计算子问题的缺点,我们以调用C4(5, 3)为例,看看下面的调用关系图:

5,3
4,3
3,3
3,2
2,2
2,1
1,1
1,0
4,2
3,2
3,1
2,2
2,1
2,1
2,0
1,1
1,0
1,1
1,0

从这个图可以清晰看出C4(3, 2)C4(2, 1) 都被计算了多次,当 m 和 n 的数字比较大的时候,会进行更多次的重复计算,严重影响计算的效率,有没有什么办法解决重复计算的问题呢?

备忘递归

解决重复计算的常用方法是利用一个备忘录,将已经计算式子结果存储起来,下次再遇到重复的计算时直接取上次的结果就可以了,我们可以将中间结果简单存储到map中。

假设 n 不超过10000,这比12已经大太多了,我们可以使用 n * 10000 + m 作为map的键,然后将结果存储到map中,每次计算一个式子前先看查询备忘录,看之前有没有计算过,如果计算过直接取结果就可以了,代码简单实现如下:

int C5(int n, int m, map<int, int>& memo)
{
    if (n == m || m == 0) return 1;

    auto itora = memo.find((n-1)*10000+m);
    int a = itora != memo.end() ? itora->second : C4(n-1, m);
    if (itora == memo.end()) memo[(n-1)*10000+m] = a;

    auto itorb = memo.find((n-1)*10000+m-1);
    int b = itorb != memo.end() ? itorb->second : C4(n-1, m-1);
    if (itorb == memo.end()) memo[(n-1)*10000+m-1] = b;

    return a + b;
}

使用 map 作为备忘录可以避免重复计算,这是解决递归效率低下的常用方法,那么有了递推公式不使用递归实现可不可以呢?当然可以了,针对于这个问题,有了递推公式我们还可以使用动态规划(dp)的方式来实现。

动态规划

动态规划常常适用于有重叠子问题和最优子结构性质的问题,试图只解决每个子问题一次,具有天然剪枝的功能。基本思想非常简单,若要解一个给定问题,我们需要解其不同子问题,再根据子问题的解以得出原问题的解。

再回顾一下递推公式:

{ C n m = 1 , ( m = 0 或 m = n ) C n m = C n − 1 m + C n − 1 m − 1 , ( n > m > 0 ) \begin{cases} {C^m_n} = 1,\qquad\qquad\qquad (m=0 或 m=n) \\ {C^m_n} = {C^m_{n-1}} + {C^{m-1}_{n-1}},\qquad(n > m > 0) \end{cases} {Cnm=1,(m=0m=n)Cnm=Cn1m+Cn1m1,(n>m>0)

翻译成人话就是,当m等于0或者等于n的时候,组合数结果为1,否则组合数结果等于另外两个组合数的和,我们可以采用正向推导的方式,将 n 和 m 逐步扩大,最终得到我们想要的结果,定义dp表格如下:

n\m(0)(1)(2)(3)(4)(5)
(0)1
(1)11
(2)121
(3)1331
(4)1464<1>
(5)1510==>10<5><1>

从表格可以清晰的看出求解 C(5,3) 只需要计算5行3列(从0开始)的数据,其余的值可以不用计算,这样我们就可以对照着表格写代码啦,定义一个dp数组,然后双重for循环就搞定了:

int C6(int n, int m)
{
    if (n == m || m == 0) return 1;

    vector<vector<int>> dp(n+1, vector<int>(m+1));
    for (int i = 0; i <= n; i++)
        for (int j = 0; j <= i && j <= m; j++)
            if (i == j || j == 0) dp[i][j] = 1;
            else dp[i][j] = dp[i-1][j] + dp[i-1][j-1];

    return dp[n][m];
}

至此,我们就采用了非递归的方式求解出了组合数的结果,但是这里的空间有点浪费,每次都要花费O(mn)的空间复杂度,有没有办法降低一点呢?我们可以找找规律进行压缩。

压缩DP

观察之前的动态规划实现的代码,我们发现求解第 i行的数据时只与第 i-1 行有关,所以我们可以考虑将二维数据压缩成一维,还是逐行求解,只不过可以用一维数组来记录求解的结果,优化代码如下:

int C7(int n, int m)
{
    if (n == m || m == 0) return 1;

    vector<int> dp(m+1);
    for (int i = 0; i <= n; i++)
        for (int j = min(i, m); j >= 0; j--)
            if (i == j || j == 0) dp[j] = 1;
            else dp[j] = dp[j] + dp[j-1];

    return dp[m];
}

这样我们就将空间复杂度降低到了O(m),需要注意的是在计算dp时,因为采用了压缩结构,为防止前面的修改影响后续结果,所以采用里倒序遍历,这是一个易错的点。

其他优化

代码实现到这里,我们的时间复杂度是O(nm),空间复杂是O(m),其实还有进一步的优化空间:

  • 减小m: 因为题目是求解C(n, m),但是我们知道组合公式中,C(n, m) 和 C(n, n-m) 相等,所以当 n-m 小于 m 的时候求解C(n, n-m)可以降低时间复杂度和空间复杂度。

  • 部分剪枝: 观察函数int C7(int n, int m),实际上当i为n时,j没必要遍历到0,只需要计算j等于m的情况就可以了,可以提前计算出结果。

  • 缩小计算范围: 从上面的剪枝操作得到启示,其实每一行没必要全部计算出来,以 C(5,3) 为例,我们只需要计算出表格中有数字的位置的结果就可以了:

n\m(0)(1)(2)(3)(4)(5)
(0)1
(1)11
(2)121
(3)331
(4)64
(5)==>10

这样来看每行最多需要计算3个值,那么时间复杂度可以降低到 O(3n),去掉常数,时间复杂度降为 O(n)

总结

  • 计算组合数可以采用逆向递归和正向递推两种方式,递归时注意写好递归出口
  • 采用正向递推方法时利用动态规划思想,使用子问题的解拼凑出最终问题的解
  • 计算组合数若使用了计算阶乘应注意范围,避免在计算时产生溢出,int最多能表示 12!
  • 使用动态规划方法时可以逐步优化空间和时间,这其实就是优化算法的过程,也是提升的过程
  • 关于组合数的求解方式,我们可以找到时间复杂度O(n)、空间复杂度O(m)的非递归解法

补充

感谢 @小胡同的诗 同学的补充和提醒,让我再次感受到数学力量的深不可测,原来求解组合数还有这样一个递推公式:

{ C n m = 1 , ( m = 0 或 m = n ) C n m = n − m + 1 m C n m − 1 , ( n > m > 0 ) \begin{cases} {C^m_n} = 1,\qquad\qquad\qquad (m=0 或 m=n) \\ C_n^m=\frac{n-m+1}{m}C_n^{m-1},\qquad(n > m > 0) \end{cases} {Cnm=1,(m=0m=n)Cnm=mnm+1Cnm1,(n>m>0)

这个公式厉害就厉害在它是一个线性的,不存在分叉的情况,也就是说即使递归也不会出现重复的计算,我们简单实现一下。

反向递归

int C8(int n, int m)
{
    if (n == m || m == 0) return 1;
    return C8(n, m-1) * (n-m+1) / m;
}

代码非常紧凑,也不存在重复计算的情况,当然我们也可以使用正向计算的方式来实现。

正向递推

int C9(int n, int m)
{
    if (n == m || m == 0) return 1;

    int ans = 1;
    m = min(m, n-m);

    for (int i = 1; i <= m; i++) ans = ans * (n-i+1) / i;
    return ans;
}

这段代码将时间复杂度降到了O(m),空间复杂度降到了O(1),不过特定的场景还是要选择特定的实现,虽然C9函数在时间复杂度和空间复杂度上都优于 C5 函数,但是如果一个实际问题中需要用到多个组合数的时候,C5 这种采用缓存的方式可能会是更好的选择。


==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

想讲故事?没人倾听?那是因为你还未到达一个指定的高度,当你在某个领域站稳了脚跟,做出了成绩,自然有的是时间去讲故事或者“编”故事,到时候随便一句话都会被很多人奉为圭臬,甚至会出现一些鸡汤莫名其妙的从你嘴里“说”出来。在你拥有了讲故事权利的同时,批判的声音也将随之而来~

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++求解组合数的具体实现 的相关文章

  • Android-推荐一个视频压缩库RxFFmpeg

    最近项目当中有遇到上传视频的时候 xff0c 需要做合理压缩的需求 分享一下我使用的视频压缩库 xff0c 希望能帮助遇到同样有视频处理的需求的小伙伴 RxFFmpeg依赖 这个开源库一共有三个版本 xff0c 这里贴两个版本的依赖 xff
  • C# winForm 软件自动升级实现方式

    对于C winform开发者来说 xff0c 软件自动升级功能是一个很重要的功能 作者根据自身经验 xff0c 和大家分享一下软件升级的实现方式 注意 xff1a 本文主要介绍通过WebService升级软件 作者的另一篇通过FTP方式升级
  • 考勤查询统计SQL脚本。

    本文主要记录下平时工作中考勤统计中的SQL脚本 以便于后续翻阅 同时和大家分享一下 不足的地方还请大牛多多给与点评 nbsp 1 首先是查询某员工的考勤记录 可以根据年份 月份 或者时间段查询结果 同时也可以去掉人员筛选条件 查询多个人的考
  • SQL语句删除具有外键约束(foreign key)的表。因为该对象正由一个 FOREIGN KEY 约束引用。

    关于包含外键的表 xff0c 清理数据的时候 xff0c 如truncateTable xff0c 网上大部分的解决办法是 xff0c 删除外键 删除数据 再新建表 这里介绍一种不需要删除外键 xff0c 只需要修改外键属性就可以删除数据的
  • Unicode&ASCII中双向控制字符 U+202D和U+202C

    ASCII编码对照表 911查询 ASCII编码转换 xff0c ASCII码在线查询工具 ASCII 在线转换器 xff0c ASCII码 xff0c ASCII 转码 在线工具 Unicode 中的 BIDI 双向性算法 掘金 1 场景
  • C++魔方阵

    问题描述 输入一个自然数 xff2e xff08 1 amp le N amp le 9 xff09 xff0c 要求输出如下的魔方阵 xff0c 即边长为2 N 1 xff0c xff2e 在中心出现一次 xff0c 其余位置上的数字从外
  • PCM和WAV音频格式的区别,以及python自动转换

    目录 WAV和PCM的简单介绍PCMWAV 关于音频的基础知识声道数channels采样位数bits采样频率sample rate 进阶内容互相转换代码 WAV和PCM的简单介绍 PCM pcm xff1a pulse code modul
  • 解决:“操作无法完成因为其中的文件夹或文件已在另一程序中打开”无法删除文件的问题

    1 xff0c 利用快捷键 xff08 Ctrl 43 Alt 43 delete xff09 打开任务管理器 xff0c 选择其中的性能 xff0c 打开 资源管理器 2 xff0c 搜索下面关联的句柄 xff08 可搜索文件储存的路径
  • windows下使用C++操作MySQL数据库

    系统环境 操作系统 xff1a windows 7 64位 编译环境 xff1a visual studio 2015 MySQL版本 xff1a 5 6 31 log MySQL Community Server GPL 环境设置 1 将
  • kali重启网络服务

    kali的命令和一般的重启网络命令稍有不同 普通系统 xff1a systemctl restart network xff08 有补全 xff09 kali xff1a systemctl restart networking xff08
  • 安全设备默认地址账密总结

    防火墙 厂商默认地址用户名密码天融信192 168 1 254supermantalent talent 64 123华为192 168 0 1 8443adminHuawei 64 123 Admin 64 123网御星云一代防火墙htt
  • Android开发——实现背景颜色渐变效果

    前言 在Android开发当中 xff0c 我们肯定会接到有业务需求是 xff1a 让APP的某一些背景颜色产生渐变效果 那我们应该怎么去实现呢 xff1f 接下就是我要为大家介绍的了 效果图 这是需求要达到的效果 接下来说一下是怎么实现的
  • Java常见设计模式总结

    一 设计模式总述 xff1a 1 什么是设计模式 xff1a 设计模式是一套经过反复使用的代码设计经验 xff0c 目的是为了重用代码 让代码更容易被他人理解 保证代码可靠性 设计模式于己于人于系统都是多赢的 xff0c 它使得代码编写真正
  • ensp解决virtualbox不兼容问题

    virtualbox版本5 2 44 这个版本很讲究 xff0c 太高太低都不行 windows版本为20H2亲测有效 防火墙默认全关 另一台windows配置 系统型号virtualbox型号windows20h25 2 44window
  • 关于元宇宙

    元宇宙融合了信息技术 xff08 5G 6G xff09 互联网时代 xff08 web3 0 xff09 人工智能 云算力 大数据 区块链以及 VR AR MR xff0c 游戏引擎在内的虚拟现实技术的成果 它将引发基础数学 xff08
  • 关于oracle预言机

    oracle预言机和oracle数据库没有任何原因 在其他语种中oracle有预言的意思 区块链预言机 xff08 Oracle xff09 是区块链与外部世界交互的一种实现机制 xff0c 它在区块链与外部世界间建立一种可信任的桥接机制
  • Windows10 系统安装微软商店(ms-windows-store)

    在下载日历是显示没有应用 xff0c 应从ms windows store下载 在此记录windows10安装ms windows store步骤 步骤如下 xff1a 1 使用win 43 x打开菜单后 xff0c 选择powershel
  • 谷歌浏览器打开本地堡垒机应用发布服务器cmd的方法

    齐治堡垒机是业界中较为出名的堡垒机 xff0c 但是依旧存在一些bug 堡垒机是通过应用发布服务器访问web的 xff0c 如果托管了web且堡垒机管理员没有加固应用发布服务器本地策略 xff0c 我们可以通过浏览器调用本地的cmd进行一系
  • 2022复盘&2023计划

    个人成长计划 2022复盘 自媒体 B站 4月10日成为UP主 发布了35个视频 播放量13 6w 累计直播431h 粉丝量1160 获赞量2058 公众号 1053关注 36篇内容 小红书 136粉丝 1167赞 知乎 85关注 48赞

随机推荐

  • 使用集简云将UpTimer同步到Notion

    使用UpTimer同步到Notion 对于集简云我们应当非常熟悉了 xff0c 之前讲过很多流程啦 利用集简云将Notion数据库更新订阅到Outlook和微信 1 干货分享 集简云 2步轻松定制个人RSS阅读器 高效获取信息 2 释放双手
  • MySQL分组查询语句

    文章目录 1 需求2 表结构与部分数据3 查询语句4 结果5 前端显示 1 需求 根据账单表 tb bookkeeping 中的用户ID user id xff0c 按时间倒序查询该用户所在房间所有支出 xff08 bk type 61 0
  • 使用pyinstaller将具有多个python文件的项目打包为exe(含依赖库)

    1 将需要打包有python文件放到一个文件夹 xff0c 例如下图所示的Demo文件夹 xff0c 其中ClickEveryDay py为主文件 xff0c telegram ico为图标文件 2 生成主函数对应的spec文件 命令 xf
  • 在win10下使用PowerShell批量替换文件名

    步骤 通过PowerShell ISE来创建扩展名为 ps1的脚本文件 具体操作过程参考 xff1a https www ithome com html win10 250196 htm编辑新建的 ps1文件 xff0c 举个栗子进行简单说
  • Kotlin笔记15——字符串转数字类型

    前言 在使用Java编程语言开发的 xff0c 我们会经常遇到字符串转数字的需求 那么在Kotlin中是怎么实现的呢 xff1f 接下来跟大家分享一下 字符串转数字 首先我们必须保证字符串是数字类型 xff0c 不能出现a3这种数字与字符混
  • 使用gitLab过程中遇到的一些问题

    之前由于疫情 xff0c 电脑放在公司 xff0c 有一些数据需要其他同事帮忙提交 xff0c 怎知居然连了他的git账号 xff0c 搞得我自己代码提交拉取老有问题 xff0c 一开始没有意识到是这个原因 xff0c 知道打开了自己git
  • 【Micropython】肝货~使用USB_VCP通过USB串口与树莓派或PC端通信

    为什么要使用USB VCP xff1f Micropython有很多串口 xff0c 例如PYboard xff0c 有5个串口可以使用 xff0c 但是 xff0c 平时我们在做一些项目的时候 xff0c 需要使用的引脚较多 xff0c
  • ROS中安装配置并使用VScode(持续更新)

    1 为什么使用VScode VSCode 全称 Visual Studio Code xff0c 是微软出的一款轻量级代码编辑器 xff0c 免费 开源而且功能强大 它支持几乎所有主流的程序语言的语法高亮 智能代码补全 自定义热键 括号匹配
  • pip install airsim问题

    直接使用pip install airsim安装airsim包会失败 airsim C Users DELL gt pip install airsim Collecting airsim Using cached airsim 1 8 1
  • vm虚拟机无法拖拽文件和复制粘贴解决办法

    sudo apt install open vm tools sudo apt install open vm tools desktop
  • PX4和Airsim通信操作流程

    坑真几把多 先在Windows上安装UE4和Airsim不再赘述 xff0c 官网都有 虚拟机或其他计算机安装好ubuntu并安装PX4 1 安装PX4的ROS相关包 xff08 mavros xff09 1 第一种 xff1a 进入官网安
  • mavros安装流程(超简单)

    只适用于Ubuntu18 04 在Ubuntu中新建一个空白文本 xff0c 命名为123 sh bin bash Bash script for setting up ROS Melodic with Gazebo 9 developme
  • 安装WSL2+Ubuntu18.04(慢慢更新记录)

    1 安装WSL和Ubuntu WSL官网在此 安装 WSL Microsoft Learn Windows下CMD xff0c 先安装WSL2 wsl install 然后进入Microsoft Store xff0c 搜索Ubuntu然后
  • -bash: ./Setup.sh: Permission denied

    sudo chmod 777 xxx
  • Linux 给文件夹或者文件添加权限

    chmod R 777 文件夹 参数 R是递归的意思 777表示开放所有权限 chmod 777 test sh chmod 43 x 某文件 如果给所有人添加可执行权限 xff1a chmod a 43 x 文件名 xff1b 如果给文件
  • Postman使用笔记——Postman发送get请求

    前言 在实际的开发当中 xff0c 我们经常用到get或者post请求 在这篇博客里面分享一下 xff0c 如何在Postman中发送get请求 发送get请求 1 在Postman工作空间选定get请求 图中我们可以看到很多请求方式 xf
  • jdbc连接mysql数据库的详细步骤

    标题 jdbc连接mysql数据库 1 首先在项目根目录创lLib文件夹 xff0c 放入jdbc驱动程序 xff0c 然后Add As Library 2 建包 bean包 xff1a 专门放置属性类 dao包 xff1a 进行数据操作的
  • css高度从0到auto的transition动画

    如题 xff0c 想实现css高度从0到auto的transition动画 xff0c 发现直接写没有效果 查了一下 xff0c 发现可以用max height解决 xff0c 代码如下 lt DOCTYPE html gt lt html
  • beego打包

    beego打包 在main go 对应的目录下 windows平台 xff1a bee pack be GOOS 61 windows 打包后生成一个tar gz文件 xff0c 发送到部署服务器 xff0c 解压gz为tar xff0c
  • C++求解组合数的具体实现

    文章目录 前言问题起因组合公式公式变形递推公式递归实现备忘递归动态规划压缩DP其他优化 总结补充反向递归正向递推 前言 很少写关于具体算法的总结笔记 xff0c 因为很难把一个算法从头到尾的叙述清晰并且完整 xff0c 容易造成误解 这次想