web-dev-qa-db-ja.com

特定の数をロールする方法の数を計算します

私は高校のコンピュータサイエンスの学生ですが、今日、次の問題が発生しました。

プログラムの説明:サイコロを振るプレイヤーの間では、3つのサイコロを投げると9よりも10の方が簡単に得られるという信念があります。この信念を証明または反証するプログラムを書くことができますか?

1 + 1 + 1、1 + 1 + 2、1 + 1 + 3など、3つのサイコロを投げることができるすべての可能な方法をコンピューターに計算させます。これらの可能性をそれぞれ合計し、結果として9を与える数を確認します。何人が10を与えます。それ以上が10を与える場合、その信念は証明されます。

私はすぐにブルートフォースソリューションを考え出しました。

int sum,tens,nines;
    tens=nines=0;

    for(int i=1;i<=6;i++){
        for(int j=1;j<=6;j++){
            for(int k=1;k<=6;k++){
                sum=i+j+k;
                //Ternary operators are fun!
                tens+=((sum==10)?1:0);
                nines+=((sum==9)?1:0);
            }
        }

    }
    System.out.println("There are "+tens+" ways to roll a 10");
    System.out.println("There are "+nines+" ways to roll a 9");

これは問題なく機能し、ブルートフォースソリューションは教師が私たちに望んでいたことです。ただし、スケーリングは行われず、ロールする方法の数を計算できるアルゴリズムを作成する方法を見つけようとしています。 n 特定の番号を取得するためのサイコロ。したがって、私は各合計を取得する方法の数を生成し始めました n サイコロ。ダイが1つある場合、それぞれに明らかに1つのソリューションがあります。次に、ブルートフォースを使用して、2つと3つのサイコロの組み合わせを計算しました。これらは2つです:

2をロールする1つの方法があります
3をロールする方法は2つあります
4をロールする方法は3つあります
5をロールする方法は4つあります
6を出すには5つの方法があります
7をロールする6つの方法があります
8を出すには5つの方法があります
9を出すには4つの方法があります
10をロールする方法は3つあります
11をロールする方法は2つあります
12をロールする方法は1つあります

これは十分に単純に見えます。単純な線形絶対値関数で計算できます。しかし、それから物事はよりトリッキーになり始めます。 3で:

3をロールする1つの方法があります
4をロールする方法は3つあります
5を出すには6つの方法があります
6を出すには10の方法があります
7をロールする15の方法があります
8を出すには21の方法があります
9を出すには25の方法があります
10を出すには27の方法があります
11をロールする27の方法があります
12を出すには25の方法があります
13をロールする方法は21あります
14をロールする15の方法があります
15をロールする10の方法があります
16をロールする6つの方法があります
17をロールする方法は3つあります
18をロールする方法は1つあります

だから私はそれを見て、私は思う:クールな三角数!しかし、それから私はそれらの厄介な25と27に気づきます。したがって、それは明らかに三角数ではありませんが、対称であるため、それでもいくつかの多項式展開です。
それで私はグーグルに行きます、そして私は出くわします このページ それは数学でこれをする方法についていくらかの詳細に入ります。繰り返し導関数や展開を使用してこれを見つけるのは(長いですが)かなり簡単ですが、私のためにそれをプログラムするのははるかに難しいでしょう。 2番目と3番目の答えは、数学の勉強でその表記や概念に出会ったことがないので、よくわかりませんでした。私自身の組み合わせ論の理解のために、誰かがこれを行うためのプログラムを書く方法を説明したり、そのページに記載されている解決策を説明したりできますか?

編集:私はこれを解決するための数学的な方法を探しています、それはサイコロをシミュレートすることによってではなく、正確な理論上の数を与えます

19
scrblnrd3

N(d, s)で関数生成メソッドを使用するソリューションは、おそらくプログラミングが最も簡単です。再帰を使用して、問題を適切にモデル化できます。

