文章目录

  • 前言
  • 问题起因
  • 组合公式
  • 公式变形
  • 递推公式
    • 递归实现
    • 备忘递归
    • 动态规划
    • 压缩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 的大小,所以临时写下的这段代码可以运行,不然这段代码会出现各种错误的。

组合公式

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

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

这个公式写出来清晰多了,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)! 是可以约分的,将这两个式子约分后,公式可以化简为:

Cnm=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!(n−m)!n!​=m!n(n−1)(n−2)...(n−m+1))​,(n≥m≥0)

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

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!,所以表示的范围还是比较小。

递推公式

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

{Cnm=1,(m=0或m=n)Cnm=Cn−1m+Cn−1m−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=0或m=n)Cnm​=Cn−1m​+Cn−1m−1​,(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)为例,看看下面的调用关系图:

#mermaid-svg-vONQ6b7QfEgfu8P1 .label{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);fill:#333;color:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .label text{fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .node rect,#mermaid-svg-vONQ6b7QfEgfu8P1 .node circle,#mermaid-svg-vONQ6b7QfEgfu8P1 .node ellipse,#mermaid-svg-vONQ6b7QfEgfu8P1 .node polygon,#mermaid-svg-vONQ6b7QfEgfu8P1 .node path{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-vONQ6b7QfEgfu8P1 .node .label{text-align:center;fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .node.clickable{cursor:pointer}#mermaid-svg-vONQ6b7QfEgfu8P1 .arrowheadPath{fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .edgePath .path{stroke:#333;stroke-width:1.5px}#mermaid-svg-vONQ6b7QfEgfu8P1 .flowchart-link{stroke:#333;fill:none}#mermaid-svg-vONQ6b7QfEgfu8P1 .edgeLabel{background-color:#e8e8e8;text-align:center}#mermaid-svg-vONQ6b7QfEgfu8P1 .edgeLabel rect{opacity:0.9}#mermaid-svg-vONQ6b7QfEgfu8P1 .edgeLabel span{color:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .cluster rect{fill:#ffffde;stroke:#aa3;stroke-width:1px}#mermaid-svg-vONQ6b7QfEgfu8P1 .cluster text{fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:12px;background:#ffffde;border:1px solid #aa3;border-radius:2px;pointer-events:none;z-index:100}#mermaid-svg-vONQ6b7QfEgfu8P1 .actor{stroke:#ccf;fill:#ECECFF}#mermaid-svg-vONQ6b7QfEgfu8P1 text.actor>tspan{fill:#000;stroke:none}#mermaid-svg-vONQ6b7QfEgfu8P1 .actor-line{stroke:grey}#mermaid-svg-vONQ6b7QfEgfu8P1 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .messageLine1{stroke-width:1.5;stroke-dasharray:2, 2;stroke:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 #arrowhead path{fill:#333;stroke:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .sequenceNumber{fill:#fff}#mermaid-svg-vONQ6b7QfEgfu8P1 #sequencenumber{fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 #crosshead path{fill:#333;stroke:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .messageText{fill:#333;stroke:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .labelBox{stroke:#ccf;fill:#ECECFF}#mermaid-svg-vONQ6b7QfEgfu8P1 .labelText,#mermaid-svg-vONQ6b7QfEgfu8P1 .labelText>tspan{fill:#000;stroke:none}#mermaid-svg-vONQ6b7QfEgfu8P1 .loopText,#mermaid-svg-vONQ6b7QfEgfu8P1 .loopText>tspan{fill:#000;stroke:none}#mermaid-svg-vONQ6b7QfEgfu8P1 .loopLine{stroke-width:2px;stroke-dasharray:2, 2;stroke:#ccf;fill:#ccf}#mermaid-svg-vONQ6b7QfEgfu8P1 .note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-vONQ6b7QfEgfu8P1 .noteText,#mermaid-svg-vONQ6b7QfEgfu8P1 .noteText>tspan{fill:#000;stroke:none}#mermaid-svg-vONQ6b7QfEgfu8P1 .activation0{fill:#f4f4f4;stroke:#666}#mermaid-svg-vONQ6b7QfEgfu8P1 .activation1{fill:#f4f4f4;stroke:#666}#mermaid-svg-vONQ6b7QfEgfu8P1 .activation2{fill:#f4f4f4;stroke:#666}#mermaid-svg-vONQ6b7QfEgfu8P1 .mermaid-main-font{font-family:"trebuchet ms", verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-vONQ6b7QfEgfu8P1 .section{stroke:none;opacity:0.2}#mermaid-svg-vONQ6b7QfEgfu8P1 .section0{fill:rgba(102,102,255,0.49)}#mermaid-svg-vONQ6b7QfEgfu8P1 .section2{fill:#fff400}#mermaid-svg-vONQ6b7QfEgfu8P1 .section1,#mermaid-svg-vONQ6b7QfEgfu8P1 .section3{fill:#fff;opacity:0.2}#mermaid-svg-vONQ6b7QfEgfu8P1 .sectionTitle0{fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .sectionTitle1{fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .sectionTitle2{fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .sectionTitle3{fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .sectionTitle{text-anchor:start;font-size:11px;text-height:14px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-vONQ6b7QfEgfu8P1 .grid .tick{stroke:#d3d3d3;opacity:0.8;shape-rendering:crispEdges}#mermaid-svg-vONQ6b7QfEgfu8P1 .grid .tick text{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-vONQ6b7QfEgfu8P1 .grid path{stroke-width:0}#mermaid-svg-vONQ6b7QfEgfu8P1 .today{fill:none;stroke:red;stroke-width:2px}#mermaid-svg-vONQ6b7QfEgfu8P1 .task{stroke-width:2}#mermaid-svg-vONQ6b7QfEgfu8P1 .taskText{text-anchor:middle;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-vONQ6b7QfEgfu8P1 .taskText:not([font-size]){font-size:11px}#mermaid-svg-vONQ6b7QfEgfu8P1 .taskTextOutsideRight{fill:#000;text-anchor:start;font-size:11px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-vONQ6b7QfEgfu8P1 .taskTextOutsideLeft{fill:#000;text-anchor:end;font-size:11px}#mermaid-svg-vONQ6b7QfEgfu8P1 .task.clickable{cursor:pointer}#mermaid-svg-vONQ6b7QfEgfu8P1 .taskText.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-vONQ6b7QfEgfu8P1 .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-vONQ6b7QfEgfu8P1 .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-vONQ6b7QfEgfu8P1 .taskText0,#mermaid-svg-vONQ6b7QfEgfu8P1 .taskText1,#mermaid-svg-vONQ6b7QfEgfu8P1 .taskText2,#mermaid-svg-vONQ6b7QfEgfu8P1 .taskText3{fill:#fff}#mermaid-svg-vONQ6b7QfEgfu8P1 .task0,#mermaid-svg-vONQ6b7QfEgfu8P1 .task1,#mermaid-svg-vONQ6b7QfEgfu8P1 .task2,#mermaid-svg-vONQ6b7QfEgfu8P1 .task3{fill:#8a90dd;stroke:#534fbc}#mermaid-svg-vONQ6b7QfEgfu8P1 .taskTextOutside0,#mermaid-svg-vONQ6b7QfEgfu8P1 .taskTextOutside2{fill:#000}#mermaid-svg-vONQ6b7QfEgfu8P1 .taskTextOutside1,#mermaid-svg-vONQ6b7QfEgfu8P1 .taskTextOutside3{fill:#000}#mermaid-svg-vONQ6b7QfEgfu8P1 .active0,#mermaid-svg-vONQ6b7QfEgfu8P1 .active1,#mermaid-svg-vONQ6b7QfEgfu8P1 .active2,#mermaid-svg-vONQ6b7QfEgfu8P1 .active3{fill:#bfc7ff;stroke:#534fbc}#mermaid-svg-vONQ6b7QfEgfu8P1 .activeText0,#mermaid-svg-vONQ6b7QfEgfu8P1 .activeText1,#mermaid-svg-vONQ6b7QfEgfu8P1 .activeText2,#mermaid-svg-vONQ6b7QfEgfu8P1 .activeText3{fill:#000 !important}#mermaid-svg-vONQ6b7QfEgfu8P1 .done0,#mermaid-svg-vONQ6b7QfEgfu8P1 .done1,#mermaid-svg-vONQ6b7QfEgfu8P1 .done2,#mermaid-svg-vONQ6b7QfEgfu8P1 .done3{stroke:grey;fill:#d3d3d3;stroke-width:2}#mermaid-svg-vONQ6b7QfEgfu8P1 .doneText0,#mermaid-svg-vONQ6b7QfEgfu8P1 .doneText1,#mermaid-svg-vONQ6b7QfEgfu8P1 .doneText2,#mermaid-svg-vONQ6b7QfEgfu8P1 .doneText3{fill:#000 !important}#mermaid-svg-vONQ6b7QfEgfu8P1 .crit0,#mermaid-svg-vONQ6b7QfEgfu8P1 .crit1,#mermaid-svg-vONQ6b7QfEgfu8P1 .crit2,#mermaid-svg-vONQ6b7QfEgfu8P1 .crit3{stroke:#f88;fill:red;stroke-width:2}#mermaid-svg-vONQ6b7QfEgfu8P1 .activeCrit0,#mermaid-svg-vONQ6b7QfEgfu8P1 .activeCrit1,#mermaid-svg-vONQ6b7QfEgfu8P1 .activeCrit2,#mermaid-svg-vONQ6b7QfEgfu8P1 .activeCrit3{stroke:#f88;fill:#bfc7ff;stroke-width:2}#mermaid-svg-vONQ6b7QfEgfu8P1 .doneCrit0,#mermaid-svg-vONQ6b7QfEgfu8P1 .doneCrit1,#mermaid-svg-vONQ6b7QfEgfu8P1 .doneCrit2,#mermaid-svg-vONQ6b7QfEgfu8P1 .doneCrit3{stroke:#f88;fill:#d3d3d3;stroke-width:2;cursor:pointer;shape-rendering:crispEdges}#mermaid-svg-vONQ6b7QfEgfu8P1 .milestone{transform:rotate(45deg) scale(0.8, 0.8)}#mermaid-svg-vONQ6b7QfEgfu8P1 .milestoneText{font-style:italic}#mermaid-svg-vONQ6b7QfEgfu8P1 .doneCritText0,#mermaid-svg-vONQ6b7QfEgfu8P1 .doneCritText1,#mermaid-svg-vONQ6b7QfEgfu8P1 .doneCritText2,#mermaid-svg-vONQ6b7QfEgfu8P1 .doneCritText3{fill:#000 !important}#mermaid-svg-vONQ6b7QfEgfu8P1 .activeCritText0,#mermaid-svg-vONQ6b7QfEgfu8P1 .activeCritText1,#mermaid-svg-vONQ6b7QfEgfu8P1 .activeCritText2,#mermaid-svg-vONQ6b7QfEgfu8P1 .activeCritText3{fill:#000 !important}#mermaid-svg-vONQ6b7QfEgfu8P1 .titleText{text-anchor:middle;font-size:18px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-vONQ6b7QfEgfu8P1 g.classGroup text{fill:#9370db;stroke:none;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:10px}#mermaid-svg-vONQ6b7QfEgfu8P1 g.classGroup text .title{font-weight:bolder}#mermaid-svg-vONQ6b7QfEgfu8P1 g.clickable{cursor:pointer}#mermaid-svg-vONQ6b7QfEgfu8P1 g.classGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-vONQ6b7QfEgfu8P1 g.classGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-vONQ6b7QfEgfu8P1 .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5}#mermaid-svg-vONQ6b7QfEgfu8P1 .classLabel .label{fill:#9370db;font-size:10px}#mermaid-svg-vONQ6b7QfEgfu8P1 .relation{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-vONQ6b7QfEgfu8P1 .dashed-line{stroke-dasharray:3}#mermaid-svg-vONQ6b7QfEgfu8P1 #compositionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-vONQ6b7QfEgfu8P1 #compositionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-vONQ6b7QfEgfu8P1 #aggregationStart{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-vONQ6b7QfEgfu8P1 #aggregationEnd{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-vONQ6b7QfEgfu8P1 #dependencyStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-vONQ6b7QfEgfu8P1 #dependencyEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-vONQ6b7QfEgfu8P1 #extensionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-vONQ6b7QfEgfu8P1 #extensionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-vONQ6b7QfEgfu8P1 .commit-id,#mermaid-svg-vONQ6b7QfEgfu8P1 .commit-msg,#mermaid-svg-vONQ6b7QfEgfu8P1 .branch-label{fill:lightgrey;color:lightgrey;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-vONQ6b7QfEgfu8P1 .pieTitleText{text-anchor:middle;font-size:25px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-vONQ6b7QfEgfu8P1 .slice{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-vONQ6b7QfEgfu8P1 g.stateGroup text{fill:#9370db;stroke:none;font-size:10px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-vONQ6b7QfEgfu8P1 g.stateGroup text{fill:#9370db;fill:#333;stroke:none;font-size:10px}#mermaid-svg-vONQ6b7QfEgfu8P1 g.statediagram-cluster .cluster-label text{fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 g.stateGroup .state-title{font-weight:bolder;fill:#000}#mermaid-svg-vONQ6b7QfEgfu8P1 g.stateGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-vONQ6b7QfEgfu8P1 g.stateGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-vONQ6b7QfEgfu8P1 .transition{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-vONQ6b7QfEgfu8P1 .stateGroup .composit{fill:white;border-bottom:1px}#mermaid-svg-vONQ6b7QfEgfu8P1 .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px}#mermaid-svg-vONQ6b7QfEgfu8P1 .state-note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-vONQ6b7QfEgfu8P1 .state-note text{fill:black;stroke:none;font-size:10px}#mermaid-svg-vONQ6b7QfEgfu8P1 .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.7}#mermaid-svg-vONQ6b7QfEgfu8P1 .edgeLabel text{fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .stateLabel text{fill:#000;font-size:10px;font-weight:bold;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-vONQ6b7QfEgfu8P1 .node circle.state-start{fill:black;stroke:black}#mermaid-svg-vONQ6b7QfEgfu8P1 .node circle.state-end{fill:black;stroke:white;stroke-width:1.5}#mermaid-svg-vONQ6b7QfEgfu8P1 #statediagram-barbEnd{fill:#9370db}#mermaid-svg-vONQ6b7QfEgfu8P1 .statediagram-cluster rect{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-vONQ6b7QfEgfu8P1 .statediagram-cluster rect.outer{rx:5px;ry:5px}#mermaid-svg-vONQ6b7QfEgfu8P1 .statediagram-state .divider{stroke:#9370db}#mermaid-svg-vONQ6b7QfEgfu8P1 .statediagram-state .title-state{rx:5px;ry:5px}#mermaid-svg-vONQ6b7QfEgfu8P1 .statediagram-cluster.statediagram-cluster .inner{fill:white}#mermaid-svg-vONQ6b7QfEgfu8P1 .statediagram-cluster.statediagram-cluster-alt .inner{fill:#e0e0e0}#mermaid-svg-vONQ6b7QfEgfu8P1 .statediagram-cluster .inner{rx:0;ry:0}#mermaid-svg-vONQ6b7QfEgfu8P1 .statediagram-state rect.basic{rx:5px;ry:5px}#mermaid-svg-vONQ6b7QfEgfu8P1 .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#efefef}#mermaid-svg-vONQ6b7QfEgfu8P1 .note-edge{stroke-dasharray:5}#mermaid-svg-vONQ6b7QfEgfu8P1 .statediagram-note rect{fill:#fff5ad;stroke:#aa3;stroke-width:1px;rx:0;ry:0}:root{--mermaid-font-family: '"trebuchet ms", verdana, arial';--mermaid-font-family: "Comic Sans MS", "Comic Sans", cursive}#mermaid-svg-vONQ6b7QfEgfu8P1 .error-icon{fill:#522}#mermaid-svg-vONQ6b7QfEgfu8P1 .error-text{fill:#522;stroke:#522}#mermaid-svg-vONQ6b7QfEgfu8P1 .edge-thickness-normal{stroke-width:2px}#mermaid-svg-vONQ6b7QfEgfu8P1 .edge-thickness-thick{stroke-width:3.5px}#mermaid-svg-vONQ6b7QfEgfu8P1 .edge-pattern-solid{stroke-dasharray:0}#mermaid-svg-vONQ6b7QfEgfu8P1 .edge-pattern-dashed{stroke-dasharray:3}#mermaid-svg-vONQ6b7QfEgfu8P1 .edge-pattern-dotted{stroke-dasharray:2}#mermaid-svg-vONQ6b7QfEgfu8P1 .marker{fill:#333}#mermaid-svg-vONQ6b7QfEgfu8P1 .marker.cross{stroke:#333}:root { --mermaid-font-family: "trebuchet ms", verdana, arial;}#mermaid-svg-vONQ6b7QfEgfu8P1 {color: rgba(0, 0, 0, 0.75);font: ;}

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)的方式来实现。

