简体   繁体   English

功能范式中的动态编程

[英]Dynamic programming in the functional paradigm

I'm looking at Problem thirty one on Project Euler, which asks, how many different ways are there of making £2 using any number of coins of 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p) and £2 (200p).我正在查看 Project Euler 上的第三十一个问题,它问有多少种不同的方法可以使用任意数量的 1 便士、2 便士、5 便士、10 便士、20 便士、50 便士、1 英镑(100 便士)和英镑的硬币赚取 2 英镑2 (200p)。

There are recursive solutions, such as this one in Scala (credit to Pavel Fatin)有递归解决方案,例如 Scala 中的这个(归功于 Pavel Fatin)

def f(ms: List[Int], n: Int): Int = ms match {
  case h :: t =>
    if (h > n) 0 else if (n == h) 1 else f(ms, n - h) + f(t, n)
  case _ => 0
} 
val r = f(List(1, 2, 5, 10, 20, 50, 100, 200), 200)

and although it runs fast enough, it's relatively inefficient, calling the f function around 5.6 million times.尽管它运行得足够快,但它的效率相对较低,调用f function 大约 560 万次。

I saw someone else's solution in Java which was programmed dynamically (credit to wizeman from Portugal)我在 Java 中看到了别人的解决方案,它是动态编程的(来自葡萄牙的 wizeman)

final static int TOTAL = 200;

public static void main(String[] args) {
    int[] coins = {1, 2, 5, 10, 20, 50, 100, 200};
    int[] ways = new int[TOTAL + 1];
    ways[0] = 1;

    for (int coin : coins) {
        for (int j = coin; j <= TOTAL; j++) {
            ways[j] += ways[j - coin];
        }
    }

    System.out.println("Result: " + ways[TOTAL]);
}

This is much more efficient and passes the inner loop only 1220 times.这样效率更高,并且仅通过内循环 1220 次。

While I could obviously translate this more or less verbatim into Scala using Array objects, is there an idiomatic functional way to do this using immutable data structures, preferably with similar conciseness and performance?虽然我显然可以使用Array对象将其或多或少逐字翻译成 Scala ,但是否有一种惯用的功能方法可以使用不可变数据结构来做到这一点,最好具有类似的简洁性和性能?

I have tried and become stuck trying to recursively update a List before deciding I'm probably just approaching it the wrong way.在决定我可能只是以错误的方式接近它之前,我已经尝试过尝试递归更新List并陷入困境。

Whenever some part of a list of data is computed based on a previous element, I think of Stream recursion.每当根据前一个元素计算数据列表的某些部分时,我都会想到Stream递归。 Unfortunately, such recursion cannot happen inside method definitions or functions, so I had to turn a function into a class to make it work.不幸的是,这种递归不会发生在方法定义或函数内部,所以我不得不将 function 转换为 class 才能使其工作。

class IterationForCoin(stream: Stream[Int], coin: Int) {
  val (lower, higher) = stream splitAt coin
  val next: Stream[Int] = lower #::: (higher zip next map { case (a, b) => a + b })
}
val coins = List(1, 2, 5, 10, 20, 50, 100, 200)
val result = coins.foldLeft(1 #:: Stream.fill(200)(0)) { (stream, coin) =>
  new IterationForCoin(stream, coin).next
} last

The definitions of lower and higher are not necessary -- I could easily replace them with stream take coin and stream drop coin , but I think it's a little clearer (and more efficient) this way. lowerhigher的定义不是必需的——我可以很容易地将它们替换为stream take coinstream drop coin ,但我认为这样更清晰(更有效)。

I don't know enough about Scala to comment specifically on that, but the typical way to translation a DP solution in to a recursive one is to memoization (use http://en.wikipedia.org/wiki/Memoization ).我对 Scala 知之甚少,无法对此进行具体评论,但将 DP 解决方案转换为递归解决方案的典型方法是记忆化(使用http://en.wikipedia.org/wiki/Memoization )。 This is basically caching the result of your function for all values of the domain这基本上是为域的所有值缓存 function 的结果

I found this as well http://michid.wordpress.com/2009/02/23/function_mem/ .我也发现了这个http://michid.wordpress.com/2009/02/23/function_mem/ HTH高温高压

