凑 40 元问题

有这样一个问题:有 1 元、2 元、5 元和 10 元纸币各无数张,凑出 40 元,共有几种方案?

最简单直接的方法就是硬算。可以按面值的选择方式分类,再分别去计算各种情况。

面值选择 方案数
单一币种 4
5元,10元 3
2元,10元 3
2元,5元 3
1元,10元 3
1元,5元 7
1元,2元 19
2元,5元,10元 3
1元,5元,10元 9
1元,2元,10元 27
1元,2元,5元 65
1元,2元,5元,10元 49

后面几种组合方案算起来还是比较麻烦的。这种硬算的方法耗时较长,容易出错。最要命的是,有多次重复计算。作为一个对计算机算法有一点皮毛了解的人,这种重复计算自然是不能忍受的。于是开始思考效率更高的算法。

对于特定目标值的组合方案数,可以分成以下两类:包含最大面值的与不包含最大面值的。以上面这道题为例,用 1、2、5、10 元凑 40 元的方案可以分为:包含 10 元的和不包含 10 元的。这样就形成了递归。

x[i]=[1, 2, 5, 10]

count (a, b)
    if a<0 return 0
    if a=0 return 1
    if b=1 return count(a-x[1], 1)
    return count(a, b-1) + count(a-x[b], b)

上面这个算法,x 为允许使用的面值数组,必须按照递增顺序排列。下标从 1 开始,即 x1=1, x2=2, x3=5, x4=10。 count(a,b) 表示用前 b 种面值凑 a 元的方案数。注意当 a 等于 0 时,函数要返回 1,这是符合递归规律的正确结果。对于凑 40 元问题,即求出 count(40, 4)。数值不大,所以我们可以用手算来复现计算机算法的计算过程。

flowchart TD
    A["(40, 4)"]
    B1["(40, 3)"]
    B2["(30, 4)"]
    A --> B1
    A --> B2
    C1["(40, 2)"]
    C2["(35, 3)"]
    C3["(30, 3)"]
    C4["(20, 4)"]
    B1 --> C1
    B1 --> C2
    B2 --> C3
    B2 --> C4
    D1["(35, 2)"]
    D2["(30, 3)"]
    D3["(20, 3)"]
    D4["(10, 4)"]
    C2 --> D1
    C2 --> D2
    C4 --> D3
    C4 --> D4
    E1["(30, 2)"]
    E2["(25, 3)"]
    E3["(10, 3)"]
    E4["(0, 4)"]
    D2 --> E1
    D2 --> E2
    D4 --> E3
    D4 --> E4
    F1["(25, 2)"]
    F2["(20, 3)"]
    E2 --> F1
    E2 --> F2
    G1["(20, 2)"]
    G2["(15, 3)"]
    F2 --> G1
    F2 --> G2
    H1["(15, 2)"]
    H2["(10, 3)"]
    G2 --> H1
    G2 --> H2
    I1["(10, 2)"]
    I2["(5, 3)"]
    H2 --> I1
    H2 --> I2
    J1["(5, 2)"]
    J2["(0, 3)"]
    I2 --> J1
    I2 --> J2

我们可以发现,第四层调用出现了 (30, 3),与第三层的相同调用可以合并,后续的调用层中也出现了可以合并的相同调用。所以用这种方法,效率又高,又不易出错。