「M-SOLUTIONS プロコンオープン」C問題
2019年6月1日開催のAtCoder M-SOLUTIONS プロコンオープンに参加した.結果はA, B 2完.このnoteでは時間内に解けなかったC問題の私なりの理解,解説を紹介する.
C問題 Best-of-(2n-1)
問題はこちら.配点は500点.今までのC問題と比べ配点も高く,難易度も高いように感じた.この問題を正解するには,ゲームが行われる回数の期待値 P/Q を求める方法と,それをmod 10^9 +7 で計算する方法の理解が必要.後者についてはこちらの記事がとてもわかりやすかった.
ゲームが行われる回数の期待値の求め方
まず,引き分けが発生しない(Cが0)場合を考える. ゲーム終了時点での高橋君,青木君の勝数のパターンは(N,0), (N, 1), (N, 2), ... , (N,N-1) または,(0,N),(1,N),...,(N-1,N) が考えられる.これらのパターンは全て排反であるため,それぞれに対して(生じる確率)x(ゲームが行われる回数)を計算し和を取ると期待値になる.例えば (N, i)の時は,A/(A+B) = a, B/(A+B) = b とおいて
(N+i回目に高橋君が勝つ確率)*(1~N+i-1回目の間で高橋君がN-1回勝ち,青木君がi回勝つ確率) * (N+i)
= a * a^(N-1) * b^i * (N+i-1個からi個選ぶ組み合わせ) * (N+i)
となる.これを地道に足し上げたものが,引き分けが発生しない場合の期待値になる.次に引き分けによるゲーム回数の嵩増しを考える.引き分けが発生しない場合,ゲームの推移は,高: 高橋君の勝ち,青:青木君の勝ちとして
高,青,高,青,高,高,青,高
のようになるが,引き分けがある場合は,上の推移の間に引き分けが割り込んで
分高,青,分高,分青,分分高,分高,分青,分分高
のようになる.それぞれの高か青の前に引き分けが何回か(0回も含む)発生すると考えられる.例えば,一番右では
高 → 分分高
となっている.一度,高or青となってから次に高or青になるまでのゲームの回数(分の回数+1)の期待値は,「1回のゲームで高or青となる確率の逆数」になる.直感的には,例えば,高or青となる確率が1/4,引き分けの確率が3/4の時,平均的に4回ゲームをすれば高or青となりそうだとわかる.(数学的に詳しいことは「幾何分布」を調べるとわかる).
さて,一度高or青となってから次に高or青になるまでのゲームの回数の期待値は,「1回のゲームで高or青となる確率の逆数」になるので,引き分けがある場合のゲームが行われる回数の期待値は,引き分け無しのときの期待値の「1回のゲームで高or青となる確率の逆数」倍になる.つまり,引き分けも考慮したゲーム回数の期待値は
a * a^(N-1) * b^i * (N+i-1個からi個選ぶ組み合わせ) * (N+i) * 「1回のゲームで高or青となる確率の逆数」
= a * a^(N-1) * b^i * (N+i-1個からi個選ぶ組み合わせ) * (N+i) * 100/(100-C)
= a * a^(N-1) * b^i * (N+i-1)! * (N-1)! * i! * (N+i) * 100/(100-C)
となる.これをC++で実装すると次のようになる.(#includeは省略)
#define rep(i,n) for(int i=0;i<n;i++)
using namespace std;
long long factorial(long long i){ // 階乗
if(i > 0)
return i * factorial(i-1);
else
return 1;
}
int main() {
long long N, A, B, C;
cin >> N >> A >> B >> C;
// N!をあらかじめ計算しておく
vector<long long> factmod;
factmod.push_back(1); // 0! = 1 と定義
for(int i=1; i<2*N+10; i++) { // (2*N)! まで計算しておけば十分
long long tmp = (factmod[i-1]*i);
factmod.push_back(tmp);
}
long long res = 0;
rep(i, N) { // AがN回勝ち,Bがi回勝ちで終了.
res += (N+i) * factorial(N-1+i) * pow(A, N) * pow(B, i)/ pow(A+B, N+i) / factorial(N-1) /factorial(i) * 100.0 / (100.0-C);
}
rep(i, N) { // Aがi回勝ち,BがN回勝ちで終了.
res += (N+i) * factorial(N-1+i) * pow(B, N) * pow(A, i)/ pow(A+B, N+i) / factorial(N-1) /factorial(i) * 100.0 / (100.0-C);
}
cout << res << endl;
return 0;
}
注) rep(i,n)はfor文を短く書くために定義しているだけ.ループ 内で繰り返し使用する数(N!など)は,ループの外側であらかじめ計算しておく.特に階乗は事前にまとめて実施しておく.
試しに,入力例1: 1 25 25 50 を入力すると2 が出力される.
あとは,mod 10^9 +7 で計算すればよい.
mod 10^9 +7 で計算する方法
冒頭でも触れたがこれに関しては,Kensuke Otsukiさんの大変詳しい記事をtwitter経由で見つけたのでそちらを参照してほしい.mod p における割り算がややこしいが,上で求めた期待値の計算をひたすらmod p での計算していけばよい.どうやら出力すべき R は P ÷ Q をmod 10^9 +7 で行ったものらしい.C++で実装すると次のようになる.(#includeは省略)
#define rep(i,n) for(int i=0;i<n;i++)
using namespace std;
long long factorial(long long i){ // 階乗
if(i > 0)
return i * factorial(i-1);
else
return 1;
}
// a^n mod
long long modpow(long long a, long long n, long long mod) {
long long res = 1;
while (n>0) {
if(n & 1) res = res * a % mod;
a = a * a % mod;
n >>= 1;
}
return res;
}
// a^(-1) mod
long long modinv(long long a, long long mod) {
return modpow(a, mod-2, mod);
}
int main() {
long long N, A, B, C;
const long long mod = 1000000007;
cin >> N >> A >> B >> C;
// N!(mod 1000000007)をあらかじめ計算しておく
vector<long long> factmod;
factmod.push_back(1); // 0! = 1 と定義
for(int i=1; i<2*N+10; i++) { // (2*N)! まで計算しておけば十分
long long tmp = (factmod[i-1]*i) %mod;
factmod.push_back(tmp);
}
// よく使う値を事前に計算
long long invAplusB = modinv(A+B, mod);
long long invFactorialNm1 = modinv(factmod[N-1], mod);
long long invHandmC = modinv(100-C, mod);
long long mothA = modpow(A, N, mod)%mod * invFactorialNm1%mod * 100 %mod * invHandmC %mod;
long long mothB = modpow(B, N, mod)%mod * invFactorialNm1%mod * 100 %mod * invHandmC %mod;
long long res = 0;
rep(i, N) { // AがN回勝ち,Bがi回勝ちで終了.
// 通常の期待値
//res += (N+i) * factorial(N-1+i) * pow(A, N) * pow(B, i)/ pow(A+B, N+i) / factorial(N-1) /factorial(i) * 100.0 / (100.0-C);
res += (N+i) * factmod[N-1+i]%mod * modpow(B, i, mod)%mod * modpow(invAplusB, N+i, mod)%mod * modinv(factmod[i], mod)%mod * mothA%mod;
res = res % mod;
}
rep(i, N) { // Aがi回勝ち,BがN回勝ちで終了.
// 通常の期待値
//res += (N+i) * factorial(N-1+i) * pow(B, N) * pow(A, i)/ pow(A+B, N+i) / factorial(N-1) /factorial(i) * 100.0 / (100.0-C);
res += (N+i) * factmod[N-1+i]%mod * modpow(A, i, mod)%mod * modpow(invAplusB, N+i, mod)%mod * modinv(factmod[i], mod)%mod * mothB % mod;
res = res % mod;
}
cout << res << endl;
return 0;
}
注) ループ 内で繰り返し使用する数は,ループの外側であらかじめ計算しておく.掛けたり足したりするたびに%mod を行う.特に階乗は事前にまとめて実施しておく.
この記事が気に入ったらサポートをしてみませんか?