Haskell: более быстрое суммирование простых чисел

Отказ от ответственности: я работаю над проблемой Эйлера 9.

Я складываю довольно большие числа, все простые числа от 1 до 2 000 000.

Суммирование этих простых чисел занимает вечность. Я использую встроенную в Haskell функцию «сумма».

as in:

sum listOfPrimes

Есть ли другие более быстрые варианты?

-- Мой основной генератор был медленным звеном в моем коде.


person cbrulak    schedule 16.07.2009    source источник


Ответы (4)


Похоже, ваша проблема не в суммировании чисел, а в их генерации. Какова ваша реализация listOfPrimes?

Этот документ может представлять интерес: http://lambda-the-ultimate.org/node/3127

person Edward Z. Yang    schedule 16.07.2009
comment
Я согласен! Тот мне очень помог! - person Jay Atkinson; 17.07.2009

Я надеюсь, вы используете ghc -O2, а не ghci, верно? Ваша проблема будет в генерации, а не в суммировании.

Один из более быстрых способов — использовать последовательности на основе слияния потоков, которые лучше оптимизируются. С обычными списками:

import Data.List
import qualified Data.Map as M

primes :: [Integer]
primes = mkPrimes 2 M.empty
  where
    mkPrimes n m = case (M.null m, M.findMin m) of
        (False, (n', skips)) | n == n' ->
            mkPrimes (succ n) (addSkips n (M.deleteMin m) skips)
        _ -> n : mkPrimes (succ n) (addSkip n m n)
    addSkip n m s = M.alter (Just . maybe [s] (s:)) (n+s) m
    addSkips = foldl' . addSkip

-- fuse:
main = print (sum (takeWhile (<= 2000000) primes))

Мы получили,

$ ghc -O2 --make A.hs
$ time ./A           
142913828922
./A  9.99s user 0.17s system 99% cpu 10.166 total

Переключение на потоки, поэтому sum . предохранители takeWhile:

import qualified Data.List.Stream as S
main = print (S.sum (S.takeWhile (<= 2000000) primes))

Экономит немного времени,

$ time ./A           
142913828922
./A  9.60s user 0.13s system 99% cpu 9.795 total

Но ваша проблема будет заключаться в простом поколении, как мы увидим, если мы вообще отбросим суммирование, заменив sum на last:

$ time ./A           
1999993
./A  9.65s user 0.12s system 99% cpu 9.768 total

Так что найдите лучший первичный генератор. :-)

Наконец, на Hackage есть библиотека для быстрых генераторов простых чисел:

http://hackage.haskell.org/packages/archive/primes/0.1.1/doc/html/Data-Numbers-Primes.html

Используя его, наше время становится:

$ cabal install primes
$ cabal install stream-fusion

$ cat A.hs
import qualified Data.List.Stream as S
import Data.Numbers.Primes

main = print . S.sum . S.takeWhile (<= 2000000) $ primes

$ ghc -O2 -fvia-C -optc-O3 A.hs --make

$ time ./A
142913828922
./A  0.62s user 0.07s system 99% cpu 0.694 total
person Don Stewart    schedule 16.07.2009
comment
Поскольку вы не указали на него ссылку, cse.unsw.edu.au/ ~dons/streams.html | Довольно крутая штука, я и не знал, что она красиво упакована на Hackage. - person ephemient; 17.07.2009

Я написал "Сито Эратосфена" здесь:

import Data.List
import qualified Data.Map as M
primes :: [Integer]
primes = mkPrimes 2 M.empty
  where
    mkPrimes n m = case (M.null m, M.findMin m) of
        (False, (n', skips)) | n == n' ->
            mkPrimes (succ n) (addSkips n (M.deleteMin m) skips)
        _ -> n : mkPrimes (succ n) (addSkip n m n)
    addSkip n m s = M.alter (Just . maybe [s] (s:)) (n+s) m
    addSkips = foldl' . addSkip

Используя это, для print . sum $ takeWhile (<= 20000000) на моем рабочем столе требуется около 25 секунд. Могло быть лучше? Конечно, для запуска J требуется менее 1 секунды.

   +/p:i.p:^:_1]20000000
12272577818052

но у него довольно оптимизированный генератор простых чисел.

person ephemient    schedule 16.07.2009
comment
очень красиво и понятно и хорошо! Три стандартных улучшения — только коэффициенты, отсрочка добавления простых чисел на карту и подача двойных простых чисел — ускоряют его работу в 10,6 раз для 2 млн (проверено) и в ~ 13,4 раза для 20 млн (прогноз). Таким образом, время выполнения станет ~ 1,9 с. Я добавил измененный код на страницу haskellwiki. :) - person Will Ness; 21.12.2012
comment
@WillNess Лучший ответ :-) - person ephemient; 22.12.2012
comment
почему, большое спасибо. :) ... забыл упомянуть, что теперь он работает в почти постоянном пространстве, 1..2 МБ. но это ожидаемо. - person Will Ness; 22.12.2012

Медленная часть вашей функции - это, безусловно, генерация простых чисел, а не функция sum. Хороший способ генерировать простые числа:

isprime :: (Integral i) => i -> Bool
isprime n = isprime_ n primes
  where isprime_ n (p:ps)
          | p*p > n        = True
          | n `mod` p == 0 = False
          | otherwise      = isprime_ n ps

primes :: (Integral i) => [i]
primes = 2 : filter isprime [3,5..]

Я думаю, что это очень читабельно, хотя, возможно, немного удивительно, что он вообще работает, потому что он использует рекурсию и ленивость списка primes. Это также довольно быстро, хотя можно было бы провести дальнейшую оптимизацию за счет удобочитаемости.

person sth    schedule 16.07.2009
comment
Эй, я раньше не видел этот конкретный алгоритм в Haskell. Довольно круто, это проще и больше похоже на Haskell, чем у меня. - person ephemient; 17.07.2009
comment
Фактически это эквивалентно неэффективному алгоритму. Он по-прежнему проверяет каждое число на соответствие каждому простому числу. - person newacct; 17.07.2009
comment
@newacc: он использует неэффективный алгоритм, но он быстрее, чем эффективная версия, по крайней мере, в этом случае. Эффективный алгоритм должен выполнять множество модификаций карты и постоянно перебирать карты все большего и большего размера. Это не обязательно лучше, чем сравнение с несколькими простыми числами. - person sth; 17.07.2009
comment
Точно. Он кажется таким же простым и чистым, как и традиционный primes = sieve [2..] where sieve (p:xs) = p:[x | x <- xs, mod x p /= 0] — фактически, он эквивалентен — но работает более эффективно. - person ephemient; 17.07.2009
comment
это (почти) оптимальное решето пробного деления (OTD), радикально более эффективное, чем традиционный неэффективный алгоритм, то есть решето Тернера ps=sv[2..] where sv(p:xs)=p:sv[x|x<-xs,rem x p>0], потому что оно останавливает тестирование для n на его квадратном корне - Тернера проверки по всем простым числам ниже самого числа. Теоретическая временная сложность для OTD равна O(k^1.5/(log k)^0.5) (эмпирически обычно определяется как ~ k^1.45), а для Тернера - O(k^2), при k произведенных простых числах. Таким образом, при получении миллиона простых чисел OTD будет в ~ 2000x ... 4000x (или более) быстрее, чем решето Тернера. - person Will Ness; 22.12.2012