素因数乗算よりも効率的な階乗アルゴリズム


38

繰り返しと再帰の両方を使用して階乗をコーディングする方法を知っています(n * factorial(n-1)たとえば)。教科書を読んで(詳細な説明はありません)、階乗を半分に再帰的に分割することで、階乗をコーディングするさらに効率的な方法があることを読みました。

なぜそうなるのか理解しています。しかし、私は自分でそれをコーディングしてみたかったのですが、どこから始めればいいのかわかりません。友人は、私が最初にベースケースを書くよう提案しました。数値を追跡できるように配列を使用することを考えていましたが、そのようなコードを設計する方法が実際にはわかりません。

どのようなテクニックを研究すべきですか?

回答:


40

知られている最良のアルゴリズムは、階乗を素数の累乗の積として表現することです。ふるいアプローチを使用して、素数と各素数の適切なパワーをすばやく決定できます。各電力の計算は、二乗の繰り返しを使用して効率的に実行でき、その後、係数が乗算されます。これは、Peter B. Borweinによる、階乗計算の複雑性について、Journal of Algorithms 6 376–380、1985 によって説明されました。(PDF)要するに、定義を使用するときに必要な時間と比較して、時間で計算できます。n!O(n(logn)3loglogn)Ω(n2logn)

教科書がおそらく意味するのは、分割統治法でしょう。積の通常のパターンを使用することにより、乗算を減らすことができます。n1

してみましょう示すの便利な表記として。要因を並べ替え as ここで、整数に対してと仮定します。(これは、以下の説明での複雑さを避けるための便利な仮定であり、一般的な拡張することができます。)その後そして、この繰り返しを拡大することにより、 コンピューティングn?135(2n1)(2n)!=123(2n)

(2n)!=n!2n357(2n1).
n=2kk>0n(2k)!=(2k1)!22k1(2k1)?
(2k)!=(22k1+2k2++20)i=0k1(2i)?=(22k1)i=1k1(2i)?.
(2k1)?各段階で部分積を乗算するには、乗算が必要です。これは、定義を使用するだけで乗算からほぼ倍に改善されます。のべき乗を計算するにはいくつかの追加の操作が必要ですが、バイナリ算術ではこれを安価に行うことができます(正確に必要なものに応じて、ゼロのサフィックスを追加するだけでよい場合があります)。(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%でした。

少しの作業で、これはさらに改善され、がべき乗であるという要件もなくなります(詳細な説明を参照)。n2


重要な要素を省略しました。Borweinの論文による計算時間はO(n log n log log n)ではありません。O(M(n log n)log log n)です。ここで、M(n log n)は、サイズn log nの2つの数値を乗算する時間です。
gnasher729

18

階乗関数は非常に速く成長するため、単純な手法よりも効率的な手法の利点を得るには、任意のサイズの整数が必要になることに注意してください。21の階乗はすでに大きすぎて64ビットに収まりませんunsigned long long int

私の知る限り、を計算するアルゴリズムはありません(階乗)これは、乗算を行うよりも高速です。¹n!n

ただし、乗算を行う順序は重要です。マシン整数での乗算は、整数の値に関係なく同じ時間がかかる基本的な操作です。しかし、任意のサイズの整数、それが増殖するのにかかる時間とbはのサイズに依存し、B:ナイーブアルゴリズムは、動作時にここで、ですの桁数—任意の基数で、結果は乗法定数まで同じです)。あります速く乗算アルゴリズムが、そこで明らかの下限Θ(|a||b|)|x|xΩ(|a|+|b|)乗算は少なくともすべての数字を読み取る必要があるためです。すべての既知の乗算アルゴリズムは、線形よりも速く成長します。max(|a|,|b|)

この背景で武装したウィキペディアの記事は理にかなっているはずです。

乗算の複雑さは、乗算される整数のサイズに依存するため、乗算される数値を小さく保つ順序で乗算を配置することにより、時間を節約できます。数字がほぼ同じサイズになるように調整するとうまくいきます。教科書が指す「半分の分割」は、整数の(複数の)セットを乗算する次の分割統治アプローチで構成されています。

  1. 乗算する数値(最初はからまでのすべての整数)を、積がほぼ同じサイズの2つのセットに配置します。これは、乗算を行うよりもはるかに安価です:(1台のマシンの追加)。1n|ab||a|+|b|
  2. 2つのサブセットのそれぞれにアルゴリズムを再帰的に適用します。
  3. 2つの中間結果を乗算します。

詳細については、GMPのマニュアルを参照してください。

因子から再配置するだけでなく、それらを素因数分解に分解し、結果として生じる非常に長い整数の非常に長い積を再配置することにより、数値を分割するさらに高速な方法があります。Wikipediaの記事「Peter Borweinによる「階乗の計算の複雑さ」」およびPeter Luschnyによる実装からの参照を引用します。1n

¹nの近似値より高速に計算する方法があります、それは階乗の計算ではなく、近似の計算です。n!


9

階乗関数は非常に急速に成長するため、コンピューターは比較的小さい。たとえば、doubleはまでの値を格納できます。したがって、を計算するための非常に高速なアルゴリズムが必要な場合、サイズテーブルを使用します。n!n171!n!171

または関数(または)に興味がある場合、質問はより興味深いものになり。これらすべてのケース(を含む)で、教科書のコメントを本当に理解していません。log(n!)ΓlogΓn!

余談ですが、末尾再帰を使用しているため、反復アルゴリズムと再帰アルゴリズムは同等です(浮動小数点エラーまで)。


「反復アルゴリズムと再帰アルゴリズムは同等です」というのは、漸近的な複雑さを指しているのですか?教科書のコメントについては、他の言語から翻訳しているので、翻訳がひどいかもしれません。
user65165

この本は、反復的で再帰的であり、nを分割するために分割と征服を使用する方法についてコメントしています!半分にあなたは...道より高速なソリューションを得ることができます
user65165

1
私の等価性の概念は完全に公式ではありませんが、実行される算術演算は同じであると言えます(再帰アルゴリズムでオペランドの順序を切り替えた場合)。「本質的に」異なるアルゴリズムは、おそらく「トリック」を使用して、異なる計算を実行します。
ユヴァルフィルマス

1
整数のサイズを乗算の複雑さのパラメーターと見なすと、算術演算が「同じ」であっても全体的な複雑さが変わる可能性があります。
Tpecatte

1
@CharlesOkwuagwuそうですね、テーブルを使用できます。
ユヴァルフィルマス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.