Спочатку я не планував писати відповідь. Але мені сказали після того, як інший користувач висловив дивну заяву, що просто множення перших парних праймерів було обчислювально дорожчим, ніж багаторазове застосування lcm
. Ось два алгоритми та деякі орієнтири:
Мій алгоритм:
Алгоритм основного покоління, що дає мені нескінченний список простих чисел.
isPrime :: Int -> Bool
isPrime 1 = False
isPrime n = all ((/= 0) . mod n) (takeWhile ((<= n) . (^ 2)) primes)
toPrime :: Int -> Int
toPrime n
| isPrime n = n
| otherwise = toPrime (n + 1)
primes :: [Int]
primes = 2 : map (toPrime . (+ 1)) primes
Тепер використовуємо цей простий список для обчислення результату для деяких N
:
solvePrime :: Integer -> Integer
solvePrime n = foldl' (*) 1 $ takeWhile (<= n) (fromIntegral <$> primes)
Тепер інший алгоритм на основі lcm, який, правда кажучи, є досить стислим, головним чином тому, що я реалізував першокласне покоління з нуля (і не використовував алгоритм розуміння суперкороткого списку через його низькі показники), тоді як lcm
був просто імпортований з Prelude
.
solveLcm :: Integer -> Integer
solveLcm n = foldl' (flip lcm) 1 [2 .. n]
-- Much slower without `flip` on `lcm`
Тепер для орієнтирів код, який я використовував для кожного, був простим: ( -prof -fprof-auto -O2
тоді +RTS -p
)
main :: IO ()
main = print $ solvePrime n
-- OR
main = print $ solveLcm n
Для n = 100,000
, solvePrime
:
total time = 0.04 secs
total alloc = 108,327,328 bytes
vs solveLcm
:
total time = 0.12 secs
total alloc = 117,842,152 bytes
Для n = 1,000,000
, solvePrime
:
total time = 1.21 secs
total alloc = 8,846,768,456 bytes
vs solveLcm
:
total time = 9.10 secs
total alloc = 8,963,508,416 bytes
Для n = 3,000,000
, solvePrime
:
total time = 8.99 secs
total alloc = 74,790,070,088 bytes
vs solveLcm
:
total time = 86.42 secs
total alloc = 75,145,302,416 bytes
Я думаю, що результати говорять самі за себе.
Профілер вказує, що основне покоління займає все менший і менший відсоток часу виконання, оскільки n
збільшується. Тож це не вузьке місце, тому ми можемо поки що це ігнорувати.
Це означає, що ми дійсно порівнюємо виклики, lcm
коли один аргумент переходить від 1 до n
, а інший іде геометрично від 1 до ans
. Для дзвінків *
з однаковою ситуацією та додатковою перевагою отримати пропустити кожне непросте число (асимптотично безкоштовно, через більш дорогий характер *
).
І це добре відомо , що *
швидше , ніж lcm
, як і lcm
вимагає повторних застосувань mod
, і mod
асимптотично повільніше ( O(n^2)
проти ~O(n^1.5)
).
Отже, наведені вище результати та короткий аналіз алгоритму повинні зробити дуже очевидним, який алгоритм швидший.