理解Haskell代码,它将`tails`函数应用于列表理解中的无限列表

时间:2014-11-28 03:22:18

标签: haskell list-comprehension primes infinite

今天早些时候向Project Euler problem 50提交我的解决方案之后,我正在浏览问题的论坛,看看其他人的解决方案/执行时间。

过了一段时间,我开始为我的代码感到非常自豪,它在大约3秒内解决了它(我的代码使用了Primes库,并使用O2编译了...)

...然后我看到下面的代码在约0.05秒内解决了它...在解释模式下(即ghci)。

有人可以解释下面的代码如何/为何解决这个特殊问题?

思维扭曲部分是将tails函数应用于列表推导中的无限素数列表(primes)。我很难理解我们如何保证我们会查看连续素数的所有可能子列表,而不仅仅是tails生成的那些子列表。

(我在ghci中尝试一些代码的常用策略在这种情况下不起作用,因为primes是无限的......)

问题:我们被要求找到1,000,000以下的最大素数,这是对连续素数求和的结果。例如,100以下的最大素数是连续素数的总和是41(2 + 3 + 5 + 7 + 11 + 13)。

import Data.List (tails)
import Data.Numbers.Primes

under n xs = takeWhile (< n) xs

takeUntil p xs = foldr (\x r-> if p x then [x] else x:r) [] xs

res :: [((Int, Int), (Int, Int))] 
-- ((top_length, sums_to), (total_length, starting_prime))

res = [(r,(length s,x)) | (x:xs) <- tails primes
                        , let s = zip [1..]
                                $ under 100
                                $ scanl (+) x xs
                        , let r = ...] 

main = mapM_ print $ takeUntil ...

2 个答案:

答案 0 :(得分:1)

要解决无限列表问题,只需定义:

primesUpTo100 = takeWhile (< 100) primes

并在primesUpTo100的定义中使用res代替primes

接下来,更改res的定义以返回不同的值,以便您可以看到正在发生的事情,例如:

res = do
  (x:xs) <- tails primesUpTo100
  let s = zip [1..] $ under 100 $ scanl (+) x xs
  let r = last  $ filter (isPrime.snd) s
  return (x, take 10 xs, s)

现在,您可以看到s是什么以及它与x以及xs的前10个元素的关系。

如果列表res太长,只需评估take 10 res。另一个技巧是定义辅助函数:

import Control.Monad
pp xs = forM_ xs print

这将打印出一个列表,其中新行上的每个元素都应该使事情更容易阅读。 E.g:

pp (take 10 res)

使用所有这些想法,我打赌你可以搞清楚。它实际上是对最长序列的强力搜索,但使用scanl可能会避免重新计算总和。

答案 1 :(得分:1)

一般来说,tails的{​​{1}}没有问题,因为Haskell lazy (评估是按需)。

这里特别是问题更少,因为每个子列表都被primes修剪 - 只有一个有限的前缀。

under 100 = takeWhile (< 100)只需查看(x:xs) <- tails primes的所有后缀 - 即 primes ,从primes开始;然后 primes x=2开始,然后是x=3。该模式只需要头元素5,7,11, ...和素数列表的尾x,甚至不会立即请求它们的值,只有所谓的“spine” primes 列表被强制执行,一次1个陷阱(当然确保xs以至少一个元素primes开头,将自动计算{{1}的实际值但是那是另一回事)。

因此x对素数列表的连续后缀进行操作。试试x,看看那里发生了什么。


键入

(x:xs) <- tails primes

在GHCi提示符下,您实际上要求所有要打印的tails [1..10]列表元素作为输出。但是

GHCi> primes

只会请求低于100的人,之后不会超过一个元素。它使用内置的primes,一个接一个地检查GHCi> under 100 primes 中的元素,直到发现一个谓词失败(在这种情况下,大于100)。终止元素不包含在结果中。

用户定义的takeWhile与此不同之处仅在于它在结果中还包含终止元素(并且谓词的含义被翻转 - 它指示何时停止)。

primes是计算序列部分和的序列的常用方法:

takeUntil

代码

scanl (+)

表示:

Prelude> scanl (+) 1 [2..10]
[1,3,6,10,15,21,28,36,45,55]

因此,我们会为每个连续的素数 [ (r,(length s,x)) | (x:xs) <- tails primes , let s = zip [1..] $ under 100 $ scanl (+) x xs , let r = last $ filter (isPrime.snd) s] 获取 for each suffix of primes -- [[2,3,5,7...],[3,5,7,11...],...] let x = head suffix -- for all primes from x xs = tail suffix -- i.e. xs t1 = scanl (+) x xs -- calculate partial sums t2 = under 100 t1 -- stopping when the sum reaches 100 s = zip [1..] t2 -- index each sum by the length of subsequence, t3 = filter (isPrime.snd) s -- keep only such that summed to a prime, r = last t3 -- and take the one created from the in -- longest subsequence, starting from x emit (r,(length s,x)) -- and collect the data 的条目列表。

((top_length, sums_to), (total_length, starting_prime))中的starting_prime = 2,3,5,7,11, ...表达式确定何时可以停止,因为不可能再改进结果。