我目前正在使用java中的动态编程算法,它使用经典的2D矩阵进行反向计算。我正在努力将它转换为像haskell这样的功能语言,但它证明了我的比赛。寻找有关解决方案可能的建议的任何建议:
int[][] dynamic = new int[size][size];
for (int outer = 0; outer < size; j++){
Arrays.fill(dynamic[outer], 0);
}
dynamic[0][0] = 1;
for (int a = 0; a < size; a++){
for (int b = a; b < size; b++){
if (a == 0){
dynamic[a+1][b+1] = dynamic[a][b] * 2;
dynamic[a][b+1] = dynamic[a][b] * -1;
} else{
dynamic[a+1][b+1] = dynamic[a+1][b+1] + (dynamic[a][b] * 3);
dynamic[a][b+1] = dynamic[a][b+1] + (dynamic[a][b] * 2);
}
}
}
到目前为止,我一直在寻找创建2D数组的不同选项,但没有运气。我猜是因为Haskell不使用可变状态所以它不是一个超级流行的选项。它可能只是我的java大脑,但我喜欢一些有功能背景的人,或许能给我一些关于如何思考或解决这种转换的指示?
答案 0 :(得分:4)
通常手动进行变异是Haskell的错误。它很尴尬,它重新引入了你通常不会在Haskell中获得的所有突变错误。通常你会让懒惰为你照顾好事。动态编程问题的结构非常简单,在Haskell中用懒惰来表达,但是你需要从一个不太模糊的代码规范开始。
要做的第一件事是反混淆到低效的递归表示。这对你的例子来说是一个彻头彻尾的痛苦 - 我希望你只是随机地输入或多或少的代码。
这是我得到的,用Haskell表达的,虽然我不能保证它是正确的:
present :: Int -> Int -> Int
present 0 0 = 1
present _ 0 = 0
present 0 b = negate (present 0 (b - 1))
present a b = future a b + 2 * present a (b - 1)
future :: Int -> Int -> Int
future 1 b = 2 * present 0 b
future a b = 3 * present (a - 1) (b - 1)
为了解决你的变异,我不得不使用相互递归函数,present
概念性地跟踪循环中当前行的状态,future
跟踪下一行的状态。请注意,第一个之后的行的未来情况并不完全反映您所编写的内容。我冒昧地修剪了dynamic[a+1][b+1] +
部分,因为dynamic[a+1][b+1]
总是与逻辑布局的方式一致为零。
通常你会使用递归解决方案开始动态编程,所以你不需要像我在这里那样进行逆向工程。此外,您通常会看到一些看起来不完全随机的操作。不过,我有点像多个函数之间的相互递归。它使下一部分更酷。
让我们研究一下ghci,只是为了大致了解它的表现。
*Main> :set +s
*Main> present 10 10
-39366
(0.00 secs, 801,616 bytes)
*Main> present 11 11
-118098
(0.00 secs, 1,530,632 bytes)
*Main> present 20 20
-2324522934
(1.07 secs, 746,657,288 bytes)
*Main> present 21 21
-6973568802
(2.22 secs, 1,493,244,064 bytes)
*Main> present 22 22
-20920706406
(4.14 secs, 2,986,417,752 bytes)
这开始看起来大致与预期的运行时指数增长相似。
这里的事情变得很酷。 Haskell中的值可以根据自身来定义。基本思想是设置与每个函数对应的常规盒装数组,其中每个单元格的值通过一个用数组查找替换函数调用的函数来表示。只要定义中没有任何循环,懒惰就会使它全部起作用。
以下是代码中的内容:
import Data.Array
import Data.Ix
memoPresent :: Int -> Int -> Int
memoPresent r c = pArray ! (r, c)
where
bounds = ((0, 0), (r, c))
pArray = listArray bounds (map (uncurry present) (range bounds))
present 0 0 = 1
present _ 0 = 0
present 0 b = negate (pArray ! (0, b - 1))
present a b = fArray ! (a, b) + 2 * pArray ! (a, b - 1)
fArray = listArray bounds (map (uncurry future) (range bounds))
future 1 b = 2 * pArray ! (0, b)
future a b = 3 * pArray ! (a - 1, b - 1)
如果您需要密切关注,您可能会注意到fArray
的第一行未定义。如果访问该行中的任何内容,则会收到错误,但如果您调用了future 0 whatever
,那么您最终会使用递归代码获得相同的情况。
让我们看看它的表现如何:
*Main> :set +s
*Main> memoPresent 10 10
-39366
(0.00 secs, 194,176 bytes)
*Main> memoPresent 11 11
-118098
(0.00 secs, 223,344 bytes)
*Main> memoPresent 20 20
-2324522934
(0.00 secs, 521,944 bytes)
*Main> memoPresent 21 21
-6973568802
(0.00 secs, 566,544 bytes)
*Main> memoPresent 22 22
-20920706406
(0.00 secs, 616,304 bytes)
*Main> memoPresent 100 100
1989748563691696842
(0.01 secs, 10,442,400 bytes)
*Main> memoPresent 200 200
7879235843265279210
(0.05 secs, 41,116,704 bytes)
*Main> memoPresent 1000 1000
-4135538464527847958
(1.26 secs, 1,064,652,488 bytes)
咦。我没想到它会突然开始得到肯定的答案。哦,那可能只是Int
下溢。 Integer
可能是更好的类型选择,但它也不能反映您的Java代码。
无论如何 - 如果你利用懒惰来优势,那么在Haskell中从递归形式到动态编程的转换非常简单。您只需确保在低效递归方面进行原始配方,而不是模糊但有效的循环变异。
答案 1 :(得分:1)
动态编程通常依赖于可变性来提高效率。但这在Haskell中完全可行。 array
和vector
包都提供可变数组数据结构。这些API与您在Java中可能习惯的略有不同,但通常像您这样的算法具有非常直接的翻译。例如,这是使用array
包和ST
monad的算法:
import Control.Monad
import Control.Monad.ST
import Data.Array.Unboxed
import Data.Array.MArray.Safe
import Data.Array.ST.Safe
foo :: Int -> UArray (Int,Int) Int
foo size = runSTUArray $ do
dynamic <- newArray ((0,size-1),(0,size-1)) 0
writeArray dynamic (0,0) 1
forM_ [0..size-1] $ \a -> do
forM_ [a..size-1] $ \b -> do
v <- readArray dynamic (a,b)
if a == 0 then do
writeArray dynamic (a+1,b+1) (v*2)
writeArray dynamic (a, b+1) (v*(-1))
else do
v1 <- readArray dynamic (a+1,b+1)
writeArray dynamic (a+1,b+1) (v1 + v * 3)
v2 <- readArray dynamic (a,b+1)
writeArray dynamic (a,b+1) (v2 + v * 2)
return dynamic