Functional dynamic programming can actually be really beautiful in a lazy language, such as Haskell (there's an article on it on the Haskell wiki).函数式动态编程实际上可以在惰性语言中非常漂亮,例如 Haskell(Haskell wiki 上有一篇文章)。 This is a dynamic programming solution to the problem:这是该问题的动态规划解决方案:

import Data.Array

makeChange :: [Int] -> Int -> Int
makeChange coinsList target = arr ! (0,target)
  where numCoins = length coinsList
        coins    = listArray (0,numCoins-1) coinsList
        bounds   = ((0,0),(numCoins,target))
        arr      = listArray bounds . map (uncurry go) $ range bounds
        go i n   | i == numCoins = 0
                 | otherwise     = let c = coins ! i
                                   in case c `compare` n of
                                        GT -> 0
                                        EQ -> 1
                                        LT -> (arr ! (i, n-c)) + (arr ! (i+1,n))

main :: IO ()
main = putStrLn $  "Project Euler Problem 31: "
                ++ show (makeChange [1, 2, 5, 10, 20, 50, 100, 200] 200)

Admittedly, this uses O( cn ) memory, where c is the number of coins and n is the target (as opposed to the Java version's O( n ) memory);诚然,这使用 O( cn ) memory,其中c是硬币的数量, n是目标(相对于 ZD52387880E1EA22817A72D375921'3819 的内存版本);( Z to get that, you'd have to use some technique of capturing mutable state (probably an STArray ).要做到这一点,您必须使用一些技术来捕获可变的 state (可能是STArray )。 However, they both run in O( cn ) time.但是,它们都在 O( cn ) 时间内运行。 The idea is to encode the recursive solution almost directly recursively, but instead of recursing within go , we look up the answer in the array.这个想法是几乎直接递归地编码递归解决方案,但不是在go中递归,而是在数组中查找答案。 And how do we construct the array?我们如何构造数组? By calling go on every index.通过在每个索引上调用go Since Haskell is lazy, it only computes things when asked to, so the order-of-evaluation stuff necessary for dynamic programming is all handled transparently.由于 Haskell 是惰性的,它只在被要求时才计算事物,因此动态编程所需的求值顺序都是透明处理的。

And thanks to Scala's by-name parameters and lazy val s, we can mimic this solution in Scala:并且由于 Scala 的别名参数和lazy val s,我们可以在 Scala 中模仿这个解决方案:

class Lazy[A](x: => A) {
  lazy val value = x
}

object Lazy {
  def apply[A](x: => A) = new Lazy(x)
  implicit def fromLazy[A](z: Lazy[A]): A = z.value
  implicit def toLazy[A](x: => A): Lazy[A] = Lazy(x)
}

import Lazy._

def makeChange(coins: Array[Int], target: Int): Int = {
  val numCoins = coins.length
  lazy val arr: Array[Array[Lazy[Int]]]
    = Array.tabulate(numCoins+1,target+1) { (i,n) =>
        if (i == numCoins) {
          0
        } else {
          val c = coins(i)
          if (c > n)
            0
          else if (c == n)
            1
          else
            arr(i)(n-c) + arr(i+1)(n)
        }
      }
  arr(0)(target)
}

// makeChange(Array(1, 2, 5, 10, 20, 50, 100, 200), 200)

The Lazy class encodes values which are only evaluated on demand, and then we build an array full of them. Lazy class 对仅按需评估的值进行编码,然后我们构建一个充满它们的数组。 Both of these solutions work for a target value of 10000 practically instantly, although go much larger and you'll run into either integer overflow or (in Scala, at least) a stack overflow.这两种解决方案几乎立即适用于 10000 的目标值,尽管 go 大得多,并且您将遇到 integer 溢出或(在 Z3012DCFF1477E1FEAAB81764587C9BBD 中至少)溢出。

Ok, here's the memoized version of Pavel Fatin's code.好的,这是 Pavel Fatin 代码的记忆版本。 I'm using Scalaz memoization stuff, though it's really simple to write your own memoization class.我正在使用 Scalaz memoization 的东西,尽管编写自己的 memoization class 真的很简单。

import scalaz._
import Scalaz._

val memo = immutableHashMapMemo[(List[Int], Int), Int]
def f(ms: List[Int], n: Int): Int = ms match {
  case h :: t =>
    if (h > n) 0 else if (n == h) 1 else memo((f _).tupled)(ms, n - h) + memo((f _).tupled)(t, n)
  case _ => 0
} 
val r = f(List(1, 2, 5, 10, 20, 50, 100, 200), 200)

For the sake of completeness, here is a slight variant of the answer above that doesn't use Stream :为了完整起见,这里是上面答案的一个轻微变体,它不使用Stream

object coins {
  val coins = List(1, 2, 5, 10, 20, 50, 100, 200)
  val total = 200
  val result = coins.foldLeft(1 :: List.fill(total)(0)) { (list, coin) =>
    new IterationForCoin(list, coin).next(total)
  } last
}

class IterationForCoin(list: List[Int], coin: Int) {
  val (lower, higher) = list splitAt coin
  def next (total: Int): List[Int] = {
    val listPart = if (total>coin) next(total-coin) else lower
    lower ::: (higher zip listPart map { case (a, b) => a + b })
  }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM