ながめも

競技プログラミングについて

AtCoder Beginner Contest ABC008 C - コイン

問題へのリンク

解説

公式解説とは異なる方針でACしたので記録を残しておきます。

まず、いわゆる主客転倒(寄与ゲー)と呼ばれる問題だと捉えます。 今回求めたいのは、 N!の並びのうち、表のコインの数の期待値ですが、 これを期待値ではなく数え上げの問題として認識します。 つまり、表のコインの枚数を数え上げ、最後に N!で割れば期待値の計算ができます。

これを、あるコインが表になる場合の数を足し合わせることで計算します。

あるコイン iが表になるのは、そのコインより左のコインの中に  C_iの約数が偶数個ある場合です。

まず、コイン i以外で C_iの約数の個数を Sとします。

コイン iが位置 jにあって、その左に k個コインがある場合を数え上げればよく( kは偶数)、これはcombinationとpermutation、階乗の計算を前計算すれば解くことができます。

  1.  C_iの約数 S個から左に置くコイン k個を選び (_{S} C_{k})
  2. 選んだ約数 k個を左への並べ方が _{j} P_{k}通り(場所を選んで並べるイメージ)、
  3. 残りの約数 S - k個を右への並べ方が _{N-1-j} P_{S - k}通り(場所を選んで並べるイメージ)、
  4. 約数以外の並べ方が (N-1-S)!(場所は上で確定しているので、並べるだけ)通り。

計算量は \Theta(N^{3})です。

実装

ところでこの階乗の計算で誤差ってどこまで許容されるんですかね(わかっていない顔)。

#include <bits/stdc++.h>
using namespace std;
#define rep(i,N) for(int i=0;i<int(N);++i)

int main() {
    cout << fixed << setprecision(20);
    int N;
    cin >> N;
    vector<int> C(N);
    rep(i, N)cin >> C[i];
    vector<int> cnt(N,0);
    rep(i,N)rep(j,N){
        if(i == j)continue;
        if(C[j]%C[i] == 0)cnt[j]++;
    }

    long double kai = 1;
    for(int i = 1; i <= N; i++)kai /= i;
    vector<long double> fact(N+1);
    fact[0] = kai;
    rep(i,N)fact[i+1] = fact[i] * (i+1);

    auto comb = [&](int a, int b) -> long double{
        if(a == 0 || b == 0)return 1;
        if(b > a)return 0;
        return fact[a] / fact[a-b] / fact[b] * kai;
    };
    auto perm = [&](int a, int b) -> long double{
        if(a == 0 || b == 0)return 1;
        if(b > a)return 0;
        return fact[a] / fact[a - b];
    };

    long double ans = 0; 
    // あるコインiが場所jにいて
    // 自分より左にk個、右にcnt[i] - k個おくときの場合の数
    // kは偶数のみ
    rep(i,N)rep(j,N){
        for(int k = 0; k <= j && k <= cnt[i]; k+=2){
            int S = cnt[i];
            if(k > j)continue;
            if(S-k > N-1-j)continue;
            long double tmp = comb(S,k) * perm(j,k) * perm(N-1-j,S-k) * fact[N-1-S];
            ans += tmp;
        }
    }
    cout << ans << endl;
}

公式解説

公式の解説では、もっと簡単に問題を捉えています。 まず、期待値の線形性を考えれば、コインごとの期待値の足し合わせが答えになることがわかります。

コイン iについて考えると、期待値を計算するのであれば、とても簡潔に考えることができます。 コイン iの位置について、約数の並べの中でどの位置にあるかを考えます。この位置に関して、共通して現れる計算として、約数の並べ方と約数以外の並べ方があります。これらはコイン iが約数の並べの中でどの位置になるかに依存しません。まず約数を並べて、位置を選び、その後約数以外を挿入すると考えるとわかりやすいでしょう。よって、約数の個数のみを考慮すれば良いことになります。

考える順番を変えるだけでかなり楽に計算することができました。