순진 곱셈보다 효율적인 요인 알고리즘


37

반복 및 재귀를 모두 사용하여 계승을 코딩하는 방법을 알고 있습니다 (예 n * factorial(n-1): 예). 나는 팩토리얼을 재귀 적으로 반으로 나누어 더 효율적인 코딩 방법이 있음을 교과서에서 (추가 설명없이) 읽었습니다.

왜 그런지 이해합니다. 그러나 나는 스스로 코딩을 시도하고 싶었지만 시작해야 할 곳을 모른다고 생각합니다. 친구가 기본 사례를 먼저 쓰라고 제안했습니다. 배열을 사용하여 숫자를 추적 할 수 있다고 생각했지만 ... 그런 코드를 설계하는 방법을 실제로 볼 수는 없습니다.

어떤 종류의 기술을 연구해야합니까?

답변:


39

알려진 가장 좋은 알고리즘은 계승을 주요한 힘의 곱으로 표현하는 것입니다. 체 접근법을 사용하여 각 프라임에 대한 올바른 힘뿐만 아니라 프라임을 신속하게 결정할 수 있습니다. 반복되는 제곱을 사용하여 각 전력을 효율적으로 계산 한 다음 계수를 곱할 수 있습니다. 이것은 Peter B. Borwein, 계승 계산의 복잡성 , Journal of Algorithms 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 1 ) . n = 2 k k >n?135(2n1)(2n)!=123(2n)

(2n)!=n!2n357(2n1).
n=2kN ( 2 K ) ! = ( 2 k - 1 ) ! 2 2 k - 1 ( 2 k - 1 ) ? ( 2 k ) ! = ( 2 2 k 1 + 2 k 2 + + 2 0 ) k 1 i = 0 ( 2 i )k>0n(2k)!=(2k1)!22k1(2k1)?( 2 K - 1 ) ? ( K - 2 ) + 2 K - 1 - 2 2 2 K - 2 2 2 K - 1
(2k)!=(22k1+2k2++20)i=0k1(2i)?=(22k1)i=1k1(2i)?.
(2k1)?각 단계에서 부분 곱을 곱하면 곱셈이 필요합니다. 이것은 정의를 사용하여 곱셈 에서 거의 의 요소를 개선 한 것입니다 . 의 거듭 제곱을 계산하려면 몇 가지 추가 작업이 필요 하지만 이진 산술에서는이 값을 저렴하게 수행 할 수 있습니다 (정확히 필요한 항목에 따라 접미사 0 만 추가하면 됨 ).(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 % 정도

약간의 작업으로 을 의 거듭 제곱으로 삼아야하는 요구 사항을 제거하면서 더 개선 할 수 있습니다 ( 광범위한 설명 참조 ).2n2


중요한 요소를 제외했습니다. Borwein의 논문에 따른 계산 시간은 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

그러나 곱셈 순서는 중요합니다. 기계 정수의 곱셈은 정수 값에 관계없이 동일한 시간이 걸리는 기본 연산입니다. 그러나, 임의의 크기의 정수 위해 걸리는 시간을 곱할 하고 , B는 의 크기에 따라 및 B : 순진한 알고리즘은 시간에서 동작 (여기서, 는 IS 의 자릿수 결과가) 곱셈 상수 같은까지 그대로 당신이 원하는대로베이스 -. 있습니다 빠른 곱셈 알고리즘 만이 명백한의 하한Θ(|a||b|)|x|xΩ(|a|+|b|)max(|a|,|b|)

이 배경으로 무장 한 Wikipedia 기사 가 의미가 있습니다.

곱셈의 곱셈은 곱셈되는 정수의 크기에 따라 달라 지므로 곱셈을 작게 유지하는 순서로 곱셈을 정렬하여 시간을 절약 할 수 있습니다. 숫자가 대략 같은 크기로 정렬되면 더 잘 작동합니다. 교과서에서 언급하는“반으로 나누기” 는 (다수) 정수 세트를 곱하는 다음과 같은 분할 및 정복 방식으로 구성됩니다 .

  1. 곱이 거의 같은 두 세트로 곱할 수 (초기에는 에서 까지의 모든 정수)를 배열합니다 . 이것은 곱셈보다 훨씬 저렴합니다.(한 대의 기계 추가).1n|ab||a|+|b|
  2. 두 서브 세트 각각에 재귀 적으로 알고리즘을 적용하십시오.
  3. 두 중간 결과를 곱하십시오.

자세한 내용은 GMP 설명서 를 참조하십시오 .

요인 부터 까지를 재정렬 할 뿐만 아니라 숫자를 소수로 분해하고 결과적으로 매우 작은 정수로 구성된 매우 긴 결과를 재정렬하여 숫자를 나누는 더 빠른 방법이 있습니다. Peter Borwein의 "계산 계산의 복잡성"Peter Luschny의 구현 에서 Wikipedia 기사의 참고 문헌을 인용 하겠습니다 .1n

¹ 근사값계산하는 더 빠른 방법이 있습니다그러나 그것은 더 이상 계승을 계산하는 것이 아니라 근사치를 계산하는 것입니다.n!


9

계승 함수가 너무 빨리 커지므로 컴퓨터는 만 저장할 수 있습니다비교적 작은 . 예를 들어, double 은 최대 값을 저장할 수 있습니다. 따라서 계산을위한 정말 빠른 알고리즘을 원한다면 크기의 테이블을 사용하십시오 .n!n171!n!171

또는 함수 (또는 )에 관심이 있다면 질문이 더 흥미로워집니다 . 이 모든 경우 ( 포함 ), 나는 교과서의 주석을 실제로 이해하지 못합니다.Γ 로그 Γ N !log(n!)ΓlogΓn!

또한 꼬리 재귀를 사용하므로 반복 및 재귀 알고리즘은 부동 소수점 오류까지 동일합니다.


"반복적이고 재귀적인 알고리즘은 동일하다"라는 점은 복잡하지 않은 복잡성을 의미 하는가? 교과서의 의견은 다른 언어로 번역하고 있으므로 번역이 어려울 수 있습니다.
user65165

이 책은 반복 및 재귀에 대해 이야기하고 나누기와 정복을 사용하여 n을 나누는 방법에 대해 언급합니다! 반 당신은 ... 빠른 방법 솔루션을 얻을 수 있습니다
user65165

1
동등성에 대한 나의 개념은 완전히 공식적인 것은 아니지만 수행되는 산술 연산이 동일하다고 말할 수 있습니다 (재귀 알고리즘에서 피연산자의 순서를 전환하는 경우). "내재적으로"다른 알고리즘은 "트릭"을 사용하여 다른 계산을 수행합니다.
Yuval Filmus

1
곱셈의 복잡성에서 정수 크기를 매개 변수로 고려하면 산술 연산이 "동일"하더라도 전체 복잡성이 변경 될 수 있습니다.
Tpecatte

1
@CharlesOkwuagwu 맞아요, 당신은 테이블을 사용할 수 있습니다.
Yuval Filmus
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.