动态规划之硬币找零问题

动态规划 Dynamic Programming

  • 理查德·贝尔曼(Richard Bellman)在1957年提出了动态规划(Dynamic Programming, 即 DP)一词
  • 通过将解决方案与包含一般子问题的子问题组合来解决问题
  • DP 与 分治之间的不同点:
    • 使用分而治之解决这些问题的效率很低, 因为相同的子问题必须多次解决(暂时缺乏具体的例子, 需要多做题目)
    • DP 将解决每个问题一次, 并将其答案存储在表格中以备将来参考

硬币找零问题

  • 目标: 给定硬币的面值, 例如, 1, 5, 10, 25, 100, 设计一种使用最少数量的硬币向客户付款的方法
  • 收银员算法(Cashier's algorithm): 在每次迭代中, 添加不会使我们超过要支付的金额的最大值的硬币
  • 例子: 33 美分(33 cents) --> 一个 25 美分 + 一个 5 美分 + 三个 1 美分

收银员算法

将 n 个硬币按照面值进行排序, 使得:

\[c_1 < c_2 < ... < c_n \]

\(S \leftarrow \empty\)

WHILE x > 0

     k \(\leftarrow\) largest coin denomination \(c_k\) such that \(c_k \leq x\)(最大硬币面值 \(c_k\) 使得 \(c_k \leq x\))
     IF no such k, RETURN "no solution"
     ELSE
          \(x \leftarrow x - c_k\)
          \(S \leftarrow S \ \cup \ \{ k \}\)
RETURN S

找零(Coin Changing)

这个贪婪算法 -- 收银员算法是最优的吗?

定理

收银员算法对于美国硬币来说是最佳的算法, 即这个贪婪算法可以求出最优解(U.S. coins -- 1, 5, 10, 25, 100).

可以根据 x 进行归纳证明(存疑, 待补充, 算法本身肯定是正确的).

其他的情况

  • 收银员算法在其他的情况下可能并不奏效
  • 考虑美国的邮费: 1, 10, 21, 34, 70, 100, 350, 1225, 1500(这些都是美国邮费的面值, 并且可能每年都会变)
    • 使用收银员算法: 140 美分 = 100 + 34 + 1 + 1 + 1 + 1 + 1 + 1
    • 而最佳的做法: 140c(即 140 美分) = 70 + 70
  • 在某些情况下, 甚至可能无法找到可行的解决方案

动态规划

  • 为了对 n 美分进行找零, 我们将首先弄清楚如何对每个值 \(x < n\) 进行找零
  • 我们可以从解决方案中构建出更小的值
    • \(C[n]\) 为给 n 美分找零所需的最少的硬币数量
    • \(x\) 为最优解中使用的第一个硬币的值
    • 然后 \(C[n] = 1 + C[n - x]\)
  • 问题的关键是: 我们不知道 x 的值

动态规划算法

我们将尝试所有可能的 \(x\) 值, 并取最小值

\[C[n] = \left\{\begin{matrix} min_{i:d_i \leq n}\left\{ C[n - d_i] + 1 \right\} \qquad if \ n > 0 & \\ 0 \qquad \qquad \qquad \qquad \qquad \qquad \ if \ n = 0& \\ \end{matrix}\right. \]

Change 代码

伪码描述

int Change(int n)
{
    if (n < 0>)
        return INFTY;
    else if (n == 0)
        return 0;

    return 1 + min(Change(n - d1), Change(n - d2), Change(n - d3));
}

图解

\(n = 12, \ d_1 = 1, \ d_2 = 3, \ d_3 = 7\)

20201113003820

这棵树的根节点是我们要求解的 n, 然后 n 分别减去 \(d_1, d_2, d_3\) 得到其三个子节点, 然后从左到右依次递归分解这 3 个子节点, 然后以此类推.

最后求解出来的找零所需的最少的硬币的数目是 4, 根据上面的树可以发现, 具体的分法不是唯一的.

动态规划的要素

DP 用于解决具有以下特征的问题:

  • 最优子结构(最优原理): 问题的最优解决方案中包括子问题的最优解决方案
  • 子问题重叠: 在某些地方, 我们多次解决同一子问题

动态规划的步骤

  • 表征最佳子结构
  • 递归定义最佳解决方案的值
  • 自下而上计算值
  • (如果需要)构建最佳解决方案

记忆化(Memoization)

  • 记忆化是处理重叠子问题的一种方法
    • 计算出子问题的解决方案后, 将其存储在表中
    • 后续调用
  • 可以修改递归算法以使用记忆化
  • Change() 有许多重复的工作, 使用一个表来使得算法的时间复杂度变为 \(O(nk)\)

DP_Change(n)

伪码描述

int DP_Change(int n)
{
    int tmp, i, j;

    for(i = 1, C[0] = 0; i<= n; i++)
    {
        tmp = INFTY;
        for(j = 0; j < ; j++)
            if (d[j] <= i && C[i - d[j]] + 1 < tmp)
                tmp = C[i - d[j]] + 1; // 更新最小值
        C[i] = tmp;
    }
    return tmp; // 最后返回的即 C[n]
}

这个算法和上面的递归相反, 是从底向上进行构建的.

从底向上构建

n 0 1 2 3 4 5 6 7 8 9 10 11 12
\(C[n]\) 0 1 2 1 2 3 2 1 2 3 2 3 4
\(d_1\) 0 1 2 0 1 2 0 0 1 2 0 1 2
\(d_2\) 0 0 0 1 1 1 2 0 0 0 1 1 1
\(d_3\) 0 0 0 0 0 0 0 1 1 1 1 1 1

动态规划 VS 贪婪算法

  • DP 适合具有以下特征的问题:
    • 最优子结构: 问题的最优解决方案包括子问题的最优解决方案
    • 子问题重叠: 子问题总数很少, 每个子问题都有许多重复出现的实例
  • 自下而上, 建立一张已解决的子问题表, 用于解决较大的子问题
  • 贪婪算法是自上而下的, 动态规划可能是过大的(overkill); 贪婪算法往往更容易编码.
posted @ 2020-11-13 01:23  模糊计算士  阅读(1169)  评论(0编辑  收藏  举报