AtCoder Beginner Contest 011 D - 大ジャンプ
お題箱より。
解法
まずは計算しやすいようにいくつか前処理をします。
ゴールまでの 軸方向それぞれの距離を で割ることで「正の方向にジャンプ○回分」という値に変換します。ここで割り切れない場合、答えは です。今後は で割った後の値を単に と表記します。
答えを求めるにあたり、 回のジャンプのうちいくつを 軸に割り振るかを全探索します。ここで 回を 軸に割り振るとすると、ゴールに止まるために正負それぞれの方向に何回飛ばないといけないかが決まります。具体的には正の方向に 回、負の方向に 回とすると
という連立方程式の解として求めることができます。これを解いたときに の両方が非負整数になる必要があり、そうでない場合この ではゴール不可能です。
同様に 軸についても、 回を正負それぞれの方向に飛ぶ回数 に割り振ります。
すると を固定した時に「合計 回のジャンプがどう並んでいるか」という場合の数は、
- まず 回を 軸と 軸に割り振る: 通り
- 軸の 回を割り振る: 通り
- 軸の 回を割り振る: 通り
の掛け算で計算できて、それぞれの並べ方について「全てのジャンプがその並びの通りになる確率」は なのでこれを掛けると答えを求めることができます。
もしこれが「有理数を で計算した余りを求めよ」という形式であればあとは階乗や逆元のライブラリ任せで計算できます。ただ今回は小数で計算する必要があるのでちょっと面倒です。
小数での確率計算
なぜ面倒かというと、浮動小数点数型には桁数の上限/下限があるからです。例えばC++で
double P = 1.0; for(int i=1; i<=N; i++) P *= i;
と小数での階乗計算をすると、 で桁数が足りなくなり、出力すると inf
と表示されるようになります。小さい方の値である も、 では 0
になります。(ともに私の手元環境で実験)
つまり急速に大きくなる値(場合の数)と急速に小さくなる値(ある1つの場合が実現される確率)を別々に計算して掛け算すると正しい値が求められません。上手く並行して計算することで桁数をコントロールする必要があります。
例えば 回のジャンプを 軸と 軸に割り振るところを考えます。これを確率込みで考えて、
- 回のジャンプが、それぞれ 軸どちらのものであるかを順番に決めていく。 軸になる確率、 軸になる確率はともに である。 軸に割り振った回数がちょうど 回となる確率はいくつか?
という値を求めましょう。
これはDPで求めることができます。状態を次のように定義します。
- ジャンプ 回について既に決めている時に、そのうち 回が 軸のものである確率
初期条件は です。 からの遷移は、次に決めたものが 軸であれば に、 軸であれば に、それぞれ確率 を係数として遷移します。
このDPの遷移は、「パスカルの三角形」の構造そのものです。
このDPの結果である が求めたい値であり、これは と一致します。
軸の中、 軸の中それぞれの割り振りも、同じ構造になっているので同様に求められます。こちらも正になる確率、負になる確率がともに なので同じDPの結果を使うことができて、それぞれ と が求められます。
これらの積が、元々求めたかった
と一致します。 を全探索してこの値を全て合計することで、答えを求めることができます。
計算量について
もし十分大きな素数で割った余りを求める形式であれば、階乗の前計算が でできていました。ただし今回は小数で求めるという都合上DP(パスカルの三角形)をしたため、前計算の計算量が になっています。他にも、合成数や大きくない素数で割った余りを答える場合など逆元が使えない時もパスカルの三角形を使うことがあります。
制約や要求されている答えの形式に応じてこれらの方法を使い分けていく必要があります。