我是Haskell的新手,我正试图以流处理方式实现Euler的Sieve。
当我检查Haskell Wiki page about prime numbers时,我发现了一些神秘的流优化技术。 在该wiki的 3.8线性合并中:
primesLME = 2 : ([3,5..] `minus` joinL [[p*p, p*p+2*p..] | p <- primes'])
where
primes' = 3 : ([5,7..] `minus` joinL [[p*p, p*p+2*p..] | p <- primes'])
joinL ((x:xs):t) = x : union xs (joinL t)
它说
“此处引入双素数Feed以防止不需要 根据Melissa O'Neill的说法,记忆并因此防止内存泄漏 代码。”
怎么会这样?我无法弄清楚它是如何运作的。
答案 0 :(得分:10)
正常情况下,理查德·伯德(Richard Bird)制定埃拉托斯讷(Eratosthenes)筛子的素数流的定义是自我指导的:
import Data.List.Ordered (minus, union, unionAll)
ps = ((2:) . minus [3..] . foldr (\p r -> p*p : union [p*p+p, p*p+2*p..] r) []) ps
此定义生成的素数ps
用作输入。为了防止恶性循环,定义用初始值2引发。这对应于Eratosthenes筛子的数学定义,即在复合材料之间的间隙中找到素数,为每个素数列举 p 以 p , P = {2} U ({3,4,... } \ U {{ p 2 , p 2 + p , 2 + 2p ,...} | P }中 p 。
生成的流在其自己的定义中用作输入。这导致整个素数流保留在内存中(或者无论如何都保留在内存中)。这里的修复点是共享, corecursive :
fix f = xs where xs = f xs -- a sharing fixpoint combinator
ps = fix ((2:) . minus [3..] . foldr (...) [])
-- = xs where xs = 2 : minus [3..] (foldr (...) [] xs)
想法 (由于Melissa O'Neill),然后将其分成两个流,内部循环进给进入第二个素数流“上面”:
fix2 f = f xs where xs = f xs -- double-staged fixpoint combinator
ps2 = fix2 ((2:) . minus [3..] . foldr (...) [])
-- = 2 : minus [3..] (foldr (...) [] xs) where
-- xs = 2 : minus [3..] (foldr (...) [] xs)
因此,当ps2
产生一些素数p
时,“核心”素数的内部流xs
只需要实例化到大约{{1}然后,系统会立即丢弃由sqrt p
生成的所有素数并将其垃圾收集:
\ \ <- ps2 <-. \ \ <- xs <-. / \ \_________/
内循环ps2
生成的素数无法立即丢弃,因为xs
流本身需要它们。当xs
生成素数xs
时,只有在q
部分计算消耗之后才能丢弃sqrt q
以下的部分。换句话说,这个序列将指针保持在自身最低生成值的foldr
之内(因为它正由其消费者使用,如sqrt
)。
因此,使用一个Feed循环(使用print
)几乎整个序列都必须保留在内存中,而使用double feed(使用fix
)时,只需要保留内部循环只能达到主流产生的当前值的平方根。因此,整体空间复杂度从大约 O(N)减少到大约 O(sqrt(N)) - 大幅减少。
为了实现这一点,代码必须使用优化进行编译,即使用fix2
开关,并独立运行。您可能还必须使用-O2
开关。并且在测试代码中必须只有一个-fno-cse
的引用:
ps2
事实上,在Ideone上进行测试时,it does show实际上是一个不变的内存消耗。
这是Eratosthenes的筛子,而不是Euler的筛子。
最初的定义是:
main = getLine >>= (read >>> (+(-1)) >>> (`drop` ps2) >>> print . take 5)
由于倍数的过早处理,两者效率都很低。通过将eratos (x:xs) = x : eratos (minus xs $ map (*x) [x..] ) -- ps = eratos [2..]
eulers (x:xs) = x : eulers (minus xs $ map (*x) (x:xs)) -- ps = eulers [2..]
和枚举融合到一个相距较远的枚举(从map
到x
,即{{1},可以很容易地修复第一个定义。 }}),因此它的处理可以 推迟 - 因为这里每个素数的倍数都是 独立生成 (以固定间隔列举):
x*x
与本帖顶部的Bird's筛相同,分段:
[x*x, x*x+x..]
(此处使用eratos (p:ps) xs | (h,t) <- span (< p*p) xs = -- ps = 2 : eratos ps [2..]
h ++ eratos ps (minus t [p*p, p*p+p..]) -- "postponed sieve"
作为pointfree简写。)
第二个定义没有简单的解决方法,即ps = 2 : [n | (r:q:_, px) <- (zip . tails . (2:) . map (^2) <*> inits) ps,
n <- [r+1..q-1] `minus` foldr union []
[[s+p, s+2*p..q-1] | p <- px, let s = r`div`p*p]]
。
另外:您可以看到使用Python生成器实现的相同想法,以进行比较here。
事实上,Python代码采用了短暂的素数流的伸缩,多级递归生成;在Haskell we can arrange for it中使用非共享,多阶段的固定点组合器 (f <*> g) x = f x (g x)
:
eulers