パスカルの三角形による二項係数(nCk)の計算

2020年3月23日数学二項係数,動的計画法,パスカルの三角形

以下のような「上の2つを足して下の数字をつくる三角形」をパスカルの三角形といい、上から \(n\) 行目・左から \(k\) 個目の数は、\(_{n}\mathrm{C}_{k}\) に対応しています。(0-indexed)

\begin{gather}
1 \\
1 ~~~~ 1 \\
1 ~~~~ 2 ~~~~ 1 \\
1 ~~~~ 3 ~~~~ 3 ~~~~ 1 \\
1 ~~~~4 ~~~~ 6 ~~~~ 4 ~~~~ 1 \\
1 ~~~~ 5 ~~~~ 10 ~~~~ 10 ~~~~ 5 ~~~~ 1
\end{gather}

\begin{gather}
_{0}\mathrm{C}_{0} \\
_{1}\mathrm{C}_{0} ~~ _{1}\mathrm{C}_{1} \\
_{2}\mathrm{C}_{0} ~~ _{2}\mathrm{C}_{1} ~~ _{2}\mathrm{C}_{2} \\
_{3}\mathrm{C}_{0} ~~ _{3}\mathrm{C}_{1} ~~ _{3}\mathrm{C}_{2} ~~ _{3}\mathrm{C}_{3}\\
_{4}\mathrm{C}_{0} ~~ _{4}\mathrm{C}_{1} ~~ _{4}\mathrm{C}_{2} ~~ _{4}\mathrm{C}_{3} ~~ _{4}\mathrm{C}_{4} \\
_{5}\mathrm{C}_{0} ~~ _{5}\mathrm{C}_{1} ~~ _{5}\mathrm{C}_{2} ~~ _{5}\mathrm{C}_{3} ~~ _{5}\mathrm{C}_{4} ~~ _{5}\mathrm{C}_{5}
\end{gather}

これを利用することで、二項係数を求めることが可能です。上図から、
$$ {}_{i}\mathrm{C}_{j} = {}_{i-1}\mathrm{C}_{j-1}+{}_{i-1}\mathrm{C}_{j} $$
が成立していることが見て取れます。(例えば \({}_{5}\mathrm{C}_{3} = {}_{4}\mathrm{C}_{2}+{}_{4}\mathrm{C}_{3} \) です。)

アルゴリズム

パスカルの三角形による二項係数の計算:

  • 前処理(パスカルの三角形をつくる):
    1. \({}_{i}\mathrm{C}_{j} = {}_{i-1}\mathrm{C}_{j-1}+{}_{i-1}\mathrm{C}_{j} \) を利用して計算(動的計画法)
  • クエリ(\(_{n}\mathrm{C}_{k}\)を求める):
    1. 前処理で計算した値を利用

※ 前処理での \({}_{i}\mathrm{C}_{j} = {}_{i-1}\mathrm{C}_{j-1}+{}_{i-1}\mathrm{C}_{j} \) は二項係数の有名公式ですし、パスカルの三角形の作り方からも分かります。

※ \({}_{i}\mathrm{C}_{j} = {}_{i-1}\mathrm{C}_{j-1}+{}_{i-1}\mathrm{C}_{j} \) は意味を考えると成立していることが分かります。左辺の意味は「\(i\) 個の中から \(j\) 個選ぶ」で、右辺の意味は「 あるものを1つ入れると決めた時は、残りの \(i-1\) の中から \(j-1\) 個選ぶ。入れないと決めた時は、残りの \(i-1\) の中から \(j\) 個選ぶ」ということを表しています。左辺と右辺は同じ場合の数です。

※ 二項係数は n が大きく k が n/2 に近い時に非常に大きな値になります。固定長の整数型では容易にオーバーフローしてしまう可能性があるので注意してください。(具体的には、\(_{50}\mathrm{C}_{25}\) は約 \(10^{14}\) ほどになり、32bit の整数型では収まりません。64bit 必要になります。)

※ 競技プログラミングなどでは、二項係数を素数で割った余りを求めさせる問題が多いです。求め方はこちらを参照してください。

計算量

  • 前処理:\(O(n^2)\)
  • クエリ:\(O(1)\)

C++ での実装例

標準入力から n と k を受け取って、nCk を返すプログラムです。

#include <bits/stdc++.h>
using namespace std;

/*
    前処理: O(MAX_N*MAX_N)
    nCk(n,k): nCk の計算。O(1)
*/
const int MAX_N = 50;         // n の最大値
vector<vector<long long>> com;  // 前計算の結果を保存

// 動的計画法で前処理
void init() {
    com.assign(MAX_N, vector<long long>(MAX_N));
    com[0][0] = 1;
    for (int i = 1; i < MAX_N; ++i) {
        com[i][0] = 1;
        for (int j = 1; j < MAX_N; j++) {
            com[i][j] = (com[i - 1][j - 1] + com[i - 1][j]);
        }
    }
}
// nCk を取得
long long nCk(int n, int k) {
    assert(!(n < k));
    assert(!(n < 0 || k < 0));
    return com[n][k];
}

int main() {
    long long n, k;
    cin >> n >> k;
    init();
    cout << nCk(n, k) << endl;
}