51

元の問題へのリンク

宿題の質問ではありません。誰かがこの問題の本当の解決策を知っているかもしれないと思っただけです。

私は 2004 年にプログラミング コンテストに参加していましたが、次の問題がありました。

n が与えられたとき、n! の桁の合計を求めます。n は 0 ~ 10000 です。制限時間: 1 秒。テストセットごとに最大100個の数字があったと思います。

私のソリューションはかなり高速でしたが、十分に高速ではなかったので、しばらく実行させました。コードで使用できる事前計算された値の配列を作成しました。それはハックでしたが、うまくいきました。

しかし、約10行のコードでこの問題を解決した男がいて、すぐに答えが得られました。ある種の動的計画法か、数論の何かだったと思います。当時私たちは 16 歳だったので、「ロケット科学」であってはなりません。

彼が使用できるアルゴリズムの種類を知っている人はいますか?

編集:質問を明確にしなかった場合は申し訳ありません。mquander が言ったように、bugnum を使用せず、単純な Pascal コード、いくつかのループ、O(n 2 ) などを使用する賢い解決策があるはずです。1秒はもはや制約ではありません。

ここで、n > 5 の場合、階乗の桁数の合計が 9 で除算されることがわかりました。また、数字の末尾にあるゼロの数もわかります。それを使えますか?

わかりました、ロシアからのプログラミング コンテストからの別の問題。1 <= N <= 2 000 000 000 の場合、出力 N! mod (N+1)。なんか関係ある?

4

10 に答える 10

31

誰がまだこのスレッドに注意を払っているのかわかりませんが、とにかくここに行きます.

まず、公式に見えるリンクされたバージョンでは、10000 階乗ではなく、1000 階乗である必要があります。また、この問題が別のプログラミングコンテストで再利用されたとき、制限時間は 1 秒ではなく 3 秒でした。これにより、十分な速さで解決策を得るためにどれだけ努力しなければならないかが大きく異なります。

第 2 に、コンテストの実際のパラメータについては、Peter のソリューションは適切ですが、さらに 1 つのひねりを加えると、32 ビット アーキテクチャで 5 倍高速化できます。(または、1000 だけが必要な場合は 6 の因数でさえも!) つまり、個々の数字を処理する代わりに、基数 100000 で乗算を実装します。次に、最後に、各スーパー ディジット内の数字を合計します。コンテストでどれだけ優れたコンピュータを使用できたかはわかりませんが、自宅にはコンテストとほぼ同じくらい古いデスクトップがあります。次のサンプル コードでは、1000 に 16 ミリ秒かかります。10000 なら 2.15 秒!このコードでは、後続の 0 が表示されても無視されますが、節約できるのは作業の約 7% だけです。

#include <stdio.h>
int main() {
    unsigned int dig[10000], first=0, last=0, carry, n, x, sum=0;
    dig[0] = 1;    
    for(n=2; n <= 9999; n++) {
        carry = 0;
        for(x=first; x <= last; x++) {
            carry = dig[x]*n + carry;
            dig[x] = carry%100000;
            if(x == first && !(carry%100000)) first++;
            carry /= 100000; }
        if(carry) dig[++last] = carry; }
    for(x=first; x <= last; x++)
        sum += dig[x]%10 + (dig[x]/10)%10 + (dig[x]/100)%10 + (dig[x]/1000)%10
            + (dig[x]/10000)%10;
    printf("Sum: %d\n",sum); }

3 番目に、別のかなりの係数で計算を高速化する、驚くほど簡単な方法があります。大きな数を乗算する最新の方法では、n! の計算に 2 次時間はかかりません。代わりに、O-tilde(n) 時間で実行できます。ここで、チルダは、対数係数を投入できることを意味します。カラツバによる単純な加速がありますが、時間の複雑さはそれほど低下しませんが、それでも改善され、さらに 4 倍ほど節約できます。それを使用するには、階乗自体を同じサイズの範囲に分割する必要もあります。k から n までの数値を擬似コード式で乗算する再帰アルゴリズム prod(k,n) を作成します。

