Какие есть алгоритмы вычисления факториалов больших чисел без калькуляторов?
ПрограммированиеМатематика+3
527
В целом, конечно, согласен. Но вычисление гамма-функции ручками — не выглядит упрощением 🙂
Комментировать ответ…Комментировать…
Кандидат физико-математических наук, выпускник ШАД · 14 мая 2022
Некий Г. С. Улер с помощью допотопных средств годами вычислял число 1000! и в 1955 году опубликовал-таки все его 2568 цифр. Сработал на совесть: все они оказались верными.
Факториалы больших чисел и записывать-то затруднительно без вычислительной техники, не то что вычислять. Да и зачем, если компьютеры делают это за доли секунды?
7,5 K
Дело в том, что я недавно узнал о том, что иногда такое задание может встретиться на экзаменах учителей… Читать дальше
Комментировать ответ…Комментировать…
к.ф.м.н., доцент МФТИ, с.н.с. Института Проблем Управления. · 14 мая 2022
Ну без вычислительной техники вряд-ли удастся обойтись, уже 10! — довольно солидное число. Но в целом, можно использовать скажем формулу Стирлинга: Читать далее
1,0 K
Ответ считаю вкратце правильным. Если точность не требуется, то для условно “порядка роста” достаточно формулы… Читать дальше
Комментировать ответ…Комментировать…
Закончил физфак Новосибирского университета. Занимался теор. физикой и преподаванием… · 14 мая 2022
586
Непонятно, зачем снова постить тот же самый кривоватый ответ с теми же грамматическими ошибками и невразумительными… Читать дальше
Комментировать ответ…Комментировать…
Закончил физфак Новосибирского университета. Занимался теор. физикой и преподаванием… · 12 мая 2022
560
Если Вы уж хотите вставить ссылку, то наверное следует дать её не на кустарное исследование, а на более точную форм… Читать дальше
Комментировать ответ…Комментировать…
Написал только для того что бы “далее” нажать · 16 мая 2022
Без калькуляторов, конечно не просто. Я бы предложил очень приближенный способ- из упрощенной ф-лы Стирлига, для счета “в уме”
2.51*sqrt(n)*10^(n(lgn-0.43))
Например, n=10000
2.51*100*10^(10000*3.57)=2.51*10^35702, ошибка на три порядка, при 36 тыс дес.знаков
Для продвинутых счетчиков, можно добавить поправку.
493
действительно без калькулятора
Комментировать ответ…Комментировать…
О сообществе
Find the factorial of a large number.
What is Factorial of a number?
Factorial of a non-negative integer, is the multiplication of all integers smaller than or equal to n.
Factorial of a number
Examples:
Input: 100
Output: 933262154439441526816992388562667004-
907159682643816214685929638952175999-
932299156089414639761565182862536979-
208272237582511852109168640000000000-
00000000000000Input: 50
Output: 3041409320171337804361260816606476884-
4377641568960512000000000000
We have discussed a simple program for factorial.
Why conventional way of computing factorial fails for large numbers?
A factorial of 100 has 158 digits. It is not possible to store these many digits even if we use long int.
The idea is to use basic mathematics for multiplication.
Illustration:
Example to show working of multiply(res[], x)
- A number 5189 is stored in res[] as following: res[] = {9, 8, 1, 5}
- let x = 10
Initialize carry = 0
- At i = 0, prod = res[0]*x + carry = 9*10 + 0 = 90.
res[0] = 0, carry = 9
- At i = 1, prod = res[1]*x + carry = 8*10 + 9 = 89
res[1] = 9, carry = 8
- At i = 2, prod = res[2]*x + carry = 1*10 + 8 = 18
res[2] = 8, carry = 1
- At i = 3, prod = res[3]*x + carry = 5*10 + 1 = 51
res[3] = 1, carry = 5
- res[4] = carry = 5
res[] = {0, 9, 8, 1, 5}
Follow the steps below to solve the given problem:
- Create an array res[] of MAX size where MAX is a number of maximum digits in output.
- Initialize value stored in res[] as 1 and initialize res_size (size of ‘res[]’) as 1.
- Multiply x with res[] and update res[] and res_size to store the multiplication result for all the numbers from x = 2 to n.
- To multiply a number x with the number stored in res[], one by one multiply x with every digit of res[].
- To implement multiply function perform the following steps:
- Initialize carry as 0.
- Do following for i = 0 to res_size – 1
- Find value of res[i] * x + carry. Let this value be prod.
- Update res[i] by storing the last digit of prod in it.
- Update carry by storing the remaining digits in carry.
- Put all digits of carry in res[] and increase res_size by the number of digits in carry.
Below is the implementation of the above algorithm.
NOTE: In the below implementation, the maximum digits in the output are assumed as 500. To find a factorial of a much larger number ( > 254), increase the size of an array or increase the value of MAX. This can also be solved using Linked List instead of using res[] array which will not waste extra space.
C++
#include <iostream>
using
namespace
std;
#define MAX 500
int
multiply(
int
x,
int
res[],
int
res_size);
void
factorial(
int
n)
{
int
res[MAX];
res[0] = 1;
int
res_size = 1;
for
(
int
x = 2; x <= n; x++)
res_size = multiply(x, res, res_size);
cout <<
"Factorial of given number is n"
;
for
(
int
i = res_size - 1; i >= 0; i--)
cout << res[i];
}
int
multiply(
int
x,
int
res[],
int
res_size)
{
int
carry = 0;
for
(
int
i = 0; i < res_size; i++) {
int
prod = res[i] * x + carry;
res[i] = prod % 10;
carry = prod / 10;
}
while
(carry) {
res[res_size] = carry % 10;
carry = carry / 10;
res_size++;
}
return
res_size;
}
int
main()
{
factorial(100);
return
0;
}
Java
class
GFG {
static
void
factorial(
int
n)
{
int
res[] =
new
int
[
500
];
res[
0
] =
1
;
int
res_size =
1
;
for
(
int
x =
2
; x <= n; x++)
res_size = multiply(x, res, res_size);
System.out.println(
"Factorial of given number is "
);
for
(
int
i = res_size -
1
; i >=
0
; i--)
System.out.print(res[i]);
}
static
int
multiply(
int
x,
int
res[],
int
res_size)
{
int
carry =
0
;
for
(
int
i =
0
; i < res_size; i++) {
int
prod = res[i] * x + carry;
res[i] = prod %
10
;
carry = prod /
10
;
}
while
(carry !=
0
) {
res[res_size] = carry %
10
;
carry = carry /
10
;
res_size++;
}
return
res_size;
}
public
static
void
main(String args[])
{
factorial(
100
);
}
}
Python3
import
sys
def
factorial(n):
res
=
[
None
]
*
500
res[
0
]
=
1
res_size
=
1
x
=
2
while
x <
=
n:
res_size
=
multiply(x, res, res_size)
x
=
x
+
1
print
(
"Factorial of given number is"
)
i
=
res_size
-
1
while
i >
=
0
:
sys.stdout.write(
str
(res[i]))
sys.stdout.flush()
i
=
i
-
1
def
multiply(x, res, res_size):
carry
=
0
i
=
0
while
i < res_size:
prod
=
res[i]
*
x
+
carry
res[i]
=
prod
%
10
carry
=
prod
/
/
10
i
=
i
+
1
while
(carry):
res[res_size]
=
carry
%
10
carry
=
carry
/
/
10
res_size
=
res_size
+
1
return
res_size
factorial(
100
)
C#
using
System;
class
GFG {
static
void
factorial(
int
n)
{
int
[] res =
new
int
[500];
res[0] = 1;
int
res_size = 1;
for
(
int
x = 2; x <= n; x++)
res_size = multiply(x, res, res_size);
Console.WriteLine(
"Factorial of "
+
"given number is "
);
for
(
int
i = res_size - 1; i >= 0; i--)
Console.Write(res[i]);
}
static
int
multiply(
int
x,
int
[] res,
int
res_size)
{
int
carry = 0;
for
(
int
i = 0; i < res_size; i++) {
int
prod = res[i] * x + carry;
res[i] = prod % 10;
carry = prod / 10;
}
while
(carry != 0) {
res[res_size] = carry % 10;
carry = carry / 10;
res_size++;
}
return
res_size;
}
static
public
void
Main() { factorial(100); }
}
PHP
<?php
$MAX
= 500;
function
factorial(
$n
)
{
global
$MAX
;
$res
=
array_fill
(0,
$MAX
, 0);
$res
[0] = 1;
$res_size
= 1;
for
(
$x
= 2;
$x
<=
$n
;
$x
++)
$res_size
= multiply(
$x
,
$res
,
$res_size
);
echo
"Factorial of given number is n"
;
for
(
$i
=
$res_size
- 1;
$i
>= 0;
$i
--)
echo
$res
[
$i
];
}
function
multiply(
$x
, &
$res
,
$res_size
)
{
$carry
= 0;
for
(
$i
= 0;
$i
<
$res_size
;
$i
++)
{
$prod
=
$res
[
$i
] *
$x
+
$carry
;
$res
[
$i
] =
$prod
% 10;
$carry
= (int)(
$prod
/ 10);
}
while
(
$carry
)
{
$res
[
$res_size
] =
$carry
% 10;
$carry
= (int)(
$carry
/ 10);
$res_size
++;
}
return
$res_size
;
}
factorial(100);
?>
Javascript
<script>
function
factorial(n)
{
let res =
new
Array(500);
res[0] = 1;
let res_size = 1;
for
(let x=2; x<=n; x++)
res_size = multiply(x, res, res_size);
document.write(
"Factorial of given number is "
+
"<br>"
);
for
(let i=res_size-1; i>=0; i--)
document.write(res[i]);
}
function
multiply(x, res, res_size)
{
let carry = 0;
for
(let i=0; i<res_size; i++)
{
let prod = res[i] * x + carry;
res[i] = prod % 10;
carry = Math.floor(prod/10);
}
while
(carry)
{
res[res_size] = carry%10;
carry = Math.floor(carry/10);
res_size++;
}
return
res_size;
}
factorial(100);
</script>
Output
Factorial of given number is 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
Time Complexity: O(N log (N!)), where O(N) is for loop and O(log N!) is for nested while loop
Auxiliary Space: O(max(digits in factorial))
Find the Factorial of a large number using Basic BigInteger
This problem can be solved using the below idea:
Big Integer can also be used to calculate the factorial of large numbers.
Illustration:
N = 5
ans = 1
At i = 2: ans = ans x i = 1 x 2 = 2
At i = 3: ans = ans x i = 2 x 3 = 6
At i = 4: ans = ans x i = 6 x 4 = 24
At i = 5: ans = ans x i = 24 x 5 = 120Hence factorial of N is 120
Follow the steps below to solve the given problem:
- Declare a BigInteger f with 1 and perform the conventional way of calculating factorial
- Traverse a loop from x = 2 to N and multiply x with f and store the resultant value in f
Below is the implementation of the above idea :
C++
#include <bits/stdc++.h>
using
namespace
std;
#define ull unsigned long long
ull factorial(
int
N)
{
ull f = 1;
for
(ull i = 2; i <= N; i++)
f *= i;
return
f;
}
int
main()
{
int
N = 20;
cout << factorial(N) << endl;
}
Java
import
java.math.BigInteger;
import
java.util.Scanner;
public
class
Example {
static
BigInteger factorial(
int
N)
{
BigInteger f
=
new
BigInteger(
"1"
);
for
(
int
i =
2
; i <= N; i++)
f = f.multiply(BigInteger.valueOf(i));
return
f;
}
public
static
void
main(String args[])
throws
Exception
{
int
N =
20
;
System.out.println(factorial(N));
}
}
Python3
def
factorial(N):
f
=
1
for
i
in
range
(
2
, N
+
1
):
f
*
=
i
return
f;
N
=
20
;
print
(factorial(N));
C#
using
System;
using
System.Collections.Generic;
using
System.Numerics;
public
class
Example {
static
BigInteger factorial(
int
N)
{
BigInteger f
=
new
BigInteger(1);
for
(
int
i = 2; i <= N; i++)
f = BigInteger.Multiply(f,
new
BigInteger(i));
return
f;
}
public
static
void
Main(
string
[] args)
{
int
N = 20;
Console.WriteLine(factorial(N));
}
}
Javascript
function
factorial(N)
{
let f = BigInt(1);
for
(
var
i = 2; i <= N; i++)
f *= BigInt(i);
return
f;
}
let N = 20;
console.log(factorial(N));
Output
2432902008176640000
Time Complexity: O(n)
Auxiliary Space: O(1)
Method 3:
1. Define a function factorial(n) that takes an integer n as input.
2. Check if n is 0 or 1. If it is, return 1.
3. If n is greater than 1, call the function recursively with input n-1 and store the result in a variable called sub_result.
4. Multiply sub_result with n and return the product as the final result.
C++
#include <iostream>
#include <vector>
using
namespace
std;
vector<
int
> multiply(vector<
int
>& digits,
int
factor) {
int
carry = 0;
for
(
int
i = 0; i < digits.size(); i++) {
int
prod = digits[i] * factor + carry;
digits[i] = prod % 10;
carry = prod / 10;
}
while
(carry) {
digits.push_back(carry % 10);
carry /= 10;
}
return
digits;
}
void
print(vector<
int
>& digits) {
for
(
int
i = digits.size() - 1; i >= 0; i--) {
cout << digits[i];
}
}
int
main() {
int
n = 100;
vector<
int
> digits;
digits.push_back(1);
for
(
int
i = 2; i <= n; i++) {
digits = multiply(digits, i);
}
print(digits);
return
0;
}
Java
import
java.math.BigInteger;
public
class
Main {
public
static
void
main(String[] args) {
int
n =
100
;
BigInteger result = BigInteger.valueOf(
1
);
for
(
int
i =
1
; i <= n; i++) {
result = result.multiply(BigInteger.valueOf(i));
}
System.out.println(result);
}
}
Python3
import
math
n
=
100
result
=
math.factorial(n)
print
(result)
Javascript
function
factorial(n) {
let result = BigInt(1);
for
(let i = 2; i <= n; i++) {
result *= BigInt(i);
}
return
result;
}
const n = 100;
const result = factorial(n);
console.log(result);
C#
using
System;
using
System.Collections.Generic;
class
MainClass {
static
List<
int
> Multiply(List<
int
> digits,
int
factor)
{
int
carry = 0;
for
(
int
i = 0; i < digits.Count; i++) {
int
prod = digits[i] * factor + carry;
digits[i] = prod % 10;
carry = prod / 10;
}
while
(carry > 0) {
digits.Add(carry % 10);
carry /= 10;
}
return
digits;
}
static
void
Print(List<
int
> digits)
{
for
(
int
i = digits.Count - 1; i >= 0; i--) {
Console.Write(digits[i]);
}
}
static
void
Main()
{
int
n = 100;
List<
int
> digits =
new
List<
int
>();
digits.Add(1);
for
(
int
i = 2; i <= n; i++) {
digits = Multiply(digits, i);
}
Print(digits);
}
}
Output
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
Time Complexity: O(n)
The time complexity of the code is O(n), where n is the value of the input parameter. This is because the math.factorial() function internally uses a loop to calculate the product of all integers from 1 to n, and the loop iterates n times. Therefore, the time taken by the function is proportional to n.
Auxiliary Space: O(1)
The auxiliary space complexity of the code is O(1), because the code uses only a constant amount of additional memory to store the result of the factorial calculation. Specifically, the memory required to store the result is proportional to the number of digits in the result, which is roughly log10(n!), or O(log n), but this is negligible compared to the input size for large values of n.
Last Updated :
09 Apr, 2023
Like Article
Save Article
#статьи
- 19 май 2023
-
0
Что такое факториал и как его вычислить
Статья, после которой вы начнёте щёлкать факториалы как орешки.
Иллюстрация: Катя Павловская для Skillbox Media
Любитель научной фантастики и технологического прогресса. Хорошо сочетает в себе заумного технаря и утончённого гуманитария. Пишет про IT и радуется этому.
Даже если вы уже давно окончили школу, факториалы всё равно могут доставить немало приятных флешбэков — например, если вы обучаетесь программированию и знакомитесь с задачками на рекурсию или комбинаторику. Поэтому мы решили максимально просто объяснить, что такое факториал, как его вычислять и зачем он вообще нужен.
Эта статья будет полезна как опытным программистам, которые хотят освежить знания, так и тем, кто ещё учится: школьникам, студентам и совсем зелёным джунам.
Содержание:
- Что такое факториал
- Для чего он нужен
- Основные свойства и формулы
- Шпаргалка: таблица факториалов
- Решаем задачи на факториалы
- Что запомнить
Факториал числа n — это произведение всех натуральных чисел от единицы до n. Обозначается факториал символом восклицательного знака: !.
Это определение из учебника, и оно пока звучит сложновато — неясно, зачем эти факториалы вообще нужны и как они могут пригодиться в науке и технике. Но об этом чуть позже — для начала давайте посмотрим на примеры факториалов:
Чтобы вычислить их, нам нужно перемножить все числа от единицы до числа, стоящего под знаком факториала — так гласит определение. Получаем выражения:
Ещё в математическом определении сказано, что факториал не может быть отрицательным или дробным — то есть вот такие факториалы вычислить нельзя:
Факториалы незаменимы там, где нужно быстро посчитать количество комбинаций и сочетаний разных предметов. В математике этому посвящён даже целый раздел — называется комбинаторика. Её методы используют много где: от лингвистики до криптографии и анализа ДНК. И во всех этих сферах факториал помогает упрощать сложные вычисления.
Разберём на примере, как это работает.
Допустим, у вас есть пять шоколадок и вы решили раздать их пяти друзьям — каждому по одной. Задача — выяснить, сколько существует способов раздать эти шоколадки. Начинаем размышлять:
- первую шоколадку можно отдать одному из пяти друзей;
- вторую — одному из четырёх друзей, потому что один уже получил свою шоколадку;
- третью — одному из трёх, потому что двое уже наслаждаются своими шоколадками;
- четвёртую — одному из двух;
- пятую — последнему другу.
Получается, что способов раздать первую шоколадку — 5, вторую — 4, третью — 3, четвёртую — 2, а пятую — всего 1. По правилам математики, чтобы выяснить общее количество всех вариантов, нужно перемножить их между собой. Ну а кто мы такие, чтобы с этими правилами спорить?
Смотрим на выражение выше и понимаем: ведь оно идеально вписывается в определение факториала — произведение натуральных чисел от одного до n (в нашем случае n равно 5). Следовательно, это выражение можно коротко и изящно записать в виде факториала:
Выходит, что всего способов раздать пять шоколадок пяти друзьям существует 120. Вот как может выглядеть один из них:
Конечно, в жизни вам вряд ли придётся считать количество способов раздать друзьям шоколадки. Но, например, в статистике, теории вероятностей, матанализе и программировании факториалы используют сплошь и рядом. Так что, если видите себя в будущем на матмехе или, на худой конец, в IT, то лучше познакомиться с ними хотя бы бегло.
Так как факториалы используются в разных областях математики, свойств у него довольно много — каждая область привносит какие-то свои методы вычислений. Одно из свойств вы уже знаете: факториал — это всегда целое положительное число. Вот ещё несколько, которые стоит запомнить:
- Факториал нуля равен единице — 0! = 1;
- Факториал единицы тоже равен единице: 1! = 1;
- Рекурсия: n! = (n — 1)! × n. Это основное свойство факториалов, о нём мы чуть подробнее поговорим дальше.
Мы видим, что каждое свойство описывается какой-то формулой — и некоторые из этих формул могут быть весьма полезны. Они позволяют нам находить факториалы проще и быстрее, чем простым перемножением натуральных чисел. Разберём эти формулы тоже.
Чтобы вычислить факториал, не используя так много операций умножения, придумали формулу Стирлинга. Вот, как она выглядит:
Выглядит страшно, но на самом деле она очень полезная. Её используют, когда хотят приблизительно узнать факториал большого числа. Обычным способом это будет сделать сложно даже мощному компьютеру — например, попробуйте посчитать в онлайн-калькуляторе факториал числа 10 024 (спойлер: это может занять несколько часов и даже дней).
Скришнот: Контрольная работа РУ — калькуляторы онлайн / Skillbox Media
Давайте попробуем вычислить факториал числа 6 по этой формуле:
Число e примерно равно 2,71, а π — 3,14. Подставляем их в выражение и получаем ответ:
Получили приближённое значение настоящего факториала, который равен 720. Но можно сделать ответ и более точным. Для этого нужно добавить больше знаков после запятой всем переменным — например, если взять 20 знаков, то ответ будет таким:
Это уже больше похоже на правду. Хотя погрешность всё равно есть.
Рекуррентная формула позволяет вычислить факториал числа n, основываясь на факториале предыдущего числа — (n — 1). Выглядит она так:
В целом рекуррентная формула не приносит нам большой пользы, так как всё равно приходится вычислять факториал предыдущего числа. Если он равен какому-то большому числу (например, 100), то использование формулы теряет смысл — слишком уж много вычислений это потребует.
Рекуррентная формула основана на главном свойстве факториалов — рекурсии: n! = (n — 1)! × n. Это свойство особенно полезно при решении задач по комбинаторике: так мы можем быстро сокращать факториалы и упрощать выражения.
Однако рекуррентная формула хорошо подходит для алгоритмов — в частности, для программирования. Мы можем задать начальное значение: например, что 0! = 1 или 1! = 1, а затем считать следующие факториалы по формуле:
Получим алгоритм для вычисления факториалов. Не очень эффективный, но простой.
Давайте вычислим по этой формуле факториал числа 4. Сначала распишем рекуррентную формулу до базового значения — факториала числа 1:
Можно записать это и в сокращённом виде:
Теперь последовательно подставляем значение факториала, которое мы уже знаем, и вычисляем результат:
Получили ответ — 24. Ничего сложного, просто перемножаем числа.
Кстати, всю эту формулу можно обернуть в реально работающую функцию на языке Python:
def factorial(n): # определяем функцию if n == 0 or n == 1: # базовый случай return 1 else: # рекуррентный случай return factorial(n-1) * n # вызываем эту же функцию, но с меньшим аргументом print(factorial(4)) # печатаем факториал 4 # Вывод: # 24
Можете попробовать запустить её в онлайн-интерпретаторе и посмотреть, как работает. Тут есть один нюанс: Python не даст вам посчитать факториал числа больше 998, так как у него есть ограничение на количество вызовов функции — в программировании это называется глубиной рекурсии.
Чтобы быстро находить, чему равен факториал, можно запомнить или сохранить в заметки вот такую табличку. Она рассчитана всего на 12 чисел, но для большинства учебных задач этого хватит.
1! | 1 |
2! | 2 |
3! | 6 |
4! | 24 |
5! | 120 |
6! | 720 |
7! | 5040 |
8! | 40 320 |
9! | 362 880 |
10! | 3 628 800 |
11! | 39 916 800 |
12! | 479 001 600 |
С теорией вроде разобрались — теперь попробуем решить несколько задач с факториалами, чтобы закрепить знания на практике.
Задача: перемножить два факториала.
Решение:
Сперва нужно вычислить значения факториалов, а затем перемножить полученные значения:
Обратите внимание: во второй строке мы применили рекуррентную формулу, чтобы быстрее вычислить факториал числа 7.
Задача: вычесть из одного факториала другой.
Решение:
Используем тот же подход, что и в предыдущей задаче: сначала вычисляем факториалы, а затем получаем ответ на всё выражение.
Вроде бы ничего сложного, главное — не запутаться в умножении.
Задача: умножить один факториал на другой:
Решение:
Вычисляем факториалы, потом перемножаем их значения:
Во второй строке мы воспользовались таблицей выше и быстро нашли значение факториала от числа 8.
Задача: сократить дробь и вычислить её значение.
Решение:
Здесь мы воспользуемся рекуррентной формулой для вычислений факториала и разложим верхний факториал на множители:
В первой строке мы применили рекуррентную формулу два раза, а во второй — просто сократили одинаковые факториалы в числителе и в знаменателе.
Задача: сократить дробь.
Решение:
Хотя здесь нет конкретных чисел, но принцип решения остаётся таким же: используем рекуррентную формулу и сокращаем одинаковые значения в числителе и знаменателе.
Главное — не запутаться и правильно применить рекуррентную формулу.
- Факториал — это произведение всех натуральных чисел от 1 до данного числа. Например, факториал числа 5 будет равен 1 × 2 × 3 × 4 × 5 = 120.
- Его используют во многих областях науки — например, комбинаторике, теории вероятности и математическом анализе.
- Помимо стандартной формулы для вычисления факториала можно использовать формулы Стирлинга и рекуррентную формулу.
- Формула Стирлинга нужна для того, чтобы посчитать факториал без большого числа операций умножения.
- Рекуррентная формула позволяет вычислить факториал на основе предыдущего факториала.
Научитесь: Профессия Data Scientist
Узнать больше
Алгоритмы быстрого вычисления факториала
Время на прочтение
6 мин
Количество просмотров 209K
Понятие факториала известно всем. Это функция, вычисляющая произведение последовательных натуральных чисел от 1 до N включительно: N! = 1 * 2 * 3 *… * N. Факториал — быстрорастущая функция, уже для небольших значений N значение N! имеет много значащих цифр.
Попробуем реализовать эту функцию на языке программирования. Очевидно, нам понадобиться язык, поддерживающий длинную арифметику. Я воспользуюсь C#, но с таким же успехом можно взять Java или Python.
Наивный алгоритм
Итак, простейшая реализация (назовем ее наивной) получается прямо из определения факториала:
static BigInteger FactNaive(int n)
{
BigInteger r = 1;
for (int i = 2; i <= n; ++i)
r *= i;
return r;
}
На моей машине эта реализация работает примерно 1,6 секунд для N=50 000.
Далее рассмотрим алгоритмы, которые работают намного быстрее наивной реализации.
Алгоритм вычисления деревом
Первый алгоритм основан на том соображении, что длинные числа примерно одинаковой длины умножать эффективнее, чем длинное число умножать на короткое (как в наивной реализации). То есть нам нужно добиться, чтобы при вычислении факториала множители постоянно были примерно одинаковой длины.
Пусть нам нужно найти произведение последовательных чисел от L до R, обозначим его как P(L, R). Разделим интервал от L до R пополам и посчитаем P(L, R) как P(L, M) * P(M + 1, R), где M находится посередине между L и R, M = (L + R) / 2. Заметим, что множители будут примерно одинаковой длины. Аналогично разобьем P(L, M) и P(M + 1, R). Будем производить эту операцию, пока в каждом интервале останется не более двух множителей. Очевидно, что P(L, R) = L, если L и R равны, и P(L, R) = L * R, если L и R отличаются на единицу. Чтобы найти N! нужно посчитать P(2, N).
Посмотрим, как будет работать наш алгоритм для N=10, найдем P(2, 10):
P(2, 10)
P(2, 6) * P(7, 10)
( P(2, 4) * P(5, 6) ) * ( P(7, 8) * P(9, 10) )
( (P(2, 3) * P(4) ) * P(5, 6) ) * ( P(7, 8) * P(9, 10) )
( ( (2 * 3) * (4) ) * (5 * 6) ) * ( (7 * 8) * (9 * 10) )
( ( 6 * 4 ) * 30 ) * ( 56 * 90 )
( 24 * 30 ) * ( 5 040 )
720 * 5 040
3 628 800
Получается своеобразное дерево, где множители находятся в узлах, а результат получается в корне
Реализуем описанный алгоритм:
static BigInteger ProdTree(int l, int r)
{
if (l > r)
return 1;
if (l == r)
return l;
if (r - l == 1)
return (BigInteger)l * r;
int m = (l + r) / 2;
return ProdTree(l, m) * ProdTree(m + 1, r);
}
static BigInteger FactTree(int n)
{
if (n < 0)
return 0;
if (n == 0)
return 1;
if (n == 1 || n == 2)
return n;
return ProdTree(2, n);
}
Для N=50 000 факториал вычисляется за 0,9 секунд, что почти вдвое быстрее, чем в наивной реализации.
Алгоритм вычисления факторизацией
Второй алгоритм быстрого вычисления использует разложение факториала на простые множители (факторизацию). Очевидно, что в разложении N! участвуют только простые множители от 2 до N. Попробуем посчитать, сколько раз простой множитель K содержится в N!, то есть узнаем степень множителя K в разложении. Каждый K-ый член произведения 1 * 2 * 3 *… * N увеличивает показатель на единицу, то есть показатель степени будет равен N / K. Но каждый K2-ый член увеличивает степень еще на единицу, то есть показатель становится N / K + N / K2. Аналогично для K3, K4 и так далее. В итоге получим, что показатель степени при простом множителе K будет равен N / K + N / K2 + N / K3 + N / K4 +…
Для наглядности посчитаем, сколько раз двойка содержится в 10! Двойку дает каждый второй множитель (2, 4, 6, 8 и 10), всего таких множителей 10 / 2 = 5. Каждый четвертый дает четверку (22), всего таких множителей 10 / 4 = 2 (4 и 8). Каждый восьмой дает восьмерку (23), такой множитель всего один 10 / 8 = 1 (8). Шестнадцать (24) и более уже не дает ни один множитель, значит, подсчет можно завершать. Суммируя, получим, что показатель степени при двойке в разложении 10! на простые множители будет равен 10 / 2 + 10 / 4 + 10 / 8 = 5 + 2 + 1 = 8.
Если действовать таким же образом, можно найти показатели при 3, 5 и 7 в разложении 10!, после чего остается только вычислить значение произведения:
10! = 28 * 34 * 52 * 71 = 3 628 800
Осталось найти простые числа от 2 до N, для этого можно использовать решето Эратосфена:
static BigInteger FactFactor(int n)
{
if (n < 0)
return 0;
if (n == 0)
return 1;
if (n == 1 || n == 2)
return n;
bool[] u = new bool[n + 1]; // маркеры для решета Эратосфена
List<Tuple<int, int>> p = new List<Tuple<int, int>>(); // множители и их показатели степеней
for (int i = 2; i <= n; ++i)
if (!u[i]) // если i - очередное простое число
{
// считаем показатель степени в разложении
int k = n / i;
int c = 0;
while (k > 0)
{
c += k;
k /= i;
}
// запоминаем множитель и его показатель степени
p.Add(new Tuple<int, int>(i, c));
// просеиваем составные числа через решето
int j = 2;
while (i * j <= n)
{
u[i * j] = true;
++j;
}
}
// вычисляем факториал
BigInteger r = 1;
for (int i = p.Count() - 1; i >= 0; --i)
r *= BigInteger.Pow(p[i].Item1, p[i].Item2);
return r;
}
Эта реализация также тратит примерно 0,9 секунд на вычисление 50 000!
Библиотека GMP
Как справедливо отметил
pomme скорость вычисления факториала на 98% зависит от скорости умножения. Попробуем протестировать наши алгоритмы, реализовав их на C++ с использованием библиотеки GMP. Результаты тестирования приведены ниже, по ним получается что алгоритм умножения в C# имеет довольно странную асимптотику, поэтому оптимизация дает относительно небольшой выигрыш в C# и огромный в C++ с GMP. Однако этому вопросу вероятно стоит посвятить отдельную статью.
Сравнение производительности
Все алгоритмы тестировались для N равном 1 000, 2 000, 5 000, 10 000, 20 000, 50 000 и 100 000 десятью итерациями. В таблице указано среднее значение времени работы в миллисекундах.
График с линейной шкалой
График с логарифмической шкалой
Идеи и алгоритмы из комментариев
Хабражители предложили немало интересных идей и алгоритмов в ответ на мою статью, здесь я оставлю ссылки на лучшие из них
lany распараллелил дерево на Java с помощью Stream API и получил ускорение в 18 раз
Mrrl предложил оптимизацию факторизации на 15-20%
PsyHaSTe предложил улучшение наивной реализации
Krypt предложил распараллеленную версию на C#
SemenovVV предложил реализацию на Go
pomme предложил использовать GMP
ShashkovS предложил быстрый алгоритм на Python
Исходные коды
Исходные коды реализованных алгоритмов приведены под спойлерами
C#
using System;
using System.Linq;
using System.Text;
using System.Numerics;
using System.Collections.Generic;
using System.Collections.Specialized;
namespace BigInt
{
class Program
{
static BigInteger FactNaive(int n)
{
BigInteger r = 1;
for (int i = 2; i <= n; ++i)
r *= i;
return r;
}
static BigInteger ProdTree(int l, int r)
{
if (l > r)
return 1;
if (l == r)
return l;
if (r - l == 1)
return (BigInteger)l * r;
int m = (l + r) / 2;
return ProdTree(l, m) * ProdTree(m + 1, r);
}
static BigInteger FactTree(int n)
{
if (n < 0)
return 0;
if (n == 0)
return 1;
if (n == 1 || n == 2)
return n;
return ProdTree(2, n);
}
static BigInteger FactFactor(int n)
{
if (n < 0)
return 0;
if (n == 0)
return 1;
if (n == 1 || n == 2)
return n;
bool[] u = new bool[n + 1];
List<Tuple<int, int>> p = new List<Tuple<int, int>>();
for (int i = 2; i <= n; ++i)
if (!u[i])
{
int k = n / i;
int c = 0;
while (k > 0)
{
c += k;
k /= i;
}
p.Add(new Tuple<int, int>(i, c));
int j = 2;
while (i * j <= n)
{
u[i * j] = true;
++j;
}
}
BigInteger r = 1;
for (int i = p.Count() - 1; i >= 0; --i)
r *= BigInteger.Pow(p[i].Item1, p[i].Item2);
return r;
}
static void Main(string[] args)
{
int n;
int t;
Console.Write("n = ");
n = Convert.ToInt32(Console.ReadLine());
t = Environment.TickCount;
BigInteger fn = FactNaive(n);
Console.WriteLine("Naive time: {0} ms", Environment.TickCount - t);
t = Environment.TickCount;
BigInteger ft = FactTree(n);
Console.WriteLine("Tree time: {0} ms", Environment.TickCount - t);
t = Environment.TickCount;
BigInteger ff = FactFactor(n);
Console.WriteLine("Factor time: {0} ms", Environment.TickCount - t);
Console.WriteLine("Check: {0}", fn == ft && ft == ff ? "ok" : "fail");
}
}
}
C++ с GMP
#include <iostream>
#include <ctime>
#include <vector>
#include <utility>
#include <gmpxx.h>
using namespace std;
mpz_class FactNaive(int n)
{
mpz_class r = 1;
for (int i = 2; i <= n; ++i)
r *= i;
return r;
}
mpz_class ProdTree(int l, int r)
{
if (l > r)
return 1;
if (l == r)
return l;
if (r - l == 1)
return (mpz_class)r * l;
int m = (l + r) / 2;
return ProdTree(l, m) * ProdTree(m + 1, r);
}
mpz_class FactTree(int n)
{
if (n < 0)
return 0;
if (n == 0)
return 1;
if (n == 1 || n == 2)
return n;
return ProdTree(2, n);
}
mpz_class FactFactor(int n)
{
if (n < 0)
return 0;
if (n == 0)
return 1;
if (n == 1 || n == 2)
return n;
vector<bool> u(n + 1, false);
vector<pair<int, int> > p;
for (int i = 2; i <= n; ++i)
if (!u[i])
{
int k = n / i;
int c = 0;
while (k > 0)
{
c += k;
k /= i;
}
p.push_back(make_pair(i, c));
int j = 2;
while (i * j <= n)
{
u[i * j] = true;
++j;
}
}
mpz_class r = 1;
for (int i = p.size() - 1; i >= 0; --i)
{
mpz_class w;
mpz_pow_ui(w.get_mpz_t(), mpz_class(p[i].first).get_mpz_t(), p[i].second);
r *= w;
}
return r;
}
mpz_class FactNative(int n)
{
mpz_class r;
mpz_fac_ui(r.get_mpz_t(), n);
return r;
}
int main()
{
int n;
unsigned int t;
cout << "n = ";
cin >> n;
t = clock();
mpz_class fn = FactNaive(n);
cout << "Naive: " << (clock() - t) * 1000 / CLOCKS_PER_SEC << " ms" << endl;
t = clock();
mpz_class ft = FactTree(n);
cout << "Tree: " << (clock() - t) * 1000 / CLOCKS_PER_SEC << " ms" << endl;
t = clock();
mpz_class ff = FactFactor(n);
cout << "Factor: " << (clock() - t) * 1000 / CLOCKS_PER_SEC << " ms" << endl;
t = clock();
mpz_class fz = FactNative(n);
cout << "Native: " << (clock() - t) * 1000 / CLOCKS_PER_SEC << " ms" << endl;
cout << "Check: " << (fn == ft && ft == ff && ff == fz ? "ok" : "fail") << endl;
return 0;
}
Как вариант, можно использовать длинную арифметику (т.е. мы наше число-результат храним не в каком-то типе long или int, а просто, например, его цифры в десятичной записи в качестве ячеек некоторого массива, который представляет число). О реализации такого метода можно почитать тут: Длинная арифметика
Длинная арифметика реализована на Java в классе BigInteger в пакете Math (если не ошибаюсь). В других языках она тоже есть, как ответил @ReinRaus
Другой вариант – считать ответ по модулю (т.е. выдать остаток от деления факториала на некоторое число). Тогда, каждый раз домножая на определённое число при подсчёте факториала нужно пользоваться формулой. Так мы не допустим переполнение типа.
Если нужно выдать только, например, 8 последних цифр факториала, то можно искать остаток от деления на 10^8.