在Haskell中加速计算分区

6

我正在尝试解决欧拉问题78,它基本上要求找到第一个使划分函数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,看来我的解决方案足够实用。 - gasche
4个回答

4

xs !! n的时间复杂度是线性的。你应该尝试使用对数或常数访问的数据结构。

编辑:这里是我通过复制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 for循环快8倍。结合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 对于大型的 Euler 问题来说速度不够快。你需要使用 'ghc -O2' 进行编译。 - Chris Kuklewicz

2

好的,一个观察是,由于您只对map (`mod` 10^6) ps感兴趣,因此您可能可以避免对大数进行计算。


那是个不错的技巧,但事实证明这个特殊的挑战可以在不使用此优化的情况下解决。 - Chris Kuklewicz
3
没问题,但这适用于许多面向PE(编程竞赛)的问题。我最喜欢的技巧之一是定义一个带有Num实例的新类型,用于进行模算术运算。这样我就可以重复使用许多现有代码。 - hammar
1
@Chris Kuklewicz 我使用我的数组实现进行了测试,模块化技巧将在我的机器上将计算“正确的n”的p(n)的速度从40秒加速到15秒。虽然不是根本性的,但仍然相当不错。 - gasche

1

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

当我看到这个:

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

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

现在看起来...先进行求和,然后再相乘会更快,而不是为每个元素执行乘法(在getP内部),然后再求和?

通常情况下,我会仔细研究并提供可行的代码;但这是一个欧拉问题(不想剧透!),所以这里只停留在这几个思考上。


你如何先求和再相乘呢?这两个操作是不同的。ax+by ≠ (a+b)(x+y)……除非你在想我没理解的其他事情? - ShreevatsaR
哦,你说得对;由于某种原因,我一直认为 sign 对它们所有的值都是相同的,所以 cx+cy = c(x+y),但实际上并不是这样。我的错误。使用 idnegate 可能比分别使用 *1*-1 更快,尽管这只是一个微小的加速。 - Dan Burton

1
受到您的问题启发,我已经使用了您的方法来用Haskell解决Euler 78问题。因此,我可以给您一些性能提示。
您缓存的五边形数列表应该很好。
选择一个大的数字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秒。

1
使用取模1000000的技巧,运行时间降至4.4秒。 - Chris Kuklewicz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接