从递归到动态规划(二)换零钱问题

sicp(计算机程序的构造和解释 )中有这样一个问题。因为那里的问题用的是美金,我这里就换成人民币吧。这个问题是这样的,现在有1元、5元、10元、20元、50元的纸币,给定任意的数量的现金,请计算出所有数零钱的方式。

咋眼一看,这题觉得挺麻烦的。要怎么去想。我是习惯用类似数学归纳法那种思维去想的。

比如

只有 1 元,那当然只有 1 种换零钱的方式,

如有有 4 元,4 个 1,只有 1 种。

如果有 6 元,那就有 (5,1), (1,1,1,1,1,1), 这 2 种方式

如果是 11 元,那就是 (10,1), (5,5,1), (5,1{6}), (1{11}),这 4 种方式。

来推一下,如果给定的钱是 11 元

  • 如果纸币的钱大于 11 元,这是不可能的,所以 20、50 元不能用
  • 所以剩下可以用 {10,5,1} 这三种纸币
  • 那么换零钱的方式分成两类,有 10 元纸币和没有 10 元纸币的,有 10 元纸币的就有 (10,1) 一种换法,而没有 10 元纸币的换法,看下嘛
  • 没有 10 元纸币的换法,也就是只有 5 元和 1 元 纸币的换法。也可以再分,有5元纸币的换法和没有5元纸币的换法。
  • 有 5 元纸币有两种,一种是有一张 5 元,(5,1*6),另一种是有二张 5 元,(5,5,1)
  • 只有1元纸币的换法,(1*11)

所以共 4 种方式。

再抽象一下。换零钱的所有数目分成两类

  1. 零钱减去一种纸币后,(包括自己在内的)所有换零钱的方法,比如,对于 5 元, 11 - 5 后,还有 (5,5,1) 这样的一种;对于 20 元,而 11 - 20,这样就直接 0 种了

  2. 零钱除去一种纸币后的所有换零钱方式。

可能要思考一下。

用上面那个例子解释一下。

比如,换零钱的函数是 F,而数组 A 代表能使用的纸币(假如 A 是由大到小排序)。 递归的过程会是这样的。

如果是给定的条件 11 元,那么其实可以用挺数学的方式去理解的。

F(X=11,A=[50,20,10,5,1]) 
F(X=11,A=[50,20,10,5,1]) = F(11,[10,5,1])
                         = F(11-10, [10,5,1]) + F(11, [5,1])
                         = 1 + F(11,[5,1]) 
                         = 1 + F(6, [5,1]) + F(11,[1])
                         = 4;
复制代码

那么就可以把代码写成这样的(用个end变量就不用在递归程序中直接裁剪数组中的元素了)

int count(int money, vector<int> &array, int end) {
  if (money < 0) {
    return 0;
  } else if (money == 0) {
    return 1;
  } else if (end == 0) {
    return 0;
  } else {
    return count(money - array[end], array, end) + count(money, array, end - 1);
  }
}

int count(int money) {
  vector<int> a = {0, 1, 5, 10, 20, 50};
  return count(money, a, a.size() - 1);
}
复制代码

但与前面的文章类似都有重复计算的问题的。所以,可以用自底向上的方式,反向递推。

用个二维数组存储换零钱的次数,横坐标是金钱,纵坐标是纸币。用 v[i][j] 表示。

  • 如果 i == 0 或者 j == 0 ,那么当然是0了。
  • 如果 i == 1 也就是说只用 1 元纸币,那么所以金额都是 1 种表示的方式。
  • 如果 i == 2,也就说用 1 元或者 5 元。那就要分 3 种情况了
  • j < 5 ,金额小于 5 ,但起码有 1 元的换零方式,所以 v[i][j] = v[i-1][j]了。
  • j >= 5 ,说明换零的方式中有5 元纸币,所以 v[i][j] = v[i-1][j] + v[i][j-5]
  • 如果 i== 3, 也是相类似的。

所以,可以写出这样的代码。

int count(int money) {
  vector<int> a = {1, 5, 10, 20, 50};

  int v[a.size()][money + 1];
  for (int i = 0; i < a.size(); i++) {
    for (int j = 0; j < money + 1; j++) {
      if (i == 0) {
        v[i][j] = 1;
      } else {
        v[i][j] = v[i - 1][j];
        if (j >= a[i]) {
          v[i][j] += v[i][j - a[i]];
        }
      }
    }
  }
  return v[a.size() - 1][money];
}
复制代码

这段代码,还能优化一下,因为当 i > 0, 每次循环都有v[i][j] = v[i - 1][j];这样的语句,也就是将上一行复制到当前行,这其实没有必要的。也就是说其实可以用一维数组就可以了,无须用二维数组。

所以最后可以变成这样了。

int count(int money) {
  vector<int> a = {1, 5, 10, 20, 50};

  int *v = new int[money + 1];
  for (int i = 0; i < money + 1; i++)
    v[i] = 1;

  for (int i = 1; i < a.size(); i++) {
    for (int j = a[i]; j < money + 1; j++) {
      v[j] += v[j - a[i]];
    }
  }

  int res = v[money];
  delete[] v;
  return res;
}
复制代码
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值