【问题标题】:My solution for Euler Project #3 is too slow我对 Euler Project #3 的解决方案太慢了
【发布时间】:2013-07-30 12:15:50
【问题描述】:

我是 Haskell 的新手,正在修补 Euler Project 的问题。我对问题 #3 的解决方案太慢了。一开始我试过这个:

-- Problem 3
-- The prime factors of 13195 are 5, 7, 13 and 29.
-- What is the largest prime factor of the number 600851475143 ?

problem3 = max [ x | x <- [1..n], (mod n x) == 0, n /= x]
    where n = 600851475143

然后我将其更改为返回所有 x 而不仅仅是最大的。

problem3 = [ x | x <- [1..n], (mod n x) == 0, n /= x]
        where n = 600851475143

30分钟后,列表仍在处理中,输出如下所示

[1,71,839,1471,6857,59569,104441,486847,1234169,5753023,10086647,87625999,408464633,716151937

为什么这么慢?我是在做一些非常错误的事情还是这种任务是正常的?

【问题讨论】:

  • 有趣的事实:您正在接触大约 6000 亿个数字。即使每个数字占用一个 CPU 时钟周期(这完全不现实),在广泛使用的最高时钟频率的 CPU 上仍然需要几十秒。所以是的,这种方法是不可行的。

标签: haskell primes prime-factoring


【解决方案1】:

一个数字的完整因式分解对于大数字可能需要很长时间。对于 Project Euler 问题,暴力解决方案(这就是)通常不足以在您的一生中找到答案。

提示:您不需要找到所有个素因数,只需找到最大的一个。

【讨论】:

  • 我明白了。回到绘图板。
【解决方案2】:

它做了很多工作! (它也会给你错误的答案,但这是一个单独的问题!)

有一些非常快速的方法可以让您先考虑一下问题来加快速度:

  • 您将函数应用于所有数字1..n,并检查每个数字以确保它不是n。相反,您可以检查所有数字 1..n-1 并跳过 n 不同的检查(尽管它们很小)。
  • 答案是奇数,因此您可以通过从1..(n-1)/2 转到2x 而不是x 来快速过滤掉任何偶数。
  • 如果你仔细想想,所有因素都是成对出现的,所以实际上你可以从1..sqrt(n)(如果你忽略偶数,则从1..sqrt(n)/2)搜索并在每个步骤中输出成对的数字。

与这个函数的性能无关,但值得注意的是,你在这里实现的会找到一个数的所有因数,而你想要的只是最大的质因数。所以要么你必须测试你的每个除数的素数(这又会很慢),或者你可以一步实现这两个。您可能想了解“筛子”,最简单的是埃拉托色尼筛子,以及如何实现它们。

【讨论】:

    【解决方案3】:

    使用您的解决方案,大约有 6000 亿个可能的数字。正如 delnan 所指出的,让每个数字的检查速度更快不会产生太大的影响,我们必须限制候选人的数量。

    您的解决方案似乎也不正确。 59569 = 71 * 839 不是吗?问题 只要求素因数。请注意 71 和 839 在您的列表中,因此您是 做正确的事。事实上,您正在尝试找出所有因素。

    我认为最显着的效果是在继续之前将因素分开。

    euler3 = go 2 600851475143
      where
        go cand num
          | cand == num           = [num]
          | cand `isFactorOf` num = cand : go cand       (num `div` cand)
          | otherwise             =        go (cand + 1) num
    
    isFactorOf a b = b `mod` a == 0
    

    这似乎是一个明显的优化,但它依赖于这样一个事实,即如果a 和b 都可以除以c 并且a 与b 互质,那么a 可以除以c/b。

    如果你想做更多,常见的“只检查直到平方根”技巧已经 这里提到。同样的技巧可以应用于这个问题,但不幸的是,在这个实例上没有显示性能提升:

    euler3 = go 2 600851475143
      where
        go cand num
          | cand*cand > num       = [num]
          | cand `isFactorOf` num = cand : go cand       (num `div` cand)
          | otherwise             =        go (cand + 1) num
    
    isFactorOf a b = b `mod` a == 0
    

    这里,当候选者大于剩余数的平方根(num)时,我们知道num 必须是质数,因此是原始数的质因数 号码(600851475143)。

    只考虑素数就可以删除更多的候选者, 但这稍微高级一些,因为您需要做出合理的表现 生成素数的方法。请参阅this page 了解执行此操作的方法。

    【讨论】:

    • 这是迄今为止最好的答案,因为它显着减少了运行时间。我建议在第一个保护条件下检查cand*cand &gt; num。这将实现常见的“仅检查平方根”技巧(实际上不取平方根)。您还可以跟踪要添加的奇数 (1 + 3 = 2^2, 1 + 3 + 5 = 3^2, 1 + 3 + 5 + 7 = 4^2) 以将平方从乘法减少到一些补充。
    • @Olathe,感谢您的建议。我添加了一段关于它的内容。不幸的是,这一实例的性能差异并不明显。
    • 让我们重构一下,euler3 n = go 2 n where ...。现在,尝试[ euler3 x | x&lt;-[600851475143..]],那里的性能提升(仅测试到 sqrt)怎么样?
    • @WillNess ,在该基准上,“仅检查到 sqrt”技巧至关重要。与分钟相比,差异不到一秒。
    • 对不起,改变了你的测试;它应该是take 7 [ euler3 x | x&lt;-[600851475143..]]。我认为应该强调这项技术的重要性。 --- 顺便说一句,如果我们只想要 一个 数的因式分解,找到要测试的素数是没有好处的。
    【解决方案4】:

    TL;DR:你做的两件事不是最优的,是:不停留在平方根,不划分每个最小的因素,因为它们找到了。


    这是the answer by HaskellElephant 中显示的(第二个)分解代码的一个小推导。我们从您的代码开始:

    f1 n = [ x | x <- [2..n], rem n x == 0]
    n3 = 600851475143
    
    Prelude> f1 n3
    [71,839,1471,6857,59569,104441,486847Interrupted.
    

    所以它不会在任何合理的时间内完成,并且它产生的一些数字不是 素数...但是不是在列表理解中添加素数检查,让我们注意71 是素数。 f1 n 产生的第一个数字是n 的最小除数,因此它是素数 .如果不是,我们会首先找到 它的最小除数 - 矛盾。

    所以,我们可以把它分出来,continue searching新约数的质因数:

    f2 n = tail $ iterate (\(_,m)-> (\f->(f, quot m f)) . head $ f1 m) (1,n) 
    
    Prelude> f2 n3
    [(71,8462696833),(839,10086647),(1471,6857),(6857,1),(*** Exception: Prelude.hea
    d: empty list
    

    (错误,因为f1 1 == [])。我们完成了! (6857 是答案,这里...)。让我们总结一下:

    takeUntil p xs = foldr (\x r -> if p x then [x] else x:r) [] xs
    pfactors1 n = map fst . takeUntil ((==1).snd) . f2 $ n   -- prime factors of n
    

    试用我们新开发的解决方案,

    Prelude> map pfactors1 [n3..]
    [[71,839,1471,6857],[2,2,2,3,3,1259Interrupted.
    

    突然间,我们在没有小除数的数字上遇到了新的低效率墙。但是如果n = a*b 和1 &lt; a &lt;= b,那么a*a &lt;= a*b == n 等就足以测试直到一个数字的平方根,找到它的最小除数。

    f12 n = [ x | x <- takeWhile ((<= n).(^2)) [2..n], rem n x == 0] ++ [n]
    f22 n = tail $ iterate (\(_,m)-> (\f->(f, quot m f)) . head $ f12 m) (1,n) 
    pfactors2 n = map fst . takeUntil ((==1).snd) . f22 $ n
    

    半小时内无法完成的事情现在在一秒钟内完成(在典型的高性能机器上):

    Prelude> f12 n3
    [71,839,1471,6857,59569,104441,486847,600851475143]
    

    sqrt n3 上面的所有除数根本不需要。我们无条件地将n 本身添加为f12 中的最后一个除数,以便它能够处理素数:

    Prelude> f12 (n3+6)
    [600851475149]
    

    从n3 / sqrt n3 = sqrt n3 ~= 775146 开始,您最初对f1 n3 的尝试应该需要大约一周 才能完成。这就是优化在平方根处停止的重要性。

    Prelude> f22 n3
    [(71,8462696833),(839,10086647),(1471,6857),(6857,1),(1,1),(1,1),(1,1),(1,1),(1,
    1),(1,1),(1,1),(1,1),(1,1),(1,1),(1,1),(1,1),(1,1),(1,1)Interrupted
    

    我们显然已经将 “Prelude.head: empty list” 错误换成了非终止 - 但生产 - 行为。


    最后,我们将f22 分成两部分,并将它们分别融合到其他函数中,以简化代码。此外,我们不会重新开始,就像 f12 所做的那样,一直在寻找 2 中的最小除数:

    -- smallest factor of n, starting from d. directly jump from sqrt n to n.
    smf (d,n) = head $ [ (x, quot n x) | x <- takeWhile ((<=n).(^2)) [d..]
                                       , rem n x == 0] ++ [(n,1)]
    
    pfactors n = map fst . takeUntil ((==1).snd) . tail . iterate smf $ (2,n)
    

    这通过higher-order functioniterate 表达受保护的(共同)递归,并且在功能上等同于上述代码。以下现在运行顺利,我们甚至可以在那里找到一对twin primes 作为奖励:

    Prelude Saga> 地图 pfactors [n3..] [[71,839,1471,6857],[2,2,2,3,3,1259,6628403],[5,120170295029],[2,13,37,227,27514 79],[3,7,7,11,163,2279657],[2,2,41,3663728507],[600851475149],[2,3,5, 5,19,31,680 0809],[600851475151],[2,2,2,2,37553217197],[3,3,3,211,105468049],[2,7,11161, 3845 351],[5,67,881,2035853],[2,2,3中断。

    【讨论】:

      【解决方案5】:

      这是我对 Euler Project #3 的解决方案。在我的 Macbook Air 上只需 1.22 秒。

      首先我们应该找到给定数字的所有因数。但我们知道,偶数不能是素数(2 号除外)。所以,要解决 Euler Project #3,我们不需要全部,而只需要奇怪的因素:

          getOddFactors num = [ x | x <- [3,5..num], num `rem` x == 0 ]
      

      但是我们可以优化这个功能。如果我们计划找到一个 num 大于 sqrt num 的因数,我们应该有另一个小于 sqrt num 的因数 - 这些可能我们已经发现的因素。因此,我们可以通过 sqrt num 限制我们的可能因素列表:

          getOddFactors num = [ x | x <- [3, 5..(floor.sqrt.fromIntegral) num], 
                                num `rem` x == 0 ]
      

      接下来我们想知道 num 的哪些奇数因数是素数:

          isPrime number = [ x | x <- [3..(floor.sqrt.fromIntegral) number], 
                             number `rem` x == 0] == []
      

      接下来我们可以用函数isPrime过滤num的奇数因子,找出num的所有素数。但是为了利用 Haskell 的惰性来优化我们的解决方案,我们将函数 filter isPrime 应用于 reversed 奇数因数列表。一旦我们的函数找到第一个质数值,Haskell 就会停止计算并返回解决方案:

          largestPrimeFactor = head . filter isPrime . reverse . getOddDivisors
      

      因此,解决方案是:

          ghci> largestPrimeFactor 600851475143
          6857
          (1.22 secs, 110646064 bytes)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-04-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-07-19
        相关资源
        最近更新 更多