嗨haskell研究员。我目前正在处理23rd problem的Project Euler。我在atm的地方是我的代码对我来说似乎是正确的 - 不是在“好的算法”意义上,而是在“应该工作”的含义中 - 但会产生堆栈内存溢出。
我知道我的算法并不完美(特别是我当然可以避免在worker
函数的每个递归步骤中计算如此大的中间结果。)
虽然,在学习Haskell的过程中,我想了解为什么这段代码失败如此悲惨,以便下次避免这种错误。
对此计划错误原因的任何见解将不胜感激。
import qualified Data.List as Set ((\\))
main = print $ sum $ worker abundants [1..28123]
-- Limited list of abundant numbers
abundants :: [Int]
abundants = filter (\x -> (sum (divisors x)) - x > x) [1..28123]
-- Given a positive number, returns its divisors unordered.
divisors :: Int -> [Int]
divisors x | x > 0 = [1..squareRoot x] >>=
(\y -> if mod x y == 0
then let d = div x y in
if y == d
then [y]
else [y, d]
else [])
| otherwise = []
worker :: [Int] -> [Int] -> [Int]
worker (a:[]) prev = prev Set.\\ [a + a]
worker (a:as) prev = worker as (prev Set.\\ (map ((+) a) (a:as)))
-- http://www.haskell.org/haskellwiki/Generic_number_type#squareRoot
(^!) :: Num a => a -> Int -> a
(^!) x n = x^n
squareRoot :: Int -> Int
squareRoot 0 = 0
squareRoot 1 = 1
squareRoot n =
let twopows = iterate (^!2) 2
(lowerRoot, lowerN) =
last $ takeWhile ((n>=) . snd) $ zip (1:twopows) twopows
newtonStep x = div (x + div n x) 2
iters = iterate newtonStep (squareRoot (div n lowerN) * lowerRoot)
isRoot r = r^!2 <= n && n < (r+1)^!2
in head $ dropWhile (not . isRoot) iters
修改:确切错误为Stack space overflow: current size 8388608 bytes.
。通过+RTS -K...
增加堆栈内存限制并不能解决问题。
Edit2:关于sqrt的事情,我只是从评论中的链接复制粘贴它。为了避免必须将整数转换为双打并面对舍入问题等...
答案 0 :(得分:12)
将来,您自己尝试一些最小化是礼貌的。例如,通过一些播放,我能够发现以下程序也堆栈溢出(使用8M堆栈):
main = print (worker [1..1000] [1..1000])
......这真正指出了什么功能让你失意。我们来看看worker
:
worker (a:[]) prev = prev Set.\\ [a + a]
worker (a:as) prev = worker as (prev Set.\\ (map ((+) a) (a:as)))
即使在我第一次阅读时,这个功能在我脑海中被标记为红色,因为它是尾递归的。 Haskell中的尾递归通常不像其他语言那样好主意;保护递归(在递归之前生成至少一个构造函数,或者在生成构造函数之前递减少量次数)通常更适合于惰性求值。事实上,在这里,正在发生的事情是每次递归调用worker
都会在prev
参数中构建一个更深入,更深层次的嵌套thunk。当最终返回prev
的时候,我们必须非常深入地进行一系列Set.\\
调用,以确定我们最终得到的内容。
明显的严格注释没有帮助,这个问题有点模糊。让我们按摩worker
直到它工作。第一个观察是第一个子句完全归入第二个子句。这是风格;它不应该影响行为(空列表除外)。
worker [] prev = prev
worker (a:as) prev = worker as (prev Set.\\ map (a+) (a:as))
现在,明显严格的注释:
worker [] prev = prev
worker (a:as) prev = prev `seq` worker as (prev Set.\\ map (a+) (a:as))
我惊讶地发现这个堆栈仍然溢出!偷偷摸摸的是,列表上的seq
仅进行足够的评估,以了解列表是否与[]
或_:_
匹配。以下不会叠加溢出:
import Control.DeepSeq
worker [] prev = prev
worker (a:as) prev = prev `deepseq` worker as (prev Set.\\ map (a+) (a:as))
我没有将最终版本插回到原始代码中,但它至少适用于上面的最小化main
。顺便说一下,您可能会喜欢以下实现方法,它也会堆栈溢出:
import Control.Monad
worker as bs = bs Set.\\ liftM2 (+) as as
但可以使用Data.Set
代替Data.List
来修复,而不是严格的注释:
import Control.Monad
import Data.Set as Set
worker as bs = toList (fromList bs Set.\\ fromList (liftM2 (+) as as))
答案 1 :(得分:8)
作为Daniel Wagner correctly said,问题在于
worker (a:as) prev = worker as (prev Set.\\ (map ((+) a) (a:as)))
构建一个严重嵌套的thunk。通过利用deepseq
的两个参数在此应用程序中排序这一事实,您可以避免这种情况并获得比worker
更好的性能。因此,您可以通过注意在prev
小于2*a
的任何步骤中的所有内容都不能是两个丰富数字的总和来获得增量输出,因此
worker (a:as) prev = small ++ worker as (large Set.\\ map (+ a) (a:as))
where
(small,large) = span (< a+a) prev
做得更好。但是,它仍然很糟糕,因为(\\)
无法使用两个列表的排序。如果用
minus xxs@(x:xs) yys@(y:ys)
= case compare x y of
LT -> x : minus xs yys
EQ -> minus xs ys
GT -> minus xxs ys
minus xs _ = xs -- originally forgot the case for one empty list
(或使用data-ordlist包的版本),计算集差是O(长度)而不是O(长度^ 2)。
答案 2 :(得分:3)
好的,我装了它并试了一下。 Daniel Wagner的建议非常好,可能比我的好。问题确实在于worker函数,但我建议使用Data.MemoCombinators来记忆你的函数。
另外,你的除数算法有点傻。有一个更好的方法来做到这一点。它有点肮脏,需要大量的TeX,所以这里有一个指向math.stackexchange页面的链接,关于如何做到这一点。我正在谈论的那个,是接受的答案,尽管其他人给出了一个递归的解决方案,我认为会更快。 (它不需要素数分解。)