【问题标题】:Project Euler 50: Algorithm is incredibly slow, failing to understand whyProject Euler 50:算法非常慢,无法理解原因
【发布时间】:2015-12-25 04:21:22
【问题描述】:

我正在使用 Project Euler 来学习 Haskell。我是 Haskell 的新手,想出一个不需要花费大量时间的算法时遇到了很多麻烦。我估计这里的程序需要 14 千兆年才能得出解决方案。

问题:

100 万以下的哪个素数可以写成最 连续素数?

这是我的来源。我遗漏了 isPrime。我已经发布了它,因为它解决问题的效率太低了。我认为问题在于 slicedChains 和 primeChains 调用,但我不确定它是什么。我以前用 C++ 解决过这个问题。但无论出于何种原因,Haskell 中的有效解决方案似乎超出了我的范围。

编辑:我已经包含了 isPrime。

import System.Environment (getArgs)
import Data.List (nub,maximumBy)
import Data.Ord (comparing)

isPrime :: Integer -> Bool
isPrime 1 = False
isPrime 2 = True
isPrime x
        | any (== 0) (fmap (x `mod`) [2..x-1]) = False
        | otherwise = True

primeChain :: Integer -> [Integer]
primeChain x = [ n | n <- 1 : 2 : [3,5..x-1], isPrime n ]

slice :: [a] -> [Int] -> [a]
slice xs args = take (to - from + 1) (drop from xs)
    where from = head args
          to = last args

subsequencesOfSize :: Int -> [a] -> [[a]]
subsequencesOfSize n xs = let l = length xs
                          in if n>l then [] else subsequencesBySize xs !! (l-n)
    where
        subsequencesBySize [] = [[[]]]
        subsequencesBySize (x:xs) = let next = subsequencesBySize xs
                                    in zipWith (++) ([]:next) (map (map (x:)) next ++ [[]])

slicedChains :: Int -> [Integer] -> [[Integer]]
slicedChains len xs = nub [x | x <- fmap (xs `slice`) subseqs, length x > 1]
    where subseqs = [x | x <- (subsequencesOfSize 2 [1..len]), (last x) > (head x)]

primeSums :: Integer -> [[Integer]]
primeSums x = filter (\ns -> sum ns == x) chain
    where xs = primeChain x
          len = length xs
          chain = slicedChains len xs

compLength :: [[a]] -> [a]
compLength xs = maximumBy (comparing length) xs

cleanSums :: [Integer] -> [[Integer]]
cleanSums xs = fmap (compLength) filtered
    where filtered = filter (not . null) (fmap primeSums xs)

main :: IO()
main = do
    args <- getArgs
    let arg = read (head args) :: Integer
    let xs = primeChain arg
    print $ maximumBy (comparing length) $ cleanSums xs

【问题讨论】:

  • 您是否尝试过在较小的输入大小(例如低于 1000 的素数)上进行分析并查看哪个函数花费的时间最多?
  • 是的,它看起来像是 slicedChain 并且可能是 primeChain 的双重调用。我只是不完全确定如何避免它们。
  • 你可能想用一个筛子来制作一个包含所有素数的向量(甚至是一个列表),直到一百万,然后你可能想把它们塞进一个IntSet。素数向量可以让你很容易地总结连续的,而IntSet 可以很容易地检查一个数字是否是素数。
  • 不理想的isPrime 很容易成为罪魁祸首。为什么要把它排除在外?它不应该。
  • 我已经包含了我的 isPrime 函数。如果我计算正确,它似乎在线性时间内运行。

标签: haskell primes


【解决方案1】:

您的基本问题是您没有根据迄今为止找到的最佳解决方案来修剪您的搜索空间。

我可以从您使用maximumBy 来查找最长序列的事实中看出这一点。

例如,如果您在搜索过程中找到一个由 4 个素数组成且总和小于 10^6 的连续序列,则您不必检查任何以大于 250000 的素数开头的序列。

要进行这种修剪,您必须跟踪迄今为止找到的解决方案,并将候选序列的测试与其生成交错,以便迄今为止找到的最佳解决方案可以及早停止搜索。

更新

slicedChains 有几个效率低下的地方。 Haskell 列表实现了一个链表。这个视频很好地概述了链表以及它们与数组的区别:(link)

您的代码中的以下表达式将有问题 w.r.t.效率:

* nub 具有二次运行时间

* length x &gt; 1 - length 的复杂度为 O(n),其中 n 是列表的长度。更好的写法是:

lengthGreaterThan1 :: [a] -> Bool
lengthGreaterThan1 (_:_:_) = True
lengthGreaterThan1 _       = False

* subsequencesOfSize 2 [1..len] 可能写得更简洁:

[ [a,b] | a <- [1..len], b <- [a+1..len] ]

这也将确保a

*slice 中的takedrop 调用也是O(n)

