楽しい!!!!!!!!!すごく勉強になったん!!!!!
大きく 2 つのやり方があって、高速ゼータ変換を用いた O(3n) から O(n2n) への高速化や、期待値に関する重要な考察をするなど色々やれるん。
問題概要
人がテストを行う。 毎秒ごとに 番目の人がテストを終える確率は で与えられる。全員がテストを終えるまでに要する時間の期待値を求めよ。
制約
解法 1: O(3n) な期待値 DP を O(n2n) に高速化する
まず、1 人の場合は有名問題で、毎ステップごとに確率 で終わる事象が終わるまでのステップ数の期待値は になる。これは期待値の方程式を立てればわかる:
これを解いて、 が導出される。
それでは 人の場合 ( で表される集合の人たち) の場合の式を書いてみる。ここで簡単のために
とおいておく。
という感じの式になる。この式で は、 のうち 1 秒後も終わらずに残ったものたちを表す。この式を について解くと、
となる。これはちょうど bit DP の枠組みで解けるように見える。状態量が な bit DP で、状態遷移が submask 全体に行き渡るタイプの bit DP なので計算量は になる。このままでは間に合わないので頑張って高速化する。
の計算をするのに毎回 の形の足し算をするのが時間かかるので、ここをなんとかしたい。もしこれの中身が には依存せずに のみに依存する式だったら、
みたいな補助変数を用意して、これを更新しながら計算すればよさそうである。DP を累積和を用いて高速化する話によく似ている。これができるようにするために、式変形をする。具体的には の部分をくくり出す。ここで
としておく。そうすると
という風に式変形できる。これでめでたく sum のところの中身が のみに依存する式になったので、アイディアとしては
とおくことで、これを毎回計算しながら頑張ればよい。 の更新は高速ゼータ変換の亜種 (naoya_t さんのブログ の (2) バージョン) の出番である!
ここでいう高速ゼータ変換は
集合に対する関数 があって
を計算したい
というものを考える ( の向きが逆で の方を高速ゼータ変換と呼ぶことの方が多いかも)。
これは以下のようなシンプルなコードが知られているが
for (int i = 0; i < n; ++i) for (int bit = 0; bit < (1<<n); ++bit) if (bit & (1<<i)) f[bit] += f[bit ^ (1<<i)];
これは配列再利用をしているため少し見えづらくなっている。結局以下のような DP をしている:
t[ i ][ bit ] := bit のうち、下から i 桁については bit の subbit 全体を動かし、それ以外の桁については bit と同じ値に固定したもの全体の総和
とすると
- 初期化: t[ 0 ][ bit ] = f(bit)
- 更新:
t[ i + 1 ][ bit ] =
t[ i ][ bit ] ( i が bit に含まれないとき)
t[ i ][ bit ] + t[ i ][ bit ^ (1<<i) ] ( i が bit に含まれるとき)
- 答え: t[ n ][ bit ]
なお、こうして t を二次元配列として定義してあげると、
for (int i = 0; i < n; ++i) for (int bit = 0; bit < (1<<n); ++bit) if (bit & (1<<i)) f[bit] += f[bit ^ (1<<i)];
の i と bit のループの順番を変更しても大丈夫になる。これを利用して、dp[bit] を求めながら、t[n][bit] も求めていく。もう 1 つの注意点として、本来は t の初期化は
t[ 0 ][ bit ] = f(bit)
という感じで行うのだが、f(bit) の値が予めわからないので、後で
for (int i = 0; i <= n; ++i) t[i][bit] += R*(dp[bit] + 1);
という感じで足している。
#include <iostream> #include <vector> using namespace std; class TestProctoring { public: double expectedTime(vector <int> a, vector <int> b) { int n = (int)a.size(); vector<double> p(n); for (int i = 0; i < n; ++i) p[i] = (double)(a[i]) / b[i]; vector<double> dp(1<<n, 0.0); vector<vector<double> > t(n+1, vector<double>(1<<n, 0.0)); for (int bit = 0; bit < (1<<n); ++bit) { double P = 1.0, Q = 1.0, R = 1.0; for (int i = 0; i < n; ++i) { t[i+1][bit] = t[i][bit]; if (bit & (1<<i)) { P *= p[i], Q *= (1.0 - p[i]), R *= (1.0/p[i] - 1); t[i+1][bit] += t[i][bit ^ (1<<i)]; } } if (bit) dp[bit] = P / (1 - Q) * (R + t[n][bit]); for (int i = 0; i <= n; ++i) t[i][bit] += R*(dp[bit] + 1); } return dp[(1<<n)-1]; } };
解法 2: 試行回数の期待値 = (k 回以上かかる確率) の和
てんぷらさんに教えてもらった解法です。kmjp さんの解法とも一致しています。今回はちょうど 回で終わる確率を として、期待値の定義から
を計算したいです。しかし「ちょうど 回になる確率」というのはとても計算しづらいので、代わりに
- 回以上かかる確率
というのを代わりに考えます。このとき になるので、
という感じになります (収束性についてはきちんと議論しないとですが...)。標語的に言えば
試行回数の期待値 = ( 回以上かかる確率) の についての無限和
ということになります。これは「ヒストグラムを書いて縦に集計するものを横に集計し直したもの」というようなものの見方ができます。結構汎用的なものの見方な気がします。AGC 023 C - Painting Machines でも出て来たとか。
さて、各 について を計算します。 回以上かかるというのはすなわち「 秒たってもまだ誰かしらが終わってない確率」とういことになります。これは余事象をとると「 秒で全員が終わる確率」です。 とおくと、 さんが 秒たっても終わらない確率は となることから、 さんが 秒で終わる確率は になります。したがって、
になります。これを展開すると、次のように 個の項が出て来ます。 を {} の空でない部分集合を動くとして、
という感じになります。この式は包除原理で導いたものと考えることもできます (kmjp さんのブログでは包除原理)。
この項それぞれについて、 についての無限級数の総和をとったものを求めればよいです。ここで
とおくと、無限級数の和は
になります。あとはこれを各 について足し引きすればよいです。
#include <iostream> #include <vector> using namespace std; class TestProctoring { public: double expectedTime(vector <int> a, vector <int> b) { double res = 0.0; int n = (int)a.size(); for (int bit = 1; bit < (1<<n); ++bit) { double r = 1.0; int bitcount = 0; for (int i = 0; i < n; ++i) if (bit & (1<<i)) r *= 1.0 - (double)(a[i])/b[i], ++bitcount; double tmp = 1.0 / (1.0 - r); if (bitcount & 1) res += tmp; else res -= tmp; } return res; } };