程序员的算法趣题2
上QQ阅读APP看书,第一时间看更新

Q 05 杨辉三角

IQ 70
目标时间:20 分钟

我们在学习规律性时经常能看到杨辉三角。每行左右两端的数都是 1,其他位置上的数等于其左上和右上两数之和,通过这种方式形成的就是杨辉三角。

现在我们假设杨辉三角中的数表示金额(以日元为单位)。例如,“1”是 1 日元,“2”是 2 日元,“10”是 10 日元。针对第行中的各个数,思考每个数表示的金额需要用到的纸币和硬币的数量的最小值,然后求该行中纸币和硬币的数量之和。

例如,当时,数组为 [1, 4, 6, 4, 1],因为 1 日元 =1(1 枚硬币),4 日元 =4(4 枚硬币),6 日元 =2(1 枚 5 日元的硬币 + 1 枚 1 日元的硬币),所以纸币和硬币的数量加起来一共是 12。同样,当时,纸币和硬币的数量一共是 48(图 1.7)。

另外,本题中可以使用的日元包括 1 日元、5 日元、10 日元、50 日元、100 日元和 500 日元的硬币,以及 1000 日元、2000 日元、5000 日元和 10 000 日元的纸币。

图 1.7 杨辉三角和计算示例

问题

时,纸币和硬币的数量之和是多少?

思路

本题我们分 3 步来求解。第 1 步,生成杨辉三角;第 2 步,以杨辉三角中第行的数为元素创建数组,针对数组中的各个数值,计算最少需要多少张纸币和多少枚硬币;第 3 步,把该行每个数所需纸币和硬币的最低数量加起来,求和。

图 1.8 所示,预设好数组后按从右往左的顺序计算,根据上一行数组中的值可以计算出下一行数组中的值。

图 1.8 按照从右往左的顺序计算数组中的值

先从第 1 行开始按顺序进行加法运算,直到取得目标行的值,然后算出该行所需纸币和硬币的最低数量。将纸币和硬币的面额按照从大到小的顺序进行除法运算,得到的商就是所需纸币和硬币的数量。

以 178 日元为例。用 178 除以 100,商是 1,所以 100 日元的硬币有 1 枚。余数是 78,除以 50 后得到的商是 1,所以 50 日元的硬币有 1 枚。接着用余数 28 除以 10,得到的商是 2,即 10 日元的硬币有 2 枚。如此循环,就能求出纸币和硬币的数量总和。

实现这个算法的程序如代码清单 05.01 和代码清单 05.02 所示。

代码清单 05.01q05.rb

N = 45

def count(n)
  coin = [10000, 5000, 2000, 1000, 500, 100, 50, 10, 5, 1]
  result = 0
  coin.each do |c|
    # 按照面额从大到小的顺序计算商和余数
    cnt, n = n.divmod(c)
    result += cnt
  end
  result
end

row = [0] * (N + 1);
row[0] = 1;
N.times do |i|
  # 每行都是从右往左赋值
  (i + 1).downto(1) do|j|
    # 前一行相应位置的值加上其左边的值
    row[j] += row[j - 1]
  end
end

# 计算总数
puts row.map{|i| count(i)}.inject(:+)

代码清单 05.02q05.js

N = 45;

function count(n){
  var coin = [10000, 5000, 2000, 1000, 500, 100, 50, 10, 5, 1];
  var result = 0;
  for (var i = 0; i < coin.length; i++){
    // 按照面额从大到小的顺序计算商和余数
    var cnt = Math.floor(n / coin[i]);
    n = n % coin[i];
    result += cnt;
  }
  return result;
}

row = new Array(N + 1);
row[0] = 1;
for (var i = 1; i < N + 1; i++){
  row[i] = 0;
}
for (var i = 0; i < N; i++){
  // 每行都是从右往左赋值
  for (var j = i + 1; j > 0; j--)
    // 前一行相应位置的值加上其左边的值
    row[j] += row[j - 1];
}

// 计算总数
var total = 0;
for (var i = 0; i < N + 1; i++){
  total += count(row[i]);
}
console.log(total);

答案 3 518 437 540

数学小知识 贪心算法

在从多个选项中选择一个最优项的时候,我们可以使用遍历法比较所有选项,也可以根据问题制定基准,以此来轻松求得最优项。后者叫作贪心算法(greedy algorithm)。

本题中出现的“依据金额求纸币和硬币的数量之和的方法”就是贪心算法的典型示例。在求数量的最小值时,原本需要找出所有的组合方式。但是,在求最少要用多少张纸币和多少枚硬币时,按照面额从大到小的顺序组合使用它们就能求解,这种方法更加直观易懂。

这种方法虽然对于某些问题不能奏效,但是能以简单的程序快速得到答案,因此也经常被人们使用。