Спочатку я не планував писати відповідь. Але мені сказали після того, як інший користувач висловив дивну заяву, що просто множення перших парних праймерів було обчислювально дорожчим, ніж багаторазове застосування 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)).
Отже, наведені вище результати та короткий аналіз алгоритму повинні зробити дуже очевидним, який алгоритм швидший.