トップページに戻る    次のC#のサンプルへ    前のC#のサンプルへ

Problem329 素数ガエル

問題

Susanは素数ガエルを飼っている.
このカエルは, 1から500までの番号がつけられた500個の正方形の上を跳びまわる.
左右のいずれかの正方形に等確率で跳ぶことができ,
範囲 [1;500] の外に跳ぶことはできない.
(いずれかの端に着地したなら, 次は自動的に移動可能な正方形に跳ぶ.)

素数である数が書かれた正方形にいる場合, 次の正方形に跳ぶ直前に,
カエルは 2/3 の確率で 'P' (PRIME) と鳴き,
1/3 の確率で 'N' (NOT PRIME) と鳴く.

素数でない数が書かれた正方形にいる場合, 次の正方形に跳ぶ直前に,
カエルは 1/3 の確率で 'P' と鳴き,
2/3 の確率で 'N' と鳴く.

カエルの開始位置は全ての正方形に対し等確率でランダムであり,
また最初の15回の鳴き声を聞いたとすると,
PPPPNNPPPNPPNPNと聞こえる確率は何か.

約分済みの分数 p/q の形で答えを入力せよ.


ソース

using System;
using System.Collections.Generic;

class Program
{
    const int UB = 500;

    static int[] mSosuuArr;

    //エラトステネスの篩
    static void Eratosthenes()
    {
        var IsSosuuArr = new System.Collections.BitArray(UB + 1);

        for (int I = 2; I <= IsSosuuArr.Count - 1; I++) {
            IsSosuuArr[I] = true;
        }
        for (int I = 2; I * I <= IsSosuuArr.Count - 1; I++) {
            if (I != 2 && I % 2 == 0) continue;

            if (IsSosuuArr[I]) {
                for (int J = I * 2; J <= IsSosuuArr.Count - 1; J += I) {
                    IsSosuuArr[J] = false;
                }
            }
        }
        var SosuuList = new List<int>();
        for (int I = 2; I <= IsSosuuArr.Count - 1; I++)
            if (IsSosuuArr[I]) SosuuList.Add(I);

        mSosuuArr = SosuuList.ToArray();
    }

    //仮分数クラス
    internal class CKabunSuu
    {
        private decimal mBunshi; //分子
        private decimal mBunbo; //分母

        //コンストラクタ (値指定)
        internal CKabunSuu(decimal pBunshi, decimal pBunbo)
        {
            mBunshi = pBunshi;
            mBunbo = pBunbo;
        }

        //コンストラクタ (インスタンス指定)
        internal CKabunSuu(CKabunSuu pIns)
        {
            mBunshi = pIns.mBunshi;
            mBunbo = pIns.mBunbo;
        }

        //加算用メソッド (インスタンス指定)
        internal void Add(CKabunSuu pIns)
        {
            decimal SavedBunbo = mBunbo; //通分する前の分母

            //通分してから、加算
            mBunbo *= pIns.mBunbo;
            mBunshi = this.mBunshi * pIns.mBunbo + pIns.mBunshi * SavedBunbo;
            Yakubun();
        }

        //乗算用メソッド (インスタンス指定)
        internal void Prod(CKabunSuu pIns)
        {
            this.mBunshi *= pIns.mBunshi;
            this.mBunbo *= pIns.mBunbo;
            Yakubun();
        }

        //除算用メソッド (値指定)
        internal void Div(decimal pVal)
        {
            mBunbo *= pVal;
            Yakubun();
        }

        //約分
        private void Yakubun()
        {
            if (mBunshi == 0) return;

            decimal GCD; //最大公約数
            GCD = DeriveGCD(mBunshi, mBunbo);
            if (GCD == 1) return;
            mBunshi /= GCD;
            mBunbo /= GCD;
        }

        //ユークリッドの互除法で2数の最大公約数を求める
        private decimal DeriveGCD(decimal pVal1, decimal pVal2)
        {
            decimal WarareruKazu = Math.Max(pVal1, pVal2);
            decimal WaruKazu = Math.Min(pVal1, pVal2);

            while (true) {
                decimal Amari = WarareruKazu % WaruKazu;
                if (Amari == 0) return WaruKazu;
                WarareruKazu = WaruKazu;
                WaruKazu = Amari;
            }
        }

        internal bool IsZero()
        {
            return mBunshi == 0;
        }

        //分子と分母を返す
        internal string ToStr()
        {
            return string.Format("{0}/{1}", mBunshi, mBunbo);
        }
    }

    static void Main()
    {
        Eratosthenes();

        //正しい鳴き順である確率[現在位置]なDP表
        var PrevDP = new CKabunSuu[UB + 1];

        for (int I = 1; I <= UB; I++) {
            PrevDP[I] = new CKabunSuu(1, UB);
        }

        for (int I = 1; I <= 15; I++) {
            var CurrDP = new CKabunSuu[UB + 1];
            for (int J = 1; J <= UB; J++) {
                CurrDP[J] = new CKabunSuu(0, 1);
            }

            for (int J = 1; J <= UB; J++) {
                if (PrevDP[J].IsZero()) continue;

                //移動可能な方向の数
                int CanMoveHoukou = 2;
                if (J == 1 || J == UB) CanMoveHoukou = 1;

                Action<int> MergeAct = (pNewJ) =>
                {
                    if (pNewJ < 1 || UB < pNewJ) return;

                    var wkIns = new CKabunSuu(PrevDP[J]);
                    var CorrectKakuritu = DeriveCorrectKakuritu(I, J);
                    wkIns.Prod(CorrectKakuritu);

                    wkIns.Div(CanMoveHoukou);
                    CurrDP[pNewJ].Add(wkIns);
                };

                MergeAct(J - 1);
                MergeAct(J + 1);
            }
            PrevDP = CurrDP;
        }

        var AnswerIns = new CKabunSuu(0, 1);
        for (int I = 1; I <= UB; I++) {
            AnswerIns.Add(PrevDP[I]);
        }
        Console.WriteLine(AnswerIns.ToStr());
    }

    //ジャンプ回数と、現在位置を引数として、正しく鳴く確率を返す
    static CKabunSuu DeriveCorrectKakuritu(int pJumpCnt, int pCurrPos)
    {
        const string CorrectStr = "PPPPNNPPPNPPNPN";
        char CurrChar = CorrectStr[pJumpCnt - 1];

        bool IsPrime = (Array.BinarySearch(mSosuuArr, pCurrPos) >= 0);

        bool IsMatch = false;
        if (CurrChar == 'P' && IsPrime) IsMatch = true;
        if (CurrChar == 'N' && IsPrime == false) IsMatch = true;

        return new CKabunSuu(IsMatch ? 2 : 1, 3);
    }
}


実行結果

199740353/29386561536000


解説

正しい鳴き順である確率[現在位置]を、
確率の加法定理と、確率の乗法定理で更新していく、
動的計画法で解いてます。