prod(k,n) = prod(k,floor((k+n)/2))*prod(floor((k+n)/2)+1,n)

次に、カラツバを使用して、結果として得られる大きな乗算を行います。

カラツバよりさらに優れているのは、フーリエ変換ベースのシェーンハーゲ シュトラッセン乗算アルゴリズムです。たまたま、どちらのアルゴリズムも最新の大きな数のライブラリの一部です。巨大な階乗を迅速に計算することは、特定の純粋数学アプリケーションにとって重要になる場合があります。Schonhage-Strassen は、プログラミング コンテストとしてはやり過ぎだと思います。カラツバは非常にシンプルで、問題に対する A+ ソリューションとして想像できます。


提起された質問の一部は、コンテストの問題を完全に変える単純な数論のトリックがあるという推測です。たとえば、質問が n! を決定する場合。mod n+1 の場合、ウィルソンの定理によれば、n+1 が素数の場合、答えは -1 であり、n=3 の場合は 2 であり、n+1 が合成の場合は 0 であることを確認するのは非常に簡単です。これにもバリエーションがあります。たとえば、n! mod 2n+1 も非常に予測可能です。合同と数字の合計の間にもいくつかの接続があります。x mod 9 の桁の合計も x mod 9 です。これが、x = n の場合、合計が 0 mod 9 になる理由です。n >= 6 の場合。x mod 11 の数字の交互の合計は、x mod 11 に等しくなります。

問題は、モジュロではなく、大きな数の桁の合計が必要な場合、数論のトリックはすぐに使い果たされることです。数字の桁の足し算は、繰り上げのある足し算や掛け算とうまく噛み合いません。高速なアルゴリズムに数学が存在しないと約束するのは難しいことがよくありますが、この場合、既知の公式はないと思います。たとえば、グーゴルの階乗の桁数の合計は、およそ 100 桁の数にすぎませんが、誰も知らないに違いありません。

于 2009-12-09T18:00:47.150 に答える
8

これはオンライン整数列百科事典のA004152です。残念ながら、それを効率的に計算する方法についての有用なヒントはありません。その maple と mathematica のレシピは単純なアプローチをとっています。

于 2009-09-24T07:43:38.883 に答える
6

N を計算するために、2 番目の問題に取り組みます。mod (N+1)、ウィルソンの定理を使用。これにより、問題は N が素数かどうかのテストに軽減されます。

于 2009-09-24T14:30:24.757 に答える
3

http://www.penjuinlabs.com/blog/?p=44にある小さくて高速な python スクリプト。それはエレガントですが、まだ力ずくです。

import sys
for arg in sys.argv[1:]:
    print reduce( lambda x,y: int(x)+int(y), 
          str( reduce( lambda x, y: x*y, range(1,int(arg)))))

 

$ time python sumoffactorialdigits.py 432 951 5436 606 14 9520
3798
9639
74484
5742
27
141651

real    0m1.252s
user    0m1.108s
sys     0m0.062s
于 2009-09-24T05:09:00.303 に答える
3

大きな数があると仮定して (N が 10000 ではなく、本当に大きいと仮定すると、これは問題の中で最小のものです)、そこから続けましょう。

以下のトリックは N を因数分解することです! すべての n<=N を因数分解し、因数の累乗を計算します。

カウンターのベクトルを持っています。N までの素数ごとに 1 つのカウンター。それらを 0 に設定します。n<= N ごとに、n を因数分解し、それに応じて素因数のカウンターを増やします (スマートに因数分解します。小さな素数から始め、因数分解しながら素数を構成し、2 による除算はシフトであることを思い出してください)。カウンターの 2 からカウンターの 5 を引き、カウンターの 5 をゼロにします (ここでは 10 の因数は誰も気にしません)。

N までのすべての素数を計算し、次のループを実行します

