web-dev-qa-db-ja.com

大きなn&kの二項係数(nCk)の計算

私はこの質問を見たばかりで、それを解決する方法がわかりません。アルゴリズム、C++コード、またはアイデアを教えていただけますか?

これは非常に単純な問題です。 NとKの値が与えられた場合、二項係数C(N、K)の値を指定する必要があります。 K <= Nであり、Nの最大値は1,000,000,000,000,000ですのでご安心ください。値が非常に大きい可能性があるため、1009を法として結果を計算する必要があります。入力

入力の最初の行には、最大1000個のテストケースTが含まれます。次のT行はそれぞれ、スペースで区切られた2つの整数NとKで構成されます。ここで、0 <= K <= Nおよび1 <= N <= 1,000,000,000,000,000 。出力

テストケースごとに、1009を法とする二項係数C(N、K)の値を新しい行に出力します。例

入力:
3
3 1
5 2
10 3

出力:
3
10
120

19
user182513

1009が素数であることに注意してください。

これで、 リュカの定理 を使用できます。

どの州:

Let p be a prime.
If n  = a1a2...ar when written in base p and
if k  = b1b2...br when written in base p

(pad with zeroes if required)

Then

(n choose k) modulo p = (a1 choose b1) * (a2 choose  b2) * ... * (ar choose br) modulo p.

i.e. remainder of n choose k when divided by p is same as the remainder of
the product (a1 choose b1) * .... * (ar choose br) when divided by p.
Note: if bi > ai then ai choose bi is 0.

したがって、問題は、最大でlog N/log 1009の数(底1009のNの桁数)の1009を法とする積を見つけることになります。ここで、a <= 109およびb <= 1009のbを選択します。

これにより、Nが10 ^ 15に近い場合でも簡単になります。

注意:

N = 10 ^ 15の場合、NはN/2が2 ^(100000000000000)を超えることを選択します。これは、unsigned longlongをはるかに超えています。

また、ルーカスの定理によって提案されたアルゴリズムはO(log N)であり、二項係数を直接計算しようとするよりもexponentially高速です(オーバーフローの問題を処理するためにmod 1009を実行した場合でも)。

これは私がずっと前に書いたBinomialのコードです。あなたがする必要があるのは、1009を法とする演算を実行するようにそれを変更することです(バグがあるかもしれず、必ずしも推奨されるコーディングスタイルではありません):

class Binomial
{
public:
    Binomial(int Max)
    {
        max = Max+1;
        table = new unsigned int * [max]();
        for (int i=0; i < max; i++)
        {
            table[i] = new unsigned int[max]();

            for (int j = 0; j < max; j++)
            {
                table[i][j] = 0;
            }
        }
    }

    ~Binomial()
    {
        for (int i =0; i < max; i++)
        {
            delete table[i];
        }
        delete table;
    }

    unsigned int Choose(unsigned int n, unsigned int k);

private:
    bool Contains(unsigned int n, unsigned int k);

    int max;
    unsigned int **table;
};

unsigned int Binomial::Choose(unsigned int n, unsigned int k)
{
    if (n < k) return 0;
    if (k == 0 || n==1 ) return 1;
    if (n==2 && k==1) return 2;
    if (n==2 && k==2) return 1;
    if (n==k) return 1;


    if (Contains(n,k))
    {
        return table[n][k];
    }
    table[n][k] = Choose(n-1,k) + Choose(n-1,k-1);
    return table[n][k];
}

bool Binomial::Contains(unsigned int n, unsigned int k)
{
    if (table[n][k] == 0) 
    {
        return false;
    }
    return true;
}
27
Aryabhatta

二項係数は、1つの階乗を他の2つで割ったものですが、下部の_k!_項は明らかな方法でキャンセルされます。

1009(その倍数を含む)が分母よりも分子に何度も現れる場合、答えmod 1009は0です。分子よりも分母に現れる回数は多くありません(二項係数は整数であるため)。したがって、何かをしなければならない唯一のケースは、両方で同じ回数表示される場合です。 (1009)^ 2の倍数を2として数えることを忘れないでください。

