找零钱是一个经典的动态规划问题,我们需要利用一堆零钱凑出想要的金额,并使得使用的零钱数量最少。例如给你 [1, 5, 10, 25, 100]
和目标15,结果应该是 [5, 10]
。
这是一个递归问题,假定零钱列表是 list
,目标是 target
。对于 list
中的每一种零钱 x
,使用它的最少零钱数是凑出 target-x
所需最少零钱数加一。最终的结果是使用 list
中每种零钱所需最小数量再求最小值。
假设我们用 min(n)
表示凑出 n
需要的最小钱币数,以示例为例,求解min(15)
需要求解 [min(14), min(10), min(5), nil, nil]
,对于负数,是无解的,所以我们直接用 nil
表示。而求解 min(14)
,我们需要求解 [min(13), min(9), min(4), nil, nil]
。
但是直接递归时间复杂度太高,如果把上面的递归过程写完整,会发现有相当多重复的计算。因此我们需要把计算结果保存下来,减少重复计算。求解 min(15)
是从15,14,13…1 往下求解,但实际上我们会从 1,2,3…15 往上求解。这样当我们求解 min(x)
时,可以保证 min(target-x)
一定已经求解过了,可以直接使用其结果。
使用go或c++很容易通过循环写出算法,但是elixir是函数式语言,它没有循环!不仅没有循环,也有全局变量!没有循环和全局变量,这是从命令式语言转向函数式语言的一大障碍,许多人在使用函数式编程语言时,会突然发现自己不会写代码了。elixir其实也不是没有循环,函数式语言的循环可以通过列表和递归来实现,这是函数式语言的两大利器,用好列表可以解决绝大多数问题。
让我们转换一下思维,从数据转换的角度来看待这个问题。我们需要做的转换是 1..target
到 [min(1)..min(target)]
,对于列表中的每一项 min(x)
,它来自于 [min(x-1), min(x-5), min(x-10), min(x-25), min(x-100)]
,这里我们直接使用示例做为讲解。而对于这个列表中的每一项,其实我们都不需要计算,因为他们一定在求解 min(x)
之前就被计算出来了。
好了思路清晰了,现在的问题是如何在循环过程中记录中间结果,毕竟没有全局变量可以用,而且变量还不可变。这里我们需要用到 Enum.reduce
来做循环,他会在每次循环时带上累加结果 acc
,此时 acc
就是那个我们需要的 “全局变量”, reduce
不仅可以用来做累加累乘,它的结果也可以是一个列表,字典,任意都行。
代码:
defmodule Change do
@doc """
Determine the least number of coins to be given to the user such
that the sum of the coins' value would equal the correct amount of change.
It returns {:error, "cannot change"} if it is not possible to compute the
right amount of coins. Otherwise returns the tuple {:ok, list_of_coins}
## Examples
iex> Change.generate([5, 10, 15], 3)
{:error, "cannot change"}
iex> Change.generate([1, 5, 10], 18)
{:ok, [1, 1, 1, 5, 10]}
"""
@spec generate(list, integer) :: {:ok, list} | {:error, String.t()}
def generate(_, target) when target < 0 ,do: {:error, "cannot change"}
def generate(_, 0) ,do: {:ok, []}
def generate(coins, target) do
changes = 1..target |> Enum.reduce(%{0=>[]}, &(do_generate(coins, &1, &2)))
case changes[target] do
nil -> {:error, "cannot change"}
res -> {:ok, res}
end
end
def do_generate(coins, target, acc) do
coins
|> Enum.filter(&(acc[target-&1]))
|> Enum.map(&[&1|acc[target-&1]])
|> Enum.min_by(&length/1, fn -> nil end)
|> then(&(Map.put(acc, target, &1)))
end
end