Факторний алгоритм більш ефективний, ніж наївне множення


38

Я знаю, як кодувати фабрикатори, використовуючи як ітераційні, так і рекурсивні (наприклад, n * factorial(n-1)наприклад). Я читав у підручнику (без подальших пояснень), що існує ще більш ефективний спосіб кодування фабрикантів, розділивши їх навпіл рекурсивно.

Я розумію, чому так може бути. Однак я хотів спробувати кодувати це самостійно, і я не думаю, що знаю, з чого почати. Друг запропонував спочатку написати базові випадки. і я думав використовувати масиви, щоб я міг відслідковувати числа ... але я дійсно не бачу жодного виходу для створення такого коду.

Які методи я повинен досліджувати?

Відповіді:


40

Найкращий алгоритм, який відомий - це виражати факторіал як продукт первинних сил. За допомогою ситового підходу можна швидко визначити праймери, а також правильну потужність для кожного праймера. Обчислення кожної потужності можна зробити ефективно, використовуючи повторне квадратування, і тоді множники множать разом. Це описав Пітер Б. Борвейн, Про складність обчислення факторів , Журнал алгоритмів 6 376–380, 1985. ( PDF ) Коротше кажучи, n!можна обчислити за час O(n(logn)3loglogn) , порівняно з Ω(n2logn) час, необхідний при використанні визначення.

Можливо, підручник мав на увазі метод ділення і перемоги. Можна зменшити множення n1 , використовуючи звичайний зразок продукту.

Нехай позначають 1 3 5 ( 2 n - 1 ) як зручне позначення. Переставіть коефіцієнти ( 2 n ) ! = 1 2 3 ( 2 n ) як ( 2 n ) ! = n ! 2 n3 5 7 ( 2 n -n?135(2n1)(2n)!=123(2n) Тепер припустимо, що n = 2 k для деякого цілого числа k > 0 . (Це корисне припущення, щоб уникнути ускладнень у наступному обговоренні, і ідею можна поширити на загальну п .) Тоді ( 2 к ) ! = ( 2 к - 1 ) ! 2 2 к - 1 ( 2 к - 1 ) ? і розширивши цю повторюваність, ( 2 к ) ! =

(2n)!=n!2n357(2n1).
n=2kk>0n(2k)!=(2k1)!22k1(2k1)? Обчислювальна техніка( 2 к - 1 )?
(2k)!=(22k1+2k2++20)i=0k1(2i)?=(22k1)i=1k1(2i)?.
(2k1)?а множення часткових добутків на кожному етапі займає множення. Це поліпшення коефіцієнта майже 2 від 2 k - 2 множення лише за допомогою визначення. Для обчислення потужності 2 потрібні деякі додаткові операції , але у двійковій арифметиці це можна зробити дешево (залежно від того, що саме потрібно, може знадобитися просто додавання суфікса 2 к - 1 нулі).(k2)+2k1222k222k1

У наведеному нижче коді Ruby реалізована спрощена версія цього. Це не уникає перерахунку навіть там, де це можна зробити:n?

def oddprod(l,h)
  p = 1
  ml = (l%2>0) ? l : (l+1)
  mh = (h%2>0) ? h : (h-1)
  while ml <= mh do
    p = p * ml
    ml = ml + 2
  end
  p
end

def fact(k)
  f = 1
  for i in 1..k-1
    f *= oddprod(3, 2 ** (i + 1) - 1)
  end
  2 ** (2 ** k - 1) * f
end

print fact(15)

Навіть цей код першого проходу поліпшується на дрібниці

f = 1; (1..32768).map{ |i| f *= i }; print f

приблизно на 20% в моєму тестуванні.

Додавши трохи роботи, це можна вдосконалити ще більше, також усуваючи вимогу, щоб була потужністю 2 (див. Широке обговорення ).n2


Ви залишили важливим фактором. Час обчислення згідно з документом Борвейна не O (n log n log log n). Це O (M (n log n) log log n), де M (n log n) - час для множення двох чисел на розмір n log n.
gnasher729

18

Майте на увазі, що факторна функція зростає так швидко, що вам знадобляться цілі числа довільного розміру, щоб отримати будь-яку користь від більш ефективних методик, ніж наївний підхід. 21-й фактор вже занадто великий, щоб вміститися в 64-бітний unsigned long long int.

n!n

Θ(|a||b|)|x|xΩ(|a|+|b|)max(|a|,|b|)

На озброєнні цим фоном стаття у Вікіпедії повинна мати сенс.

Оскільки складність множення залежить від величини цілих чисел, які перемножуються, ви можете заощадити час, упорядкувавши множення в порядку, який забезпечує, щоб числа не перемножувались малими. Це виходить краще, якщо ви домовитеся, щоб номери були приблизно однакового розміру. "Розділення навпіл", на яке посилається ваш підручник, складається з наступного підходу ділити і перемагати для множення (множинного) набору цілих чисел:

  1. Впорядкуйте числа, які потрібно помножити (спочатку всі цілі числа від до ) у два набори, добуток яких приблизно однакового розміру. Це набагато дешевше, ніж робити множення:(одне машинне додавання).п | a b | | а | + | б |1n|ab||a|+|b|
  2. Застосовуйте алгоритм рекурсивно до кожної з двох підмножин.
  3. Помножте два проміжні результати.

Детальнішу інформацію див. У посібнику GMP .

Існують ще більш швидкі методи, які не лише переставляють коефіцієнти на але розбивають числа, розкладаючи їх на їх основну факторизацію та переставляючи отриманий дуже довгий добуток здебільшого-малих цілих чисел. Я просто наводжу посилання зі статті Вікіпедії: «Про складність обчислення факторів» Пітера Борвейна та реалізацій Петра Лучного .н1n

¹ Є більш швидкі способи обчислення наближень по, але це вже не обчислення факторіалу, а обчислення його наближення.n!


9

Оскільки факторна функція зростає так швидко, ваш комп'ютер може зберігати лишедля відносно невеликих . Наприклад, подвійний може зберігати значення до. Тож якщо ви хочете по-справжньому швидкий алгоритм обчислення, просто використовуйте таблицю розміром .n 171 ! н ! 171n!n171!n!171

Питання стає цікавішим, якщо вас цікавить Або функція (або ). У всіх цих випадках (включаючи ) Я не дуже розумію коментар у вашому підручнику.Γ log Γ n !log(n!)ΓlogΓn!

На додаток, ваш ітеративний та рекурсивний алгоритми рівнозначні (аж до помилок з плаваючою комою), оскільки ви використовуєте хвостову рекурсію.


"ваш ітеративний та рекурсивний алгоритми еквівалентні", ви маєте на увазі їх асимптотичну складність, правда? що стосується коментаря до підручника, добре, що я перекладаю його з іншої мови, тож, можливо, мій переклад гасить.
користувач65165

Книга розповідає про ітераційне та рекурсивне, а потім коментує, як, якщо ви використовуєте ділити і перемагати, щоб ділити n! навпіл ви можете отримати швидше рішення ...
user65165

1
Моє поняття еквівалентності не є повністю формальним, але ви можете сказати, що виконані арифметичні операції однакові (якщо ви перемикаєте порядок операндів в алгоритмі рекурсивності). "Притаманний" інший алгоритм виконає інший обчислення, можливо, використовуючи якийсь "трюк".
Yuval Filmus

1
Якщо розглянути розмір цілого числа як параметр у складності множення, загальна складність може змінитися, навіть якщо арифметичні операції "однакові".
Tpecatte

1
@CharlesOkwuagwu Правильно, ви можете використовувати таблицю.
Yuval Filmus
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.