その後、いくつかのテストなしではわかりませんが、あなたは小さなケース(乗算/除算する値の数が少ないことを意味します)をモップアップしているだけだと思います。プラス側では1009が素数であるため、1009を法とする算術演算がフィールドで行われます。つまり、1009の倍数を上と下の両方からキャストした後、残りの乗算と除算のmod1009を任意の順序で実行できます。

小さいケース以外が残っている場合でも、連続する整数の長い実行を乗算する必要があります。これは、1008! (mod 1009)を知ることで簡略化できます。 1 ... 1008はp上の素数体の_p-1_非ゼロ要素であるため、-1(必要に応じて1008)です。したがって、それらは1、-1、そして_(p-3)/2_の逆数のペアで構成されます。

たとえば、C((1009 ^ 3)、200)の場合を考えてみましょう。

1009の数が等しいと想像してください(私は調べるための式をコーディングしていないので、それらが等しいかどうかはわかりません)。したがって、これは作業が必要な場合です。

上部には201 ... 1008があり、事前に計算されたテーブルで計算または検索する必要があります。次に1009、次に1010 ... 2017、2018、2019 ... 3026、3027などです。 ..範囲はすべて-1なので、そのような範囲がいくつあるかを知る必要があります。

これで1009、2018、3027が残ります。これを下から1009でキャンセルすると、1、2、3、... 1008、1010、...に加えて、1009 ^ 2の倍数になります。キャンセルして、乗算する連続した整数を残します。

下の部分と非常によく似た方法で、「1 ... 1009 ^ 3-200、1009のすべての累乗を分割した」の積mod1009を計算できます。それは私たちに素数の分野での分裂を残します。 IIRCは原則としてトリッキーですが、1009は、1000個(テストケース数の上限)を管理できるほど小さい数です。

もちろん、k = 200の場合、非常に大きな重複があり、より直接的にキャンセルできます。それが私が小さなケースと非小さなケースで意味したことです。実際、_((1009^3-199) * ... * 1009^3) / 200!_を計算することで、これを「ブルートフォース」するだけで回避できるのに、私はそれを非小さなケースのように扱いました。

8
Steve Jessop

C(n、k)を計算してからmod 1009を減らしたいとは思いません。最大のものであるC(1e15,5e14)には、1e16ビット〜1000テラバイトのようなものが必要です。

さらに、snakilesの回答でループを1e15回実行すると、時間がかかるようです。あなたが使うかもしれないものは、

n = n0 + n1 * p + n2 * p ^ 2 ... + nd * p ^ d

m = m0 + m1 * p + m2 * p ^ 2 ... + md * p ^ d

(ここで、0 <= mi、ni <p)

次に、C(n、m)= C(n0、m0)* C(n1、m1)* ... * C(nd、nd)mod p

たとえば、 http://www.cecm.sfu.ca/organics/papers/granville/paper/binomial/html/binomial.html を参照してください。

1つの方法は、パスカルの三角形を使用して、0 <= m <= n <= 1009のすべてのC(m、n)のテーブルを作成することです。

5
dmuir

nCkを計算するための疑似コード:

result = 1    
for i=1 to min{K,N-K}:
   result *= N-i+1
   result /= i
return result

時間計算量:O(min {K、N-K})

ループはfrom i=1 to min{K,N-K} の代わりに from i=1 to K、それは大丈夫です。

C(k,n) = C(k, n-k)

また、GammaLn関数を使用すると、さらに効率的に計算できます。

nCk = exp(GammaLn(n+1)-GammaLn(k+1)-GammaLn(n-k+1))

GammaLn関数は、 ガンマ関数 の自然対数です。 GammaLn関数を計算するための効率的なアルゴリズムがあることは知っていますが、そのアルゴリズムは決して簡単ではありません。

2
snakile