public int numPossibilities(int numDice, int sum) {
    if (numDice == sum)
        return 1;
    else if (numDice == 0 || sum < numDice)
        return 0;
    else
        return numPossibilities(numDice, sum - 1) +
               numPossibilities(numDice - 1, sum - 1) -
               numPossibilities(numDice - 1, sum - 7);
}

一見すると、これはかなり簡単で効率的な解決策のように見えます。ただし、numDicesumの同じ値の多くの計算が繰り返され、再計算される可能性があることに気付くでしょう。このソリューションは、おそらく元のブルートフォース手法よりも効率が低くなります。たとえば、3ダイスのすべてのカウントを計算する際に、私のプログラムはnumPossibilities関数を合計15106回実行しました。これに対して、ループは6^3 = 216のみを取ります。実行。

このソリューションを実行可能にするには、以前に計算された結果のメモ化(キャッシング)というもう1つの手法を追加する必要があります。たとえば、HashMapオブジェクトを使用すると、すでに計算された組み合わせを格納し、再帰を実行する前に最初にそれらを参照することができます。キャッシュを実装したとき、numPossibilities関数は合計151回だけ実行して、3ダイスの結果を計算します。

サイコロの数を増やすと、効率の向上は大きくなります(結果は、私が実装したソリューションを使用したシミュレーションに基づいています)。

# Dice | Brute Force Loop Count | Generating-Function Exec Count
3      | 216 (6^3)              | 151
4      | 1296 (6^4)             | 261
5      | 7776 (6^5)             | 401
6      | 46656 (6^6)            | 571
7      | 279936 (6^7)           | 771
...
20     | 3656158440062976       | 6101
13
mellamokb

数学的記述は、同じカウントを行うための単なる「トリック」です。多項式を使用してサイコロを表します。1*x^6 + 1*x^5 + 1*x^4 + 1*x^3 + 1*x^2 + 1*xは、各値1〜6が1回カウントされることを意味し、多項式の乗算P_1*P_2を使用してさまざまな組み合わせをカウントします。これは、ある指数(k)の係数が、指数の合計がkになるP_1P_2のすべての係数を合計することによって計算されるためです。

例えば。 2つのサイコロの場合:

(1*x^6 + 1*x^5 + 1*x^4 + 1*x^3 + 1*x^2 + 1*x) * (x^6 + x^5 + x^4 + x^3 + x^2 + x) = 
(1*1)*x^12 + (1*1 + 1*1)*x^11 + (1*1 + 1*1 + 1*1)*x^11 + ... + (1*1 + 1*1)*x^3 + (1*1)*x^2

この方法による計算は、「数える」のと同じ複雑さです。

