我正在关注Project Euler上的Problem thirty one,问问:有多少不同的方法可以使用任意数量的1p,2p,5p,10p,20p,50p,1英镑的硬币赚2英镑(100p)和£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
函数调用了大约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
。
答案 0 :(得分:20)
每当根据前一个元素计算数据列表的某些部分时,我会想到Stream
递归。不幸的是,这样的递归不可能在方法定义或函数中发生,所以我不得不将一个函数转换为一个类来使其工作。
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
lower
和higher
的定义不是必需的 - 我可以轻松地将其替换为stream take coin
和stream drop coin
,但我认为它更清晰一点(以及更多)这种方式。
答案 1 :(得分:16)
我不太了解Scala对此有何具体评论,但将DP解决方案转换为递归方法的典型方法是memoization(使用http://en.wikipedia.org/wiki/Memoization)。这基本上是为域
的所有值缓存函数的结果我也发现了http://michid.wordpress.com/2009/02/23/function_mem/。 HTH
答案 2 :(得分:11)
在惰性语言中,功能性动态编程实际上非常漂亮,例如Haskell(Haskell wiki上有an article on it)。这是解决问题的动态编程解决方案:
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 )内存,其中 c 是硬币数量, n 是目标(相对于Java版本的O( n )内存);为此,你必须使用一些捕获可变状态的技术(可能是STArray
)。但是,它们都在O( cn )时间运行。我们的想法是直接递归地对递归解决方案几乎进行编码,但不是在 go 中递归,而是在数组中查找答案。我们如何构建数组?通过在每个索引上调用 go 。由于Haskell是懒惰的,它只会在被要求时计算,因此动态编程所需的评估顺序都是透明处理的。
感谢Scala的名字参数和lazy val
,我们可以在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
类对仅按需评估的值进行编码,然后我们构建一个完整的数组。这两个解决方案实际上只能立即达到目标值10000,虽然要大得多,但你会遇到整数溢出或(至少在Scala中)堆栈溢出。
答案 3 :(得分:7)
好的,这是Pavel Fatin代码的备忘版本。我正在使用Scalaz memoization东西,尽管编写自己的memoization类非常简单。
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)
答案 4 :(得分:4)
为了完整起见,以上是上述答案的略微变体,不使用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 })
}
}