【问题标题】:Dynamic Programming with Scala使用 Scala 进行动态编程
【发布时间】:2018-06-14 10:02:07
【问题描述】:

为了练习 Scala,我想用 Scala 解决简单的动态编程练习。对于一些练习,我已经编写了 Python 实现。大多数时候我使用 Python 将中间结果保存在一个数组中。

例如这个练习:

在给定价值和硬币列表的情况下,该算法会输出达到某个价值所需的最少硬币数量。 (假设有无限数量的硬币。)

def automat(n, coins = [1,2,5,10,20,50,100,200]):
    A = [0]
    for i in range(1, n + 1):
        smallest_change = min([A[i - coin] for coin in coins if 0 <= i - coin])
        A.append(smallest_change + 1)
    return A[n]

在 Python 中看起来非常简单。不过,我在 Scala 中编写相同的代码时遇到问题。由于函数范式,我不想使用 for 循环,而是使用尾递归算法来解决该问题。我找不到任何好的解决方案。也许尾递归算法不是正确的方法,并且有一种不同的 scala 方法来实现这个简单的算法。

我已经在寻找动态编程练习的 scala 实现,但还没有找到好的来源。有人知道实现此功能的 scala 方式是什么样的吗?

更新:

到目前为止,我最接近的方法与 Dimas 尾递归方法非常相似。看起来是这样的

def automat(n: Int, coins: List[Int] = List(1, 2, 5, 10, 20, 50, 100, 200)): Int = {
    inner(n, n + 1, coins, List(0)).last
}

def inner(n: Int, m: Int, coins: List[Int], result: List[Int]): List[Int] = n match {
    case 0 => result
    case _ => inner(n-1, m, coins, result ::: List(coins.collect { case coin if (0 <= m - n - coin) => result(m - n - coin) }.min + 1))
}

【问题讨论】:

    标签: python algorithm scala dynamic-programming


    【解决方案1】:

    你可以这样做:

     def automat(n: Int, coins: List[Int]): Int = (n, coins) match {
             case (0, _) => 1
             case (m, _) if m < 0 => 0
             case (_, cs)  if cs.isEmpty => 0
             case (m, cs) => automat(m - cs.head, cs) + automat(m, cs.tail) 
        }
    

    如果您对 Scala 感兴趣,Martin Odersky 的 Coursera class 是一个很好的起点。

    【讨论】:

    • 感谢您的回答。你有这种方法的尾递归版本吗?我已经完成了类似的解决方案,但是由于两个递归调用,这种递归呈指数增长。小改进:你可以写case (_, Nil) 而不是(_, cs) if cs.isEmpty
    • 我不知道(我不确定是否存在)一个简单的尾递归版本。除非你使用记忆。但这可能非常消耗内存。
    • Coursera 课程中是否解释了记忆,或者您有不同的记忆来源?该算法只需要记住 (0,..., n-1) 的所有解来计算 n 的结果,而不是一遍又一遍地计算相同的结果。
    • 我不记得在 Coursera 课程中看到过记忆化,但 wiki 页面在该主题上相当广泛。但不要忘记内存使用和 CPU 使用之间存在权衡。
    【解决方案2】:

    这是您的算法或多或少的字面翻译成惯用的scala。像这样的实现中的常见想法是用递归替换循环更新可变状态,将状态更新作为函数参数向下传递,并作为返回值向上传递:

       def automat(n: Int, coins: Seq[Int] = Seq(1,2,5,10,20,50,100,200)): Array[Int] = n match {
         case 0 => Array(0)
         case n => 
           val out = automat(n-1, coins)
           val num = coins
             .collect { case coin if coin <= n  =>  out(n - coin) } 
             .min + 1
           out :+ num
       }
    

    更好的方法是反转递归的方向,这样算法就可以实现为 tail-recursive 函数,从而消除了非常大的n 堆栈溢出的可能性:

      @tailrec
      def automat2(
        max: Int, 
        coins: Seq[Int] = Seq(1,2,5,10,20,50,100,200), 
        n: Int = 1, 
        out: Array[Int] = Array(0)
      ): Array[Int] = n match {
        case n if n > max => out
        case n => 
          val num = coins
            .collect { case coin if coin <= n  =>  out(n - coin) } 
            .min + 1
          automat2(max, coins, n+1, out :+ num)
      }
    

    当然,如果你不想坚持你一直在使用的特定算法,而是愿意选择另一种方法,那么你可以有一个更简单的解决方案,如下所示:

    @tailrec
    def automat3(n: Int, coins: List[Int] = List(200, 100, 50, 20, 10, 5, 2, 1), result: Int = 0): Int = (n, coins) match { 
       case (0, _) => result
       case (n, Nil) => 0
       case (n, head::tail) if head > n => automat3(n, tail, result)
       case (n, head::_) => automat3(n - head, coins, result+1)
    }  
    

    【讨论】:

    • 感谢您的回答。我将仔细研究第二种尾递归方法。我希望有这样的事情!我认为第三种算法不太正确,它看起来像一个贪心算法,总是减去最大的硬币值。这可能适用于默认硬币,但有些例子会失败。
    • @Lando-L 你对贪心算法是对的。我想知道它工作所需的一组硬币的条件是什么。有什么想法吗?
    • 不,不幸的是,我不知道贪心算法适用于哪些硬币值。我只知道有贪心算法失败的例子。
    【解决方案3】:

    这是我记得为 Scala couse 做的一个版本

    def minCoins(amount: Int, coins: Seq[Int]): Int =
      if (amount < 0) 0
      else if (coins.isEmpty) if (amount == 0) 1 else 0
      else
        minCoins(amount, coins.tail) + minCoins(amount - coins.head, coins)
    

    所以你基本上要做的是首先通过从给定硬币集合中减去数量并将给定硬币集合中其余硬币的组合连接起来来获得一些组合。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-11
      • 2021-11-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-01
      相关资源
      最近更新 更多