関数(x^6 + x^5 + x^4 + x^3 + x^2 + x)^nはより単純な式(x(x-1)^6/(x-1))^nを持っているので、導出アプローチを使用することが可能です。 (x(x-1)^6/(x-1))^nは多項式であり、x^sa_s)で係数を探しています。 x^0派生の自由係数(s'th)はs! * a_kです。したがって、0のs'th派生はs! * a_kです。

したがって、この関数をs回導出する必要があります。微分法則を使用して行うことができますが、各微分は「より複雑な」関数を生成するため、カウントアプローチよりもさらに複雑になると思います。 Wolfram Alphaからの最初の3つの派生物は次のとおりです: firstsecond および third

一般的に、私は解を数えることを好みます、そして、mellamokbは素晴らしいアプローチと説明をしました。

2
Ante

最初のロールが2番目のロールで使用できる値を決定し、1番目と2番目のロールの両方が3番目のロールを決定するため、ブルートフォースを行う必要はありません。数十の例を見てみましょう。6をロールすると、10-6=4は、まだ4が必要であることを意味します。 3番目のロールは少なくとも3をカウントする必要があるため、2番目のロールには少なくとも1が必要です。したがって、2番目のロールは1から3になります。 2番目のロールが2で、10-6-2=2があるとすると、3番目のロールIS a 2であり、他に方法はありません。

数十の擬似コード:

tens = 0

for i = [1..6] // first roll can freely go from 1 to 6
   from_j = max(1, 10 - i - 6) // We have the first roll, best case is we roll a 6 in the third roll
   top_j = min(6, 10 - i - 1) // we need to sum 10, minus i, minus at least 1 for the third roll
   for j = [from_j..to_j]
      tens++

各ループは1を加算するため、最後にこのコードが正確に27回ループすることがわかります。

これが18個すべての値のRubyコードです(申し訳ありませんが、Javaではありませんが、簡単に追跡できます)。各ダイスロールを持つことができる値を決定する最小値と最大値に注意してください。

counts = [0] * 18

1.upto(18) do |n|
  from_i = [1, n - 6 - 6].max # best case is we roll 6 in 2nd and 3rd roll
  top_i = [6, n - 1 -1].min # at least 1 for 2nd and 3rd roll
  from_i.upto(top_i) do |i|
    from_j = [1, n - i - 6].max # again we have the 2nd roll defined, best case for 3rd roll is 6
    top_j = [6, n - i -1].min # at least 1 for 3rd roll
    from_j.upto(top_j) do
      # no need to calculate k since it's already defined being n - first roll - second roll
      counts[n-1] += 1
    end
  end
end

print counts

数学的アプローチについては、 https://math.stackexchange.com/questions/4632/how-can-i-algorithmically-count-the-number-of-ways-nm-sided-diceをご覧ください。 -can-add-up-t

2
aromero

チェックアウト モンテカルロ法 通常、入力サイズに比例してスケーリングします。この場合、例は簡単です。一度投げたサイコロは他のサイコロに影響を与えないので、組み合わせを数える代わりに、ランダムに投げたサイコロの面の合計を数えることができると仮定します(十分な回数)。

   public class MonteCarloDice {

    private Map<Integer, Integer> histogram;
    private Random rnd;
    private int nDice;
    private int n;

    public MonteCarloDice(int nDice, int simulations) {
        this.nDice = nDice;
        this.n = simulations;
        this.rnd = new Random();
        this.histogram = new HashMap<>(1000);
        start();
    }

    private void start() {
        for (int simulation = 0; simulation < n; simulation++) {
            int sum = 0;
            for (int i = 0; i < nDice; i++) {
                sum += rnd.nextInt(6) + 1;
            }
            if (histogram.get(sum) == null)
                histogram.put(sum, 0);
            histogram.put(sum, histogram.get(sum) + 1);
        }
        System.out.println(histogram);
    }


    public static void main(String[] args) {
        new MonteCarloDice(3, 100000);
        new MonteCarloDice(10, 1000000);
    }

}

エラーはシミュレーションの数とともに減少しますが、CPU時間は犠牲になりますが、上記の値はかなり高速でした。

3つのサイコロ

{3=498, 4=1392, 5=2702, 6=4549, 7=7041, 8=9844, 9=11583, 10=12310, 11=12469, 12=11594, 13=9697, 14=6999, 15=4677, 17=1395, 16=2790, 18=460}

10個のサイコロ

{13=3, 14=13, 15=40, 17=192, 16=81, 19=769, 18=396, 21=2453, 20=1426, 23=6331, 22=4068, 25=13673, 24=9564, 27=25136, 26=19044, 29=40683, 28=32686, 31=56406, 30=48458, 34=71215, 35=72174, 32=62624, 33=68027, 38=63230, 39=56008, 36=71738, 37=68577, 42=32636, 43=25318, 40=48676, 41=40362, 46=9627, 47=6329, 44=19086, 45=13701, 51=772, 50=1383, 49=2416, 48=3996, 55=31, 54=86, 53=150, 52=406, 59=1, 57=2, 56=7}
1
arynaq