簡體   English   中英

功能范式中的動態編程

[英]Dynamic programming in the functional paradigm

我正在查看 Project Euler 上的第三十一個問題,它問有多少種不同的方法可以使用任意數量的 1 便士、2 便士、5 便士、10 便士、20 便士、50 便士、1 英鎊(100 便士)和英鎊的硬幣賺取 2 英鎊2 (200p)。

有遞歸解決方案,例如 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)

盡管它運行得足夠快,但它的效率相對較低,調用f function 大約 560 萬次。

我在 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]);
}

這樣效率更高,並且僅通過內循環 1220 次。

雖然我顯然可以使用Array對象將其或多或少逐字翻譯成 Scala ,但是否有一種慣用的功能方法可以使用不可變數據結構來做到這一點,最好具有類似的簡潔性和性能?

在決定我可能只是以錯誤的方式接近它之前,我已經嘗試過嘗試遞歸更新List並陷入困境。

每當根據前一個元素計算數據列表的某些部分時,我都會想到Stream遞歸。 不幸的是,這種遞歸不會發生在方法定義或函數內部,所以我不得不將 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

lowerhigher的定義不是必需的——我可以很容易地將它們替換為stream take coinstream drop coin ,但我認為這樣更清晰(更有效)。

我對 Scala 知之甚少,無法對此進行具體評論,但將 DP 解決方案轉換為遞歸解決方案的典型方法是記憶化(使用http://en.wikipedia.org/wiki/Memoization )。 這基本上是為域的所有值緩存 function 的結果

我也發現了這個http://michid.wordpress.com/2009/02/23/function_mem/ 高溫高壓

函數式動態編程實際上可以在惰性語言中非常漂亮,例如 Haskell(Haskell wiki 上有一篇文章)。 這是該問題的動態規划解決方案:

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)

誠然,這使用 O( cn ) memory,其中c是硬幣的數量, n是目標(相對於 ZD52387880E1EA22817A72D375921'3819 的內存版本);( Z 要做到這一點,您必須使用一些技術來捕獲可變的 state (可能是STArray )。 但是,它們都在 O( cn ) 時間內運行。 這個想法是幾乎直接遞歸地編碼遞歸解決方案,但不是在go中遞歸,而是在數組中查找答案。 我們如何構造數組? 通過在每個索引上調用go 由於 Haskell 是惰性的,它只在被要求時才計算事物,因此動態編程所需的求值順序都是透明處理的。

並且由於 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)

Lazy class 對僅按需評估的值進行編碼,然后我們構建一個充滿它們的數組。 這兩種解決方案幾乎立即適用於 10000 的目標值,盡管 go 大得多,並且您將遇到 integer 溢出或(在 Z3012DCFF1477E1FEAAB81764587C9BBD 中至少)溢出。

好的,這是 Pavel Fatin 代碼的記憶版本。 我正在使用 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)

為了完整起見,這里是上面答案的一個輕微變體,它不使用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