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

Problem240 上位のサイコロ

問題

6面のサイコロ(各面は 1 から 6)を 5 個振って,
上位 3 個の合計が 15 となる場合は 1111 通りある.
いくつか例を挙げる:

D1,D2,D3,D4,D5 = 4,3,6,3,5
D1,D2,D3,D4,D5 = 4,3,3,5,6
D1,D2,D3,D4,D5 = 3,3,3,6,6
D1,D2,D3,D4,D5 = 6,6,3,3,3

12面のサイコロ(各面は 1 から 12)を 20 個振って,
上位 10 個の合計が 70 となる場合は何通りあるか.


ソース

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        Solve(6, 5, 3, 15);
        Solve(12, 20, 10, 70);
    }

    static void Solve(int pMenCnt, int pShikouCnt, int pTopN, int pNeedSum)
    {
        //深さ優先探索で上位N件の組み合わせを求める
        List<int[]> TopNPatternArrList = DeriveTopNPatternArrList(pMenCnt, pTopN, pNeedSum);

        long Answer = 0;
        foreach (int[] EachTopNPatternArr in TopNPatternArrList) {
            int TopNPatternMin = EachTopNPatternArr.Min();
            int TopNPatternMinCnt = EachTopNPatternArr.Count(X => X == TopNPatternMin);

            int TopNPatternNonMinCnt = EachTopNPatternArr.Length - TopNPatternMinCnt;

            //TopNの最小値の個数で場合分け
            for (int I = TopNPatternMinCnt; I <= pShikouCnt - TopNPatternNonMinCnt; I++) {
                //残りのマス数
                int RestMasuCnt = pShikouCnt;

                //現在の場合の数
                long CurrPatternCnt = 1;

                var Query = EachTopNPatternArr.GroupBy(X => X);
                foreach (var EachItem in Query) {
                    if (EachItem.Key == TopNPatternMin) {
                        CurrPatternCnt *= DeriveCombi(RestMasuCnt, I);
                        RestMasuCnt -= I;
                    }
                    else {
                        CurrPatternCnt *= DeriveCombi(RestMasuCnt, EachItem.Count());
                        RestMasuCnt -= EachItem.Count();
                    }
                }
                //残りのマスがある場合
                if (RestMasuCnt > 0) {
                    //埋める数字がない場合
                    if (TopNPatternMin == 1) continue;

                    //TopNの最小値未満での重複順列
                    for (int J = 1; J <= RestMasuCnt; J++) {
                        CurrPatternCnt *= (TopNPatternMin - 1);
                    }
                }

                //var sb = new System.Text.StringBuilder();
                //sb.AppendFormat("Top{0}が", pTopN);
                //Array.ForEach(EachTopNPatternArr, X => sb.AppendFormat("{0},", X));
                //sb.AppendFormat("TopNの最小値である{0}の使用回数が{1}だと、", TopNPatternMin, I);
                //sb.AppendFormat("{0}通り", CurrPatternCnt);
                //Console.WriteLine(sb.ToString());

                Answer += CurrPatternCnt;
            }
        }
        Console.WriteLine("面数{0}、試行回数{1}、上位{2}件、合計{3}での解は{4}",
            pMenCnt, pShikouCnt, pTopN, pNeedSum, Answer);
    }

    struct JyoutaiDef
    {
        internal int CurrNum;
        internal int SumVal;
        internal List<int> NumList;
    }

    //深さ優先探索で上位N件の組み合わせを求める
    static List<int[]> DeriveTopNPatternArrList(int pMenCnt, int pTopN, int pNeedSum)
    {
        var WillReturn = new List<int[]>();

        var Stk = new Stack<JyoutaiDef>();
        JyoutaiDef WillPush;
        WillPush.CurrNum = 1;
        WillPush.SumVal = 0;
        WillPush.NumList = new List<int>();
        Stk.Push(WillPush);

        while (Stk.Count > 0) {
            JyoutaiDef Popped = Stk.Pop();

            //クリア判定
            if (Popped.NumList.Count == pTopN) {
                if (Popped.SumVal == pNeedSum) {
                    WillReturn.Add(Popped.NumList.ToArray());
                }
                continue;
            }

            for (int I = Popped.CurrNum; I <= pMenCnt; I++) {
                WillPush.CurrNum = I;
                WillPush.SumVal = Popped.SumVal + I;

                //枝切り
                if (WillPush.SumVal > pNeedSum) break;

                WillPush.NumList = new List<int>(Popped.NumList);
                WillPush.NumList.Add(I);
                Stk.Push(WillPush);
            }
        }
        return WillReturn;
    }

    //nCrを求める
    static long DeriveCombi(int pN, int pR)
    {
        long WillReturn = 1;
        pR = Math.Min(pR, pN - pR);

        for (int I = pN - pR + 1; I <= pN; I++) {
            WillReturn *= I;
        }
        for (int I = 2; I <= pR; I++) {
            WillReturn /= I;
        }
        return WillReturn;
    }
}


実行結果

面数6、試行回数5、上位3件、合計15での解は1111
面数12、試行回数20、上位10件、合計70での解は7448717393364181966


解説

深さ優先探索で上位N件の組み合わせ候補を求めてから、
組み合わせの最小値の個数で場合分けしてます。