Elixir求解找零钱问题

找零钱是一个经典的动态规划问题,我们需要利用一堆零钱凑出想要的金额,并使得使用的零钱数量最少。例如给你 [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

  • 9
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值