for (j = 0; j< last_prime; ++j) {
  count[j] = 0;
  for (i = N/ primes[j]; i; i /= primes[j])
    count[j] += i; 
}

前のブロックでは (非常に) 小さい数のみを使用したことに注意してください。

素因数 P ごとに、P を適切なカウンターのべき乗で計算する必要があります。これには、反復二乗を使用して log(counter) 時間がかかります。ここで、これらすべての素数の累乗を掛ける必要があります。

全体として、小さな数 (log N の素因数) に対して約 N log(N) 操作があり、大きな数に対して Log N Log(Log N) 操作があります。

編集の改善後、少数の N 操作のみ。

HTH

于 2009-09-24T14:01:25.930 に答える
2

1秒?n を計算できないのはなぜですか。数字を合計しますか?これは 10000 回の乗算であり、加算は数万回にすぎず、約 10 億分の 1 秒かかります。

于 2009-09-24T02:57:55.133 に答える
1

Fatcorial を計算する必要があります。

1 * 2 * 3 * 4 * 5 = 120。

桁の合計のみを計算する場合は、末尾のゼロを無視できます。

6のために!120 * 6 の代わりに 12 x 6 = 72 を実行できます

7のために!(72 * 7)MOD 10を使用できます

編集。

返事を書くのが早すぎた…

10 は 2 つの素数 2 と 5 の結果です。

これらの 2 つの要因があるたびに、それらを無視できます。

1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * 11 * 12 * 13 * 14 * 15...

1   2   3   2   5   2   7   2   3    2   11    2   13    2    3
            2       3       2   3    5         2         7    5
                            2                  3

係数 5 は 5、10、15... の位置に表示されます。
次に、5、10、15 を掛けた後に末尾のゼロが表示されます。

2 と 3 がたくさんあります...すぐにオーバーフローします :-(

次に、大きな数のライブラリが必要です。

私は反対票を投じられるに値する!

于 2009-09-25T16:56:49.020 に答える
0

任意精度の整数がなくても、これはブルートフォース可能である必要があります。リンクした問題ステートメントでは、計算する必要がある最大の階乗は1000!です。これは約2500桁の数字です。だからこれを行うだけです:

  1. 3000バイトの配列を割り当て、各バイトは階乗の1桁を表します。値1から始めます。
  2. 階乗を計算するために、配列に対して小学校の乗算を繰り返し実行します。
  3. 数字を合計します。

繰り返し乗算を行うことは、唯一の潜在的に遅いステップですが、1000回の乗算が1秒間に実行できると確信しています。これは最悪のケースです。そうでない場合は、事前にいくつかの「マイルストーン」値を計算して、それらをプログラムに貼り付けることができます。

考えられる最適化の1つ:末尾のゼロが表示されたときに配列から削除します。それらは答えに影響を与えません。

明らかな注意:私はここでプログラミングと競争のアプローチを取っています。あなたはおそらくこれを専門的な仕事で行うことは決してないでしょう。

于 2009-10-15T18:04:58.100 に答える
0

どれどれ。n! の計算は知っています。合理的に大きな数値は、最終的に合計に寄与しない多くの末尾ゼロを持つ数値につながります。途中でゼロを切り落とすのはどうですか?それは数字のサイザーを少し小さく保ちますか?

うーん。いいえ。チェックしたところ、整数オーバーフローは依然として大きな問題です...

于 2009-09-25T16:44:03.850 に答える
-1

BigInteger を使用した別のソリューション

 static long q20(){
    long sum = 0;
    String factorial = factorial(new BigInteger("100")).toString();
    for(int i=0;i<factorial.length();i++){
        sum += Long.parseLong(factorial.charAt(i)+"");
    }
    return sum;
}
static BigInteger factorial(BigInteger n){
    BigInteger one = new BigInteger("1");
    if(n.equals(one)) return one;
    return n.multiply(factorial(n.subtract(one)));
}
于 2015-08-14T22:38:49.443 に答える