用函数式编程和尾递归解决硬币变化问题?

时间:2015-04-26 04:48:17

标签: scala recursion functional-programming dynamic-programming tail-recursion

我知道如何用命令式编程和DP来解决硬币变化问题。

我知道如何用FP和非尾递归来解决硬币变化问题。但它会多次计算同样的问题,导致效率低下。

我知道如何使用FP和DP /尾递归来计算斐波纳契数。很多文章都使用这个例子来解释" FP可以与DP" "递归也可以像循环一样有效"

但我不知道如何用FP和DP /尾递归来解决硬币变化问题。

我觉得奇怪的是,关于命令式编程的文章总是会提到在硬币更改问题上多次计算同一问题的效率低下,而FP上的那些只是省略它。

在更一般的意义上,我想知道FP是否足够强大以解决这种类型的"二维"问题,而斐波那契是一个"一维"问题。有人能帮助我吗?

硬币改变问题:

  

给定值N,如果我们想要改变N美分,我们就有   每个S = {S1,S2,..,Sm}价值硬币的无限供应,如何   我们可以通过多种方式进行改变?硬币的顺序无关紧要。

     

例如,对于N = 4和S = {1,2,3},有四种解决方案:   {1,1,1,1},{1,1,2},{2,2},{1,3}。因此输出应为4.对于N = 10和S.   = {2,5,3,6},有五种解决方案:{2,2,2,2,2},{2,2,3,3},{2,2,6},{2,3 ,5}和{5,5}。所以输出应该是5。

我在学习Scala时遇到了这个问题,所以使用Scala来演示代码(如果可能的话)。 Lisp也会很好。但我对Haskell知之甚少。

1 个答案:

答案 0 :(得分:0)

您肯定会在互联网上找到大量的硬币找零问题的递归实现,但几乎没有一种解决方案是@tailrec

如果你研究the above-referenced discussions怎么做@tailrec,例如Fibonacci sequencePascal Triangle 问题,你可能会注意到他们通常将方法转向地面 —

<块引用>

不是使用较小的参数调用递归函数,而是 从最小的开始计算并通过累加递归向上 值,直到它们命中所需的参数

我们如何使用这种方法来解决硬币找零问题?

让我们试着想象我们如何累积可能的硬币总数循环遍历所有可能的选项......从最初的硬币组开始。它非常类似于解决方案树。假设可用的硬币面额为 1、5 和 7。

         root      
         /|\       
        / | \      
       /  |  \     
      /   |   \    
     /    |    \   
    1     5     7  
   /|\   /|\   /|\ 
  1 5 7 1 5 7 1 5 7

这里是算法的想法 — 对于树中的每个节点 — 产生另一个子树,将所有硬币面额作为子树,计算我们沿着分支前进的总和。

为简单起见 — 让我们回顾一下a Boolean variant of coin-change problem, where you just need to answer (True/False) whether it's possible to split the sum with available set of coin denominations

只要我们可以通过不同的分支达到相同的总和,我们就会用 distinct 剔除重复的总和。

  val coins = List(1, 5, 7)

  def canChange(sum: Int): Boolean = {

    @tailrec def canChangeInternal(possibleSums: List[Int]): Boolean = {
      val targetSums = possibleSums.distinct.filter(x => x <= sum)
      if (targetSums.isEmpty)
        false
      else if (targetSums.contains(sum))
        true
      else {
        val spawnedSums = targetSums.flatMap(x =>
          coins.map(coin => x + coin)
        )
        canChangeInternal(spawnedSums)
      }
    }

    canChangeInternal(coins)
  }

实际上,在上述解决方案中,可以对不同的值使用更有效的集合,例如 HashSet,如果这在实践中可能很重要。

为了从上面的布尔问题解决方案过渡到原始问题 — 实际上征集所有可能的拆分变体,您可能需要执行以下操作:

  1. 不仅要维护总和列表,还要维护元组列表,其中一个元组元素是总和,另一个是由硬币组成的列表,说明我们如何达到该总和。
  2. 我们绝对不需要对总和执行 distinct,因为可能通过不同的选项累积相同的总和。我们宁愿对实际路径进行区分,因为树枝可能只是表示相同元素的重新排序。出于路径存储和比较的目的,可以使用类似 SortedList 或类似的结构。
  3. 为了获得所有可能的选项,我们不会仅使用 contains 积极退出递归,而是需要 all 元素满足 == sum 条件。
  4. 最后(但并非最不重要) — @tailrec 函数的返回类型类似于列表列表(或类似的 — 取决于您要如何处理它)