1. 典型的な二項係数の求め方
競プロをしていると、「 mod 」を計算する場面にしばしば出くわします。最近では、 であることが多いですね。
mod の計算方法は、時と場合によって色んな方法が考えられますが、すぐ下で紹介する方法が最も頻繁に使用されています。多くの AtCoder のトッププレイヤーたちも使用している形式で、高速です。
使い方としては、最初に一度前処理として COMinit()
を呼び出します。その後は、毎回 COM(n, k)
関数を呼べばよいです。
- 前処理
COMinit()
:計算量 - クエリ処理
COM(n, k)
:計算量
1-1. mod の実装
この実装では、ACL (AtCoder Library) の modint
を用いています。さらに下に、modint
を使わない実装も載せています。
#include <iostream> using namespace std; // AtCoder の modint を使う #include <atcoder/modint> using mint = atcoder::modint998244353; const int MAX = 510000; mint fac[MAX], finv[MAX], inv[MAX]; // テーブルを作る前処理 void COMinit() { const int MOD = mint::mod(); fac[0] = fac[1] = 1; finv[0] = finv[1] = 1; inv[1] = 1; for (int i = 2; i < MAX; i++){ fac[i] = fac[i - 1] * i; inv[i] = MOD - inv[MOD%i] * (MOD / i); finv[i] = finv[i - 1] * inv[i]; } } // 二項係数計算 mint COM(int n, int k){ if (n < k) return 0; if (n < 0 || k < 0) return 0; return fac[n] * finv[k] * finv[n - k]; } int main() { // 前処理 COMinit(); // 計算例 cout << COM(100000, 50000).val() << endl; }
注意:modint
を使わない二項係数の実装
modint を使わない場合は、次のように実装できます。
コードを開く
#include <iostream>
using namespace std;
const int MAX = 510000;
const int MOD = 998244353;
long long fac[MAX], finv[MAX], inv[MAX];
// テーブルを作る前処理
void COMinit() {
fac[0] = fac[1] = 1;
finv[0] = finv[1] = 1;
inv[1] = 1;
for (int i = 2; i < MAX; i++){
fac[i] = fac[i - 1] * i % MOD;
inv[i] = MOD - inv[MOD%i] * (MOD / i) % MOD;
finv[i] = finv[i - 1] * inv[i] % MOD;
}
}
// 二項係数計算
long long COM(int n, int k){
if (n < k) return 0;
if (n < 0 || k < 0) return 0;
return fac[n] * (finv[k] * finv[n - k] % MOD) % MOD;
}
int main() {
// 前処理
COMinit();
// 計算例
cout << COM(100000, 50000) << endl;
}
1-2. 使用可能場面
- mod. の は素数である (上の実装ではさらに を仮定している)
1-3. 使用原理
C
であることを利用しています。COMinit() で、 (fac[a]) と (finv[a]) のテーブルを予め作っています。これを作っておくことで、クエリ計算が掛け算のみになって高速になります。
fac[0], fac[1], ..., fac[n-1] の計算が でできることは難しくない感じです。いわゆる累積和ならぬ累積積をやっている感じです。一方、finv[0], finv[1], ..., finv[n-1] の計算も実は でできることは驚きです。finv を計算するために、mod. における の逆元 inv[1], inv[2], ..., inv[n] を で求めます。そうすれば、inv の累積積をとることで finv も で計算できます。
を素数としたとき、mod. での逆元計算方法には大きく分けて
- 拡張 Euclid の互除法
- Fermat の小定理
とがあります。ともに かかりますが、多くの場合、拡張 Euclid の互除法の方が高速に動作します。さて、愚直に の逆元を計算していては かかってしまいます。ところが
- の逆元を、 % (これは より小さいことに注意) の逆元を利用して で求める
という魔法のようなテクニックがあります。そのテクニックを用いることで、mod. における の逆元を で計算できます。そのことを理解するために、そもそも拡張 Euclid の互除法が何をしていたかを考えます。
1-4. 拡張 Euclid の互除法による逆元計算
mod. を計算するとはすなわち
を満たす を求めたいということになります。Euclid の互除法を適用します。すなわち、 を で割ってみます:
これを代入すると
になります。これによって に関する問題が、それより数値の小さな に関する問題に帰着できました。これを再帰的に解くのが拡張 Euclid の互除法です。具体的には に関する小問題を解いて
と解 が再帰的に得られたとすると、
という風に、元の問題の解を構成できます。下に (mod. ) を求める実装を示します。なお注意点として、
- 逆元を求める mod. の が素数でなくても、 と が互いに素であればよい
- 下の拡張 Euclid の互除法自体は と が互いに素でなくても適用できるが、逆元計算するときの と とは互いに素である必要がある
となっています。
#include <iostream> using namespace std; // ax + by = gcd(a, b) となるような (x, y) を求める // 多くの場合 a と b は互いに素として ax + by = 1 となる (x, y) を求める long long extGCD(long long a, long long b, long long &x, long long &y) { if (b == 0) { x = 1; y = 0; return a; } long long d = extGCD(b, a%b, y, x); // 再帰的に解く y -= a / b * x; return d; } // 負の数にも対応した mod (a = -11 とかでも OK) inline long long mod(long long a, long long m) { return (a % m + m) % m; } // 逆元計算 (ここでは a と m が互いに素であることが必要) long long modinv(long long a, long long m) { long long x, y; extGCD(a, m, x, y); return mod(x, m); // 気持ち的には x % m だが、x が負かもしれないので } int main() { // 計算例 cout << modinv(3, 7) << endl; }
1-5. 改めて、inv[1], inv[2], ..., inv[n] を高速に計算する方法
拡張 Euclid の互除法におけるアイディアを少し変形して実現します。 を求めるために同じように
を満たす を求めて行きます。 を で割って
とするのですが、ここから上手いことやります。まず、 の両辺を 倍して変形していくと、
となります。拡張 Euclid の互除法では に関する問題を に関する問題に帰着していたのに対し、今回は に関する問題に帰着しています。 を残していることがミソですね。さて、 とおいて、 を満たす を求めることができれば万事解決ということになります。
を満たす を とすると、 であり、これを両辺 倍することで
となるので、 は を満たします。
- %
であることに注意すると、
% (mod.
であることが導かれました。実装上は
inv[a] = MOD - inv[MOD % a] * (MOD / i) % MOD;
という風にします。
1-6. 逆元漸化式のもう 1 つの導出方法
上では拡張 Euclid の互除法を意識した導出を示しましたが、もっと直接的に導くこともできます。takapt さんの記事にもある導出方法です。 を で割ると
%
で、これを変形することによって直接導出することができます。この両辺の mod. をとると、
%
% (両辺に をかける)
%
%
という風に簡潔に導かれます。
2. n がさらに巨大で固定値なとき (1 ≦ n ≦ 109, 1 ≦ k ≦ 107 程度)
が巨大なときは先程の手が使えません。しかし が小さければ望みがあります。
を利用して で計算することができます。さらに が固定値の場合も多く、そんなときは配列テーブル
com[ k ] = nCk
を で前計算しておくことが有効です。
3. n も k も小さいとき ( 1≦ k ≦ n ≦ 2000 程度、mod. p が素数でなくてもよい)
動的計画法によって nCk のテーブルを生成する方法が有力で、mod の p が素数でなくてもよいのが魅力的です。
const long long MOD = 1000000007; const int MAX_C = 1000; long long Com[MAX_C][MAX_C]; void calc_com() { memset(Com, 0, sizeof(Com)); Com[0][0] = 1; for (int i = 1; i < MAX_C; ++i) { Com[i][0] = 1; for (int j = 1; j < MAX_C; ++j) { Com[i][j] = (Com[i-1][j-1] + Com[i-1][j]) % MOD; } } }
4. さらに
n と k がそれほど小さくなく mod が素数でない場合など、いくらでもイヤな場合は考えられますが、そこまで来たら uwi さんの記事を読めば大抵のことは解決します:
5. 二項係数を用いる問題たち
二項係数を用いる問題たちです。
- AOJ 1501 Grid (DP でテーブル作るやつが間に合います)
- ABC 021 D - 多重ループ (重複組合せを計算します、二項係数の verify にちょうど良さそうです)
- AGC 025 B - RGB Coloring (気づけば単純な問題で実装は軽いため、二項係数の verify にちょうど良さそうです)
- ABC 066 D - 11 (少し難しくなりますがシンプルです)
- ABC 003 D - AtCoder社の冬 (包除原理を用います)