[英]Efficient (O(n^2)) Sieve of Sundaram in Haskell
SO上有很多答案解釋了如何在Haskell中實現Sundaram篩子,但它們都......真的效率低下嗎?
我見過的所有解決方案都是這樣工作的:
<= n
[1..n]
過濾這些* 2 + 1
例如,這是我的實現,它找到1
和2n+2
之間的所有素數:
sieveSundaram :: Integer -> [Integer]
sieveSundaram n = map (\x -> 2 * x + 1) $ filter (flip notElem toRemove) [1..n]
where toRemove = [i + j + 2*i*j | i <- [1..n], j <- [i..n], i + j + 2*i*j <= n]
我遇到的問題是, filter
必須為[1..n]
的每個元素遍歷整個toRemove
列表,因此這具有復雜性 O(n^3) 而簡單的迭代實現具有復雜性 O(n^2 )。 如何在 Haskell 中實現這一點?
根據評論,不應將base
視為 Haskell 的完整標准庫。 每個 Haskell 開發人員都知道和使用幾個關鍵包,並且會考慮作為 Haskell事實標准庫的一部分。
通過“直接迭代實現”,我假設您的意思是標記和清除一組標志? 通常為此使用Vector
或Array
。 (兩者都將被視為“標准”。) O(n^2) Vector
解決方案如下所示。 盡管它在內部使用了可變向量,但批量更新運算符(//)
隱藏了這一事實,因此您可以將其編寫為典型的 Haskell 不可變和無狀態樣式:
import qualified Data.Vector as V
primesV :: Int -> [Int]
primesV n = V.toList -- the primes!
. V.map (\x -> (x+1)*2+1) -- apply transformation
. V.findIndices id -- get remaining indices
. (V.// [(k - 1, False) | k <- removals n]) -- scratch removals
$ V.replicate n True -- everyone's allowed
removals n = [i + j + 2*i*j | i <- [1..n], j <- [i..n], i + j + 2*i*j <= n]
另一種更直接的可能性是IntSet
,它基本上是一組具有O(1)
插入/刪除和O(n)
有序遍歷的整數。 (這就像評論中建議的HashSet
,但專門用於整數。)這是在containers
包中,另一個“標准” package 實際上與 GHC 源捆綁在一起,即使它與base
不同。 它給出了一個 O(n^2) 的解決方案,如下所示:
import qualified Data.IntSet as I
primesI :: Int -> [Int]
primesI n = I.toAscList -- the primes!
. I.map (\x -> x*2+1) -- apply transformation
$ I.fromList [1..n] -- integers 1..n ...
I.\\ I.fromList (removals n) -- ... except removals
請注意,另一個重要的性能改進是使用更好的removals
定義,避免過濾所有n^2
組合。 我相信以下定義會產生相同的刪除列表:
removals :: Int -> [Int]
removals n = [i + j + 2*i*j | j <- [1..(n-1) `div` 3], i <- [1..(n-j) `div` (1+2*j)]]
我認為是 O(n log(n))。 如果你將它與上面的primesV
或primesI
一起使用,它就是瓶頸,所以我認為得到的整體算法應該是 O(n log(n))。
這個問題沒有定義低效的含義。 OP 似乎掛斷了使用 Haskell 惰性列表解決方案,這從一開始就效率低下,因為列表上的操作是順序的,並且在需要每個包含許多內部“管道”部件的元素分配 memory 時具有很高的恆定開銷實現可能的懶惰。
正如我在評論中提到的,Sundaram 篩子的原始定義是模糊的,並且由於過度訂閱表示奇數的范圍而包含許多冗余操作; 如那里所述,它可以大大簡化。
然而,即使在最小化 SoS 的低效率之后,如果使用 List 是想要 go 的方式:正如 OP 所指出的那樣,簡單的重復過濾整個列表效率不高,因為根據以下修訂的每個 List 元素將有許多重復操作OP的代碼:
sieveSundaram :: Int -> [Int]
sieveSundaram n = map (\x -> 2 * x + 3) $ filter (flip notElem toRemove) [ 0 .. lmt ]
where lmt = (n - 3) `div` 2
sqrtlmt = (floor(sqrt(fromIntegral n)) - 3) `div` 2
mkstrtibp i = ((i + i) * (i + 3) + 3, i + i + 3)
toRemove = concat [ let (si, bp) = mkstrtibp i in [ si, si + bp .. lmt ]
| i <- [ 0 .. sqrtlmt ] ]
main :: IO ()
main = print $ sieveSundaram 1000
由於改進的連接toRemove
列表中有O(n log n)
值,並且必須掃描所有這些值以查找所有奇數到篩分極限,因此漸近復雜度為O(n^2 log n)
,其中回答了這個問題,但不是很好。
最快的列表素數過濾技術是懶惰地合並生成的復合剔除列表的樹(而不是僅僅連接它),然后生成一個 output 列表,其中包含不在合並復合中的所有奇數(按升序排列,以便避免每次都掃描整個列表)。 使用線性合並不是那么有效,但是我們可以使用無限樹狀合並,它只會花費log n
的額外因子,當乘以剔除值數量的O(n log n)
復雜度時正確的 Sundaram 篩給出了O(n log^2 n)
的組合復雜度,這比以前的實現要小得多。
這種合並是有效的,因為每個連續的復合剔除 List 從最后一個奇數的平方加 2 開始,因此整個 List of List 中每個基值的剔除序列 List 的第一個值已經按遞增順序排列; 因此,列表列表的簡單合並排序不會競爭並且很容易實現:
primesSoS :: () -> [Int]
primesSoS() = 2 : sel 3 (_U $ map(\n -> [n * n, n * n + n + n..]) [ 3, 5.. ]) where
sel k s@(c:cs) | k < c = k : sel (k+2) s -- ~= ([k, k + 2..] \\ s)
| otherwise = sel (k+2) cs -- when null(s\\[k, k + 2..])
_U ((x:xs):t) = x : (merge xs . _U . pairs) t -- tree-shaped folding big union
pairs (xs:ys:t) = merge xs ys : pairs t
merge xs@(x:xs') ys@(y:ys') | x < y = x : merge xs' ys
| y < x = y : merge xs ys'
| otherwise = x : merge xs' ys'
cLIMIT :: Int
cLIMIT = 1000
main :: IO ()
main = print $ takeWhile (<= cLIMIT) $ primesSoS()
當然,必須要問一個問題:“為什么是 Sundaram 的篩子?” 當去除原始 SoS 公式的殘缺時( 參見 Wikipedia 文章),很明顯 SoS 和 Eratosthenes 的 Odds-Only Sieve 之間的唯一區別是 SoS 不會過濾基數剔除奇數只有那些像 Odds-Only SoE 那樣的優質產品。 以下代碼僅對找到的基本素數進行遞歸反饋:
primesSoE :: () -> [Int]
primesSoE() = 2 : _Y ((3:) . sel 5 . _U . map (\n -> [n * n, n * n + n + n..])) where
_Y g = g (_Y g) -- = g (g (g ( ... ))) non-sharing multistage fixpoint combinator
sel k s@(c:cs) | k < c = k : sel (k+2) s -- ~= ([k, k + 2..] \\ s)
| otherwise = sel (k+2) cs -- when null(s\\[k, k + 2..])
_U ((x:xs):t) = x : (merge xs . _U . pairs) t -- tree-shaped folding big union
pairs (xs:ys:t) = merge xs ys : pairs t
merge xs@(x:xs') ys@(y:ys') | x < y = x : merge xs' ys
| y < x = y : merge xs ys'
| otherwise = x : merge xs' ys'
cLIMIT :: Int
cLIMIT = 1000
main :: IO ()
main = print $ takeWhile (<= cLIMIT) $ primesSoE()
固定點_Y
組合器負責遞歸,rest 是相同的。 此版本將復雜度降低了一個log n
因子,因此現在漸近復雜度為O(n log n log log n)
。
如果一個人真的想要效率,一個人不使用列表,而是使用可變的 arrays。 以下代碼使用內置的位壓縮數組將 SoS 實現到固定范圍:
{-# LANGUAGE FlexibleContexts #-}
import Control.Monad.ST ( runST )
import Data.Array.Base ( newArray, unsafeWrite, unsafeFreezeSTUArray, assocs )
primesSoSTo :: Int -> [Int] -- generate a list of primes to given limit...
primesSoSTo limit
| limit < 2 = []
| otherwise = runST $ do
let lmt = (limit - 3) `div` 2 -- limit index!
oddcmpsts <- newArray (0, lmt) False -- indexed true is composite
let getbpndx i = (i + i + 3, (i + i) * (i + 3) + 3) -- index -> bp, si0
cullcmpst i = unsafeWrite oddcmpsts i True -- cull composite by index
cull4bpndx (bp, si0) = mapM_ cullcmpst [ si0, si0 + bp .. lmt ]
mapM_ cull4bpndx
$ takeWhile ((>=) lmt . snd) -- for bp's <= square root limit
[ getbpndx i | i <- [ 0.. ] ] -- all odds!
oddcmpstsf <- unsafeFreezeSTUArray oddcmpsts -- frozen in place!
return $ 2 : [ i + i + 3 | (i, False) <- assocs oddcmpstsf ]
cLIMIT :: Int
cLIMIT = 1000
main :: IO ()
main = print $ primesSoSTo cLIMIT
O(n log n)
的漸近復雜度和以下代碼對 Odds-Only SoE 執行相同的操作:
{-# LANGUAGE FlexibleContexts #-}
import Control.Monad.ST ( runST )
import Data.Array.Base ( newArray, unsafeWrite, unsafeFreezeSTUArray, assocs )
primesSoETo :: Int -> [Int] -- generate a list of primes to given limit...
primesSoETo limit
| limit < 2 = []
| otherwise = runST $ do
let lmt = (limit - 3) `div` 2 -- limit index!
oddcmpsts <- newArray (0, lmt) False -- when indexed is true is composite
oddcmpstsf <- unsafeFreezeSTUArray oddcmpsts -- frozen in place!
let getbpndx i = (i + i + 3, (i + i) * (i + 3) + 3) -- index -> bp, si0
cullcmpst i = unsafeWrite oddcmpsts i True -- cull composite by index
cull4bpndx (bp, si0) = mapM_ cullcmpst [ si0, si0 + bp .. lmt ]
mapM_ cull4bpndx
$ takeWhile ((>=) lmt . snd) -- for bp's <= square root limit
[ getbpndx i | (i, False) <- assocs oddcmpstsf ]
return $ 2 : [ i + i + 3 | (i, False) <- assocs oddcmpstsf ]
cLIMIT :: Int
cLIMIT = 1000
main :: IO ()
main = print $ primesSoETo cLIMIT
漸近效率為O(n log log n)
。
由於可變數組操作而不是列表操作中的常數因子執行時間減少以及漸近復雜度中log n
因子的減少,這兩個最后一個版本可能比它們的 List 等效版本快一百倍。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.