Debugging inefficient Project Euler 58

时间:2015-12-10 02:01:08

标签: haskell

I'm trying to solve Project Euler's Problem 58.

For some reason, when I run the following code, it makes it through around 3000 rings before my computer freezes, presumably from how taxing the code is. I'd get the solution given enough resources, but at present, I'm taking too long.

However, it's as efficient as I can get it. I don't know what I could improve to solve the problem.

The function isPrime simply takes an Int and returns if it's prime.

The problem and my current solutions are below.

{-|
 - Problem 58
 -
 - Starting with 1 and spiralling anticlockwise in the following way, a square 
 - spiral with side length 7 is formed.
 -
 - 37 36 35 34 33 32 31
 - 38 17 16 15 14 13 30
 - 39 18  5  4  3 12 29
 - 40 19  6  1  2 11 28
 - 41 20  7  8  9 10 27
 - 42 21 22 23 24 25 26
 - 43 44 45 46 47 48 49
 -
 - It is interesting to note that the odd squares lie along the bottom right 
 - diagonal, but what is more interesting is that 8 out of the 13 numbers    lying
 - along both diagonals are prime; that is, a ratio of 8/13 ≈ 62%.
 -
 - If one complete new layer is wrapped around the spiral above, a square spiral
 - with side length 9 will be formed. If this process is continued, what is the
 - side length of the square spiral for which the ratio of primes along both 
 - diagonals first falls below 10%?
 -}

import Data.List (genericLength)
import Data.List.Split (splitPlaces)
import EulerFunctions (isPrime)

-- Each ring is another concentric circle in the spiral. 
-- The first 3 are [1], [2..9], and [10..25]
type Ring = [Int]

-- Returns an infinite list of rings.
rings :: [Ring]
rings  = [1] : splitPlaces [8,16..] [2..]

-- Given a ring, returns the numbers on its corners.
corners     :: Ring -> [Int]
corners [1]  = [1]
corners ring = let end  = last ring
                   diff = (length ring) `div` 4
                in map (\n -> end - n * diff) [0..3]

diagPrimes      = scanl1 (+) $ map (genericLength . filter isPrime . corners) rings
diagLengths     = [1,5..]
diagPrimeRatios = zipWith (/) diagPrimes diagLengths

p58 = fst . head . filter ((< 0.1) . snd) $ [1,3..] `zip` tail diagPrimeRatios

2 个答案:

答案 0 :(得分:2)

汇编

与Stack Overflow一样,我觉得我需要确保人们1.编译代码而不是使用ghci 2.使用优化。考虑Haskell更像C,因为你应该编译成二进制并使用-O2,而不是像你假设解释的Python一样好。简而言之:

ghc -O2 -fllvm so.hs

如果你这样做,你会发现你的算法会在适当的时间内终止 - 几分钟和低堆使用。

算法问题

为什么这需要几分钟?让我们看看!

  1. corners正在建立一个整体列表然后转角。为什么?你知道每个新包装边的长度,只需得到四个数字! [1]然后[3,5,7,9][13,17,21,25]。也就是说,每次添加2,2,2,2 .. 4,4,4,4 ... 6,6,6,6 ...... replicate 4 (sidelength-1)

  2. [删除]我以为你正在重新验证素数。

  3. 一个小点并且由于缺少类型签名而隐藏,但你的diagLengths算术都是双倍的 - 使用整数运算然后强制转换为哦性能上的无意义差异diagLength = map fromIntegral ([1,5..] :: [Int])

  4. 编辑:所以我认为主要问题是我错了的素数。无论如何,我试过的惯用解决方案产生的结果约为0.3秒,因此它是可行的。我认为为corners构建和遍历这些较大的列表是相当杀手的,因为这会花费分配和内存访问。

答案 1 :(得分:1)

虽然你当然可以采用蛮力方法,但有一个非常简单的功能解决方案。

首先,请注意您只需要考虑螺旋的角落。并且您可以直接生成这些数字(无需计算其间的数字)。在给定的7长螺旋中,角有一个非常简单的几何序列:

[3,5,7,9],[13,17,21,25],[31,37,43,49]

请注意,每个列表中连续元素之间的差异是不变的,并且每个列表的常量差异根据[2,4,6..]增加,并且一侧的最后一个角与下一个的第一个角之间的差异是两次第一个列表的连续值之间的差异。这篇散文完全对应于以下代码:

{-# LANGUAGE BangPatterns #-}

corners :: [[Integer]]
corners = [1] : go 3 2 where 
  go i0 !d = let l = i0+3*d 
                 d' = d+2 
             in [i0,i0+d,i0+2*d,l] : go (l+d') d' 

请注意,第一个角落是一个特例。还要注意使用爆炸模式 - 对于像这样的函数来说严格是必不可少的(角点永远不会检查d因此必须手动强制)。这是一个重要的性能考虑因素。

接下来,考虑一个计算单个列表中素数比率的函数。由于它必须考虑旧比率,因此还应将旧比率作为参数:

import Data.List (foldl')
import Math.NumberTheory.Primes.Testing (isPrime)

primeRatio :: (Int, Int) -> [Integer] -> (Int, Int) 
primeRatio pR nums = foldl' (\(!p,!t) n -> (p + fromEnum (isPrime n), 1+t)) pR nums 

fromEnum直接将False转换为0,将True转换为1(而不是if) - 否则此功能很简单。再次注意,爆炸模式和foldl'的使用 - 对于此处的性能都很重要。此外,素性测试是由优秀的arithmoi包实现的高效Baille PSW - 您使用的素性测试将显着影响您的表现。

现在我们终于可以编写一个快速循环,它在线性时间内消耗每个角落列表:

  go !rIndex (cs:css) tgt pR  
    | tgt pR'   = 2*rIndex+1
    | otherwise = go (rIndex+1) css tgt pR' 
      where pR' = primeRatio pR cs 

请注意,最终答案(行数)直接从索引(从中心到外行的距离)计算得出。这必须始终是奇数,表达式2*x+1就是这种情况。终止检查只是作为此功能的参数编码 - 我们不必决定它是什么。如果终止检查失败,那么我们所做的就是用角落列表中的下一个元素更新当前比率!多么简单

我们有一个最终考虑因素:前几行的比率低于10%。所以我们必须特别对待那些:

euler58 :: Integer
euler58 =  
  let prefix = 4 -- Number of cases to handle specially

      -- Special and non special cases
      (first,rest) = splitAt prefix corners 

      -- Initial part where the ratio is allowed to fall below the targe
      initVals = foldl' primeRatio (0,0) first 

      go !rIndex (cs:css) tgt pR  
        | tgt pR'   = 2*rIndex+1
        | otherwise = go (rIndex+1) css tgt pR' 
          where pR' = primeRatio pR cs 

  in go prefix rest (\(nPrimes, nTotal) -> (nPrimes%nTotal) < (1%10)) initVals 

即使在没有优化的情况下进行编译,这也会在我的机器上在不到半秒的时间内计算出答案(26241)。我甚至不打算尝试进一步优化 - 也许它可以被推得更快,但它已经是&#34;瞬间&#34;所以似乎并不是一个重点。