*primeSums 中,对primeChain 的调用将一遍又一遍地重新生成基本上相同的列表,从而导致对isPrime 的多次调用。更好的方法是像这样定义primeChain

allPrimes = filter isPrime [1..]

primeChain x = takeWhile (<= x) allPrimes

allPrimes 列表将生成一次,primeChain 只需使用该列表的前缀。

* primeSums x 负责查找总和为正好 x 的序列,但它会查看很多不可能工作的序列。例如,primeSums 31 将检查:

11 + 13 + 17, 11 + 13 + 17 + 23, 11 + 13 + 17 + 23 + 29,
17 + 19, 17 + 19 + 23, 17 + 19 + 23 + 29,
19 + 23, 19 + 23 + 29
23 + 29

尽管很明显这些总和都不等于 31。

【讨论】:

  • 谢谢。我将使用什么样的结构或功能来进行交织和跟踪?我想它会是某种地图。
【解决方案2】:

因此,您首先需要的是一个好的数据结构:一旦找到长度为n 的序列,您就不会关心长度较短的序列,因此您的主要需求是:(1) 跟踪总和,( 2)跟踪集合中的素数,(3)删除最小元素,(4)添加一个新的最大元素。关键是摊销,在这种情况下,很少支付大笔费用,以至于您可以假装它是每次手术的小笔费用。数据结构如下所示:

data Queue x = Q [x] [x]
q_empty (Q [] []) = True
q_empty _ = False

q_headtails (Q (x:xs) rest) = (x, Q xs rest)
q_headtails (Q [] xs) = case reverse xs of y:ys -> (y, Q ys [])
                                           []   -> error "End of queue."

q_append el (Q beg end) = Q beg (el:end)

所以解构列表是可能的,但有时会触发 O(n) 操作,但这没关系,因为当它发生时,我们不必再为另一个 n 步骤执行此操作,因此它平均为一个操作每一步。 (您可能还想使用严格严格的列表来实现。)

为了节省长度操作并对列表中的项目求和,您可能还想缓存这些项目:

type Length = Int
type Sum = Int
type Prime = Int
data PrimeSeq = PS Length Sum (Queue Prime)

headTails (PS len sum q) = (x, PS (len - 1) (sum - x) xs)
  where (x, xs) = q_headtails q

append x (PS len sum xs) = PS (len + 1) (sum + x) (q_append x xs)

这些算法看起来像:

  1. 缓存您开始使用的 PrimeSeq 的副本
  2. 继续向其添加素数并测试素数,直到达到 10^6。
  3. 如果您发现新的素数序列较长,请更换缓存。
  4. 每当遇到 10^6 时,恢复到缓存,从队列前面拉出一个素数,然后根据需要重复。

【讨论】:

  • 事实证明,这里的列表足够快;虽然我不能在我的答案中包含代码,因为那毕竟是 PE。
【解决方案3】:

您的素数生成是二次的(isPrime 101 测试 rem 101 100 == 0 尽管 10 是需要测试 101 的最大数字 - 实际上 7 就足够了)。

即便如此,一个足够简单的基于列表的代码也能在 2 秒内找到答案(在 Intel Core i7 2.5 GHz,在 GHCi 中解释)。并且代码更正以利用上述优化(此外,仅通过素数进行测试),它需要 0.1s

另外,f x | t = False | otherwise = Truef x = not t 相同。

PE 网站要求我们不要给您任何提示。

但总的来说,Haskell 效率的关键在于它的惰性,它是生成,并尽可能少地重复工作。举个例子,我们可以将一堆它们一起生成为一个进程的一部分,而不是重新开始单独计算列表的每个切片,

 slices :: Int -> [a] -> [[a]]
 slices n = map (take n) . iterate tail -- sequence of list's slices of length n each

另一个原则是,尝试解决一个更一般的问题,你的就是一个例子。

编写了这样一个函数后,我们可以通过尝试不同的参数值(从小到大)来尝试解决问题的探索方式。我们被告知 21 个连续素数。 22 个呢? 27? 1127 个? ...我已经说得够多了。

如果开始花费太多时间,我们可以通过empirical orders of growth 分析来评估完整解决方案所需的运行时间。

尽管使用未优化的isPrime 代码可以很快找到解决方案,但探索过程可能会非常缓慢,但使用优化的代码已经足够快了:

 primes :: [Int]
 primes = 2 : filter isPrime [3,5..]
 isPrime n = and [rem n p > 0 | p <- takeWhile ((<= n).(^2)) primes]

【讨论】:

  • 好的,谢谢。 rem 和 mod 有什么区别?
  • @thespectre 如果两个参数都是肯定的,则没有区别。但rem 有时更快。
猜你喜欢
  • 1970-01-01
  • 2012-07-20
  • 2010-11-26
  • 2011-02-15
  • 1970-01-01
  • 2012-03-11
  • 2015-07-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多