【问题标题】:Project euler 10 - [haskell] Why so inefficient?Project euler 10 - [haskell] 为什么效率这么低?
【发布时间】:2013-12-18 09:04:05
【问题描述】:

好的,所以我选择了使用 java 时中断的项目 euler,我遇到了问题 10。我现在使用 Haskell,我认为学习一些 Haskell 会很好,因为我还在非常初学者。

http://projecteuler.net/problem=10

我还在用 java 编码的朋友想出了一个非常直接的方法来实现eratosthenes 的筛子:

http://puu.sh/5zQoU.png

我尝试实现一个更好看(并且我认为会稍微更有效)的 Haskell 函数来查找所有最高 2,000,000 的素数。 我来到了这个非常优雅但显然效率极低的函数:

primeSieveV2 :: [Integer] -> [Integer]
primeSieveV2 [] = []
primeSieveV2 (x:xs) = x:primeSieveV2( (filter (\n -> ( mod n x ) /= 0) xs) )

现在我不确定为什么我的函数比他的慢得多(他声称他的作品在 5 毫秒内完成),如果我的任何东西应该更快,因为我只检查一次复合材料(当它们被从列表中删除时)找到),而他会尽可能多地检查它们。

有什么帮助吗?

【问题讨论】:

  • 埃拉托色尼筛? 5毫秒?对于所有高达 200 万的素数?对我来说听起来很可疑。如果您还想做其他事情,要让计算机计数那么快是半困难的。
  • 他在撒谎,除非他在 NSA 工作,但我对此表示怀疑,如果他是,他就不会从事 euler 项目
  • 如果您只需要不超过 2,000,000 的数字,请使用 Int 而不是 Integer。如果您关心速度,请使用rem 而不是mod
  • 你的不是筛子。这是蛮力划分数字。他使用布尔向量来实现您的筛子,返回实际数字的任意精度整数。结果甚至没有可比性。
  • 还有人不知道cs.hmc.edu/~oneill/papers/Sieve-JFP.pdf?在这一点上应该需要阅读..

标签: haskell primes sieve-of-eratosthenes


【解决方案1】:

要在 Haskell 中有效地实现筛子,您可能需要使用 Java 方式(即分配一个可变数组并对其进行修改)。

我喜欢这样生成素数:

primes = 2 : filter (isPrime primes) [3,5 ..]
  where isPrime (p:ps) x = p*p > x || x `rem` p /= 0 && isPrime ps x

然后你可以打印所有素数的总和

main = print $ sum $ takeWhile (< 2000000) primes

您可以通过添加类型签名primes :: [Int] 来加快速度。 但它也适用于Integer,并且也可以为您提供正确的总和(32 位 Int 不会)。

更多信息请参见The Genuine Sieve of Eratosthenes

【讨论】:

  • 在链接到一篇展示如何在没有可变数组的情况下非常有效地完成它的论文之前声称需要一个可变数组是很奇怪的。
  • 我一直在想,是否有证据证明 p p > x 始终存在? isPrime 依赖于这一点。假设最后的_computed_p 小于sqrt x。为了找到 p 使得 pp > x,我们可能需要计算 p >= x。然而,在这个算法中我们不会知道这一点,因为 (p:ps) 需要头部可用。真可惜。底部。
  • n 和 2n 之间实际上有一个素数,这是 Bertrand 的猜想,并在 1850 年被证明。
  • 我敢打赌,可变数组比我提供的链接更有效。
  • 仅供参考,如果你想公平对待 O'Neill 的技术,你应该使用一个好的实现。 hackage.haskell.org/package/NumberSieves-0.1.2/docs/… 中的那个似乎相当小心,尽管我怀疑将轮子表示为矢量可能比循环列表更有效。
【解决方案2】:

这里实际上没有筛子。在 Haskell 中,您可以将筛子写为

import Data.Vector.Unboxed hiding (forM_)
import Data.Vector.Unboxed.Mutable
import Control.Monad.ST (runST)
import Control.Monad (forM_, when)
import Prelude hiding (read)

sieve :: Int -> Vector Bool
sieve n = runST $ do
  vec <- new (n + 1) -- Create the mutable vector
  set vec True       -- Set all the elements to True
  forM_ [2..n] $ \ i -> do -- Loop for i from 2 to n
    val <- read vec i -- read the value at i
    when val $ -- if the value is true, set all it's multiples to false
      forM_ [2*i, 3*i .. n] $ \j -> write vec j False
  freeze vec -- return the immutable vector

main = print . ifoldl' summer 0 $ sieve 2000000
  where summer s i b = if b then i + s else s

这通过使用可变的未装箱向量来“作弊”,但速度非常快

$ ghc -O2 primes.hs
$ time ./primes
  142913828923
  real: 0.238 s

这比我对 augustss 解决方案的基准测试快了大约 5 倍。

【讨论】:

  • 如果你能对“奇怪”的行进行简短的解释,那就太棒了。 (主要是指 ST 和矢量的东西。)
  • @kqr 更新了一些文档。它非常接近 Java 代码,Haskell 是一种很棒的命令式语言
  • @jozefg 它与我的简单素数生成器相比如何?
  • @augustss 你的打卡时间是2.24 秒。所以它快了大约 5 倍,是吗?
  • 请注意,数字 0 和 1 实际上不是素数,因此为了使筛子完全正确,您还必须在返回向量之前将它们划掉。这也会给你所有素数的正确总和为 200 万,即 142913828922。
【解决方案3】:

您的代码的时间复杂度是 n2(在 n 个素数中产生)。运行产生超过前 10...20,000 个素数是不切实际的。

该代码的主要问题不是它使用了rem,而是它过早地启动了它的过滤器,因此创建了太多的过滤器。以下是您修复它的方法,通过 small 调整:

{-# LANGUAGE PatternGuards #-}
primes = 2 : sieve primes [3..] 

sieve (p:ps) xs | (h,t) <- span (< p*p) xs = h ++ sieve ps [x | x <- t, rem x p /= 0]
                                               -- sieve ps (filter (\x->rem x p/=0) t)
main = print $ sum $ takeWhile (< 100000) primes

这将时间复杂度提高了大约 n1/2(在 n 个生成的素数中),并大大加快了速度:它得到到 100,000 快​​ 75 倍。您的 28 秒 应该变为 ~0.4 秒。但是,您可能在 GHCi 中将其作为解释代码进行了测试,而不是编译。将其1) 标记为 :: [Int] 并使用 -O2 标志进行编译使其速度再提高约 40 倍,因此将是 ~ 0.01 秒。使用此代码到达 2,000,000 需要大约 90 倍的时间,预计运行时间会高达 ~ 1 秒

1)一定要在main中使用sum $ map (fromIntegral :: Int -&gt; Integer) $ takeWhile ...

另请参阅:http://en.wikipedia.org/wiki/Analysis_of_algorithms#Empirical_orders_of_growth

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-08-11
    • 2012-09-18
    • 2010-12-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多