动态规划

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

再回顾一下递推公式:

{Cnm=1,(m=0或m=n)Cnm=Cn−1m+Cn−1m−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=0或m=n)Cnm​=Cn−1m​+Cn−1m−1​,(n>m>0)​

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

n\m (0) (1) (2) (3) (4) (5)
(0) 1
(1) 1 1
(2) 1 2 1
(3) 1 3 3 1
(4) 1 4 6 4 <1>
(5) 1 5 10 ==>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) 1 1
(2) 1 2 1
(3) 3 3 1
(4) 6 4
(5) ==>10

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

总结

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

补充

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

{Cnm=1,(m=0或m=n)Cnm=n−m+1mCnm−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=0或m=n)Cnm​=mn−m+1​Cnm−1​,(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 这种采用缓存的方式可能会是更好的选择。


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


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

C++求解组合数的具体实现相关推荐

  1. FZU2020 lucas定理求解组合数

    组合 给出组合数C(n,m), 表示从n个元素中选出m个元素的方案数.例如C(5,2) = 10, C(4,2) = 6.可是当n,m比较大的时候,C(n,m)很大!于是xiaobo希望你输出 C(n ...

  2. 组合数求解与(扩展)卢卡斯定理

    前言: 咳咳咳咳 ,最近瘟疫盛行,围观的记得要戴口罩. 求解组合数的方法大家应该都见了很多了,这篇博客将围绕这个问题进行归纳和深入学习. 问题: 给定 n , k , p n,k,p n,k,p求解组 ...

  3. Codeforces Beta Round #95 (Div. 2) 部分解题报告 (dp,组合数,)

    做这样的比赛既考快速编码的能力,还有快速思维的能力.本人很弱,跌了rating..加油!!!.. 第一题上来就把题意理解错了..粗心啊..直接模拟着做就行:1:如果字符串全是大写字母就进行大小写转换: ...

  4. C. The Intriguing Obsession(神仙组合数)

    不会写,看的题解,emm,很厉害讷 为了方便解释,假设现在只需要考虑两个集合红,蓝(a,b) 那么同集合间的点肯定不能连边,否则距离是1了那么同集合间的点肯定不能连边,否则距离是1了那么同集合间的点肯 ...

  5. 组合数c(n,m)计算的四种方法

    转载自 组合c(m,n)的计算方法    问题:求解组合数C(n,m),即从n个相同物品中取出m个的方案数,由于结果可能非常大,对结果模10007即可. 共四种方案.ps:注意使用限制. 方案1: 暴 ...

  6. 组合数C(n,m)的四种计算方法

    转载自 组合c(m,n)的计算方法    问题:求解组合数C(n,m),即从n个相同物品中取出m个的方案数,由于结果可能非常大,对结果模10007即可. 共四种方案.ps:注意使用限制. 方案1: 暴 ...

  7. 4002 构造数组(可重复组合数问题--隔板法)

    1. 问题描述: 现在需要构造一对数组 (a,b),要求: 数组 a 和数组 b 的长度都为 m: 两个数组中的元素的取值范围都是 [1,n]: ∀i∈[1,m],ai ≤ bi: 数组 a 中元素非 ...

  8. 【组合数算法】以及校赛小复盘

    HUSTOJ 这题真的不难,比赛的时候对字符串数组不熟悉 复盘感觉当时应该错了两点细节 字符串真的好多细节 C++中输入字符串的几种方法_c++输入字符串_lixiaoyu20106587的博客-CS ...

  9. LeetCode 1569. 将子数组重新排序得到同一个二叉查找树的方案数(DP)

    文章目录 1. 题目 2. 解题 1. 题目 给你一个数组 nums 表示 1 到 n 的一个排列. 我们按照元素在 nums 中的顺序依次插入一个初始为空的二叉查找树(BST). 请你统计将 num ...

  10. Codeforces Round #697 (Div. 3)A~G解题报告

    Codeforces Round #697 (Div. 3)A~G解题报告 题 A Odd Divisor 题目介绍 解题思路 乍一想本题,感觉有点迷迷糊糊,但是证难则反,直接考虑没有奇数因子的情况, ...

最新文章

  1. 测试服务命名和动态注册路由的方式@Xan
  2. 独家 | 使EfficientNet更有效率的三种方法(附链接)
  3. 解决 mybatis-generator-maven-plugin 中 overwrite 配置无效的问题
  4. PHP RSA2加密和解密以及接口签名和验签
  5. 为什么使用ES6生成器
  6. [转]ASP.Net篇之Session与Cookie
  7. java请求接口示例_Java 8:功能接口示例
  8. Android Ac 控件,Android控件--MultiAutoCompleteTextView
  9. 最受白领欢迎的12大办公软件
  10. UIPickView 和 UIDatePicker
  11. QT下信号与槽不在同一个线程中如何connect
  12. csp php,了解CSP
  13. 以下7种硬件测试的种类,不知道的赶紧收藏了!
  14. FastQC 与 质控
  15. 林轩田《机器学习基石》(九)—— Linear regression
  16. [感动]知道我为什么喜欢SUPER JUNIOR吗?
  17. java后端工程师面试题(笔试):2022-11-04 经历(一)
  18. 通过kiwix浏览wikipedia
  19. 主板电池(华硕主板,电池是KTS的)问题导致电脑无法启动
  20. Sublist3r使用教程-子域名扫描工具

热门文章

  1. 短信机bug,发短信发的直吐血…………
  2. Kaggle注册及绑定手机号
  3. php元万亿单位转换,万与亿的换算(万元换成亿元换算器)
  4. 用CSS实现对话气泡框!
  5. windows server2019共享选项中网络发现无法启用
  6. 华三交换机配置access命令_H3C 交换机常用配置命令
  7. android多开技术,多开常见配置 - 技术交流 - 逍遥安卓论坛 - Powered by Discuz!
  8. 中国矿业大学计算机学院进复试,拟录取名单陆续公布!初试第二败北,倒数第一逆袭!...
  9. SAI颈部正面的画法
  10. Power BI应用案例:淘宝用户行为分析实战