【问题标题】:Speed up calculation of partitions in Haskell加速 Haskell 中的分区计算
【发布时间】:2011-04-25 14:34:51
【问题描述】:

我正在尝试解决欧拉问题 78,它基本上要求partition function p(n) 可以被 1000000 整除的第一个数字。

我使用基于五边形数的欧拉递归公式(在pents 中计算,加上正确的符号)。这是我的代码:

ps = 1 : map p [1..] where
  p n = sum $ map getP $ takeWhile ((<= n).fst) pents where
    getP (pent,sign) = sign * (ps !! (n-pent)) 

pents = zip (map (\n -> (3*n-1)*n `div` 2) $ [1..] >>= (\x -> [x,-x]))
            (cycle [1,1,-1,-1])

虽然ps 似乎产生了正确的结果,但它太慢了。有没有办法加快计算速度,还是我需要一种完全不同的方法?

【问题讨论】:

  • 有了 hammar 的想法和 Chris Kuklewicz 的建议不要使用 ghci,看来我的解决方案已经足够实用了。

标签: performance haskell math number-theory


【解决方案1】:

xs !! n 具有线性复杂度。您应该尝试使用对数或常量访问数据结构。

编辑:这是我通过复制 a similar one by augustss 提出的一个快速实现:

psOpt x = psArr x
  where psCall 0 = 1
        psCall n = sum $ map getP $ takeWhile ((<= n).fst) pents where
          getP (pent,sign) = sign * (psArr (n-pent))
        psArr n = if n > ncache then psCall n else psCache ! n
        psCache = listArray (0,ncache) $ map psCall [0..ncache]

在 ghci 中,我观察到您的列表版本没有明显的加速。不走运!

编辑: 事实上,使用 Chris Kuklewicz 建议的 -O2,这个解决方案比 n=5000 快八倍。结合 Hammar 对模 10^6 求和的见解,我得到了一个足够快的解决方案(在我的机器上大约 10 秒内找到希望正确的答案):

import Data.List (find)
import Data.Array 

ps = 1 : map p [1..] where
  p n = sum $ map getP $ takeWhile ((<= n).fst) pents where
    getP (pent,sign) = sign * (ps !! (n-pent)) 

summod li = foldl (\a b -> (a + b) `mod` 10^6) 0 li

ps' = 1 : map p [1..] where
  p n = summod $ map getP $ takeWhile ((<= n).fst) pents where
    getP (pent,sign) = sign * (ps !! (n-pent)) 

ncache = 1000000

psCall 0 = 1
psCall n = summod $ map getP $ takeWhile ((<= n).fst) pents
  where getP (pent,sign) = sign * (psArr (n-pent))
psArr n = if n > ncache then psCall n else psCache ! n
psCache = listArray (0,ncache) $ map psCall [0..ncache]

pents = zip (map (\n -> ((3*n-1)*n `div` 2) `mod` 10^6) $ [1..] >>= (\x -> [x,-x]))
            (cycle [1,1,-1,-1])

(我破坏了 psCache 抽象,所以你应该使用psArr 而不是psOpt;这确保了对psArr 的不同调用将重用相同的记忆数组。这在你编写find ((== 0) . ...) 时很有用。 . 好吧,我认为最好不要发布完整的解决方案。)

感谢大家的额外建议。

【讨论】:

  • ghci 对于大的欧拉问题不够快。您需要使用 'ghc -O2' 进行实际编译
【解决方案2】:

嗯,一个观察结果是,由于您只对map (`mod` 10^6) ps 感兴趣,您也许可以避免对大量数字进行计算。

【讨论】:

  • 这是一个很好的技巧,但事实证明,这个特殊的挑战可以在没有这种优化的情况下解决。
  • 是的,但它适用于很多 PE 问题。我最喜欢的一个技巧是使用 Num 实例定义一个新类型,该实例进行模运算。这样我就可以重用很多现有的代码。
  • @Chris Kuklewicz 我用我的 Array 实现进行了测试,模块化技巧在我的机器上将“右 n”的 p(n) 计算速度从 40 秒缩短到 15 秒。不是基本的,但仍然很不错。
【解决方案3】:

我没有做过那个欧拉问题,但通常对于欧拉问题,有一个聪明的技巧可以加快计算速度。

当我看到这个时:

sum $ map getP $ takeWhile ((<=n).fst) pents

我不禁想到,一定有比每次计算 ps 的元素时调用 sum . map getP 更聪明的方法。

现在我看...不是先执行求和然后相乘,而不是对每个元素执行相乘(在getP 内)然后然后 em> 求和?

通常我会更深入地研究并提供工作代码;但这是一个欧拉问题(不想剧透它!),所以我将在此停止思考。

【讨论】:

  • 如何先求和再乘?这是不一样的。 ax+by ≠ (a+b)(x+y)……除非你在想别的我不明白的东西?
  • 哦,你是对的;出于某种原因,我认为sign 对所有这些都是相同的,因此 cx+cy = c(x+y),但它不一样。我的错。分别使用 idnegate 而不是 *1*-1 可能会更快,尽管这充其量只是一个小小的加速。
【解决方案4】:

受您的问题启发,我使用您的方法用 Haskell 求解 Euler 78。所以我可以给你一些性能提示。

您的 pent 缓存列表应该是好的。

选择一些大数 maxN 来限制您对 (p n) 的搜索。

计划是使用一个(Array Int64 Integer)来记忆(p n)的结果,下界为0,上界为maxN。这就需要用'p'来定义数组,用数组来定义'p',它们是相互递归定义的:

将 (p n) 重新定义为 (pArray n) 以查找对数组 A 中“p”的递归调用。

使用新的 pArray 和 Data.Array.IArray.listArray 创建数组 A。

一定要使用 'ghc -O2' 编译。这在此处运行 13 秒。

【讨论】:

  • 使用模数 1000000 的技巧将运行时间降低到 4.4 秒。
猜你喜欢
  • 2013-09-14
  • 1970-01-01
  • 2021-01-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多