在Haskell中使用动态规划?[警告:ProjectEuler 31溶液内部]

在Haskell中使用动态规划?[警告:ProjectEuler 31溶液内部],haskell,ocaml,dynamic-programming,Haskell,Ocaml,Dynamic Programming,在解决projecteuler.net的问题#31[前面的破坏者(计算用英国硬币赚2英镑的方法的数量)时,我想使用动态规划。我从OCaml开始,编写了以下简短而高效的编程: open Num let make_dyn_table amount coins = let t = Array.make_matrix (Array.length coins) (amount+1) (Int 1) in for i = 1 to (Array.length t) - 1 do for j

在解决projecteuler.net的问题#31[前面的破坏者(计算用英国硬币赚2英镑的方法的数量)时,我想使用动态规划。我从OCaml开始,编写了以下简短而高效的编程:

open Num

let make_dyn_table amount coins =
  let t = Array.make_matrix (Array.length coins) (amount+1) (Int 1) in
  for i = 1 to (Array.length t) - 1 do
    for j = 0 to amount do
      if j < coins.(i) then
        t.(i).(j) <- t.(i-1).(j)
      else
        t.(i).(j) <- t.(i-1).(j) +/ t.(i).(j - coins.(i))
    done
  done;
  t

let _ =
  let t = make_dyn_table 200 [|1;2;5;10;20;50;100;200|] in
  let last_row = Array.length t - 1 in
  let last_col = Array.length t.(last_row) - 1 in
  Printf.printf "%s\n" (string_of_num (t.(last_row).(last_col)))
opennum
让我们把硬币的数量定在表上=
设t=Array.make_矩阵(Array.length硬币)(金额+1)(整数1)in
对于i=1到(Array.length t)-1 do
对于j=0的情况,则为
如果jt、 哈斯克尔是纯洁的。纯度意味着值是不可变的,因此在步骤中

j
为更新的每个条目创建一个完整的新数组。这对于像2英镑这样的小额交易来说已经非常昂贵了,但对于100英镑的交易来说却变得非常猥亵

此外,数组是装箱的,这意味着它们包含指向条目的指针,这会恶化局部性,使用更多的存储,并允许创建thunk,而当它们最终被强制执行时,计算速度也较慢

所用算法的效率取决于可变数据结构,但可变性仅限于计算,因此我们可以使用旨在允许使用临时可变数据的安全屏蔽计算、
ST
state transformer monad系列以及相关的[unbox,for efficiency]数组

给我半个小时左右的时间,用STUArray将算法翻译成代码,你会得到一个Haskell版本,它不太难看,应该与O'Caml版本的性能相当(我不知道它的差异是大于还是小于1,或多或少都是常数)

这是:

主模块(Main),其中
导入System.Environment(getArgs)
导入Data.Array.ST
进口管制站
导入Data.Array.unbox
标准硬币::[Int]
标准硬币=[1,2,5,10,20,50100200]
变更组合::Int->[Int]->Int
更改组合金额硬币=runST$do
let coinBound=硬币长度-1
coinsArray::UArray Int
coinsArray=列表数组(0,硬币绑定)硬币
table coinBound=readArray table(coinBound,amount)
|j>金额=go(i+1)0
|jInt
---
>变更组合::Int->[Int]->Integer
17c17

只需要调整两种类型的签名-数组必须被装箱,因此我们需要确保没有在第28行中向数组写入thunk,并且

$time./mutArrIgr
73682
实0.002s
用户0.000s
系统0m0.002s
$time./mutArrIgr 1000000
99341140660285639188927260001
真正的0m1.314s
用户0m1.157s
系统0m0.156s
对于
Int
s溢出的大结果,计算时间明显更长,但正如预期的那样,与O'Caml相当


花一些时间理解奥卡姆,我可以提供一个更接近、更短、甚至可以说更好的翻译:

主模块(Main),其中
导入System.Environment(getArgs)
导入Data.Array.ST
进口管制站
导入Data.Array.unbox
导入控制.Monad(表单)
标准硬币::[Int]
标准硬币=[1,2,5,10,20,50100200]
变更组合::Int->[Int]->Integer
更改组合金额硬币=runST$do
let coinBound=硬币长度-1
coinsArray::UArray Int
coinsArray=列表数组(0,硬币绑定)硬币
桌子
表格[0..amount]$\j->
如果jv您可以利用Haskell的懒惰,而不是自己安排数组填充,而是依靠懒惰求值以正确的顺序完成。(对于较大的输入,需要增加堆栈大小。)

导入数据。数组
createDynTable::Integer->Array Int Integer->Array(Int,Integer)Integer
可创造的金额硬币=
设numCoins=(snd.bounds)硬币
t=数组((0,0),(numCoins,amount))

[((i,j),go i j)|我没有忘记更新,我发现O'Caml版本使用任意精度的整数进行计算,因此我们不希望Haskell避免这种情况。感谢使用大整数的更新。至于状态单子,我理解正确吗?在引擎盖下,数组实际上正在修改,但这种变异的影响无法观察到?是的,数组在适当的位置被修改了,但是虽然它是可变的,但不能从计算外部访问它。仔细看了一下算法(实际考虑它,而不是盲目地转换),我对O'Caml有一个更好更简短的翻译,现在正在更新。@gnuvince:注意,ST monad与State monad不同,尽管它们有类似的用途。ST支持真正的易变性,State只是关于线程参数。FWIW,如果我在Haskell中从头开始写这篇文章,我会做得更近一些根据@augustss的答案,而不是Daniel的答案。仅使用列表和右折叠可以更有效地完成此操作。请参阅
import Data.Array

createDynTable :: Integer -> Array Int Integer -> Array (Int, Integer) Integer
createDynTable amount coins =
    let numCoins = (snd . bounds) coins
        t = array ((0, 0), (numCoins, amount))
            [((i, j), go i j) | i <- [0 .. numCoins], j <- [0 .. amount]]
        go i j | i == 0        = 1
               | j < coins ! i = t ! (i-1, j)
               | otherwise     = t ! (i-1, j) + t ! (i, j - coins!i)
    in t


changeCombinations amount coins =
    let coinsArray = listArray (0, length coins - 1) coins
        dynTable = createDynTable amount coinsArray
        ((_, _), (i, j)) = bounds dynTable
    in
       dynTable ! (i, j)

main =
    print $ changeCombinations 200 [1,2,5,10,20,50,100,200]