LeetCode Weekly 191 - D (組み合わせ数)
組み合わせ数(確率)を求める問題。苦戦したので解法メモを残しておく。
https://leetcode.com/problems/probability-of-a-two-boxes-having-the-same-number-of-distinct-balls/
色1~kで染められたボールが2n個ある。ランダムにn個ずつ2グループに分けたとき、それぞれのグループにある色の種類数が同じになる確率を求めよ。
1 <= k <= 8
1 <= 各色のボールの数 <= 6
たとえば次の場合、
色1: 2個
色2: 1個
色3: 1個
組み合わせは12通りになる。
[1,1 / 2,3], [1,1 / 3,2], [1,2 / 1,3], [1,2 / 3,1], [1,3 / 1,2], [1,3 / 2,1], [2,1 / 1,3], [2,1 / 3,1], [2,3 / 1,1], [3,1 / 1,2], [3,1 / 2,1], [3,2 / 1,1]
このうち8パターンで、各グループにある色の種類数が同じになる。
[1,2 / 1,3], [1,2 / 3,1], [1,3 / 1,2], [1,3 / 2,1], [2,1 / 1,3], [2,1 / 3,1], [3,1 / 1,2], [3,1 / 2,1]
よって確率は8/12=0.6667。
考え方
順番は無視できる。上の例だと
[1,1 / 2,3], [1,2 / 1,3], [1,3 / 1,2], [2,1 / 1,3], [2,3 / 1,1], [3,1 / 1,2],
[1,2 / 1,3], [1,3 / 1,2], [2,1 / 1,3], [3,1 / 1,2],
で4/6=0.6667。
まず分母は全組み合わせ数なのでnCkで求められる。
次にグループ名をA・Bとしたとき、dp[i, j, k, l]を
i: Aに入っているボールの数
j: Bに入っているボールの数
k: 箱Aの色種数
l: 箱Bの色種数
の組み合わせ数と定義する。
色cのボールがn個あるとすると、x: 0~nについて
dp[i + x, j + n - x, k + 1, l + 1] += dp[i, j, k, l] * nCk(n, x);
と更新していくことができる。
最後にdpテーブルから条件に合うものを合算し、分子が求まる。
コード例は配列ではなくDictionaryを使った。
public class Solution { public double GetProbability(int[] balls) { var nCk = GetNcks(); var sum = balls.Sum(); // 分母。全ボールから半分選ぶパターン数 var denominator = GetNck(sum, sum / 2); // i: 箱Aに入っているボールの数 // j: 箱Bに入っているボールの数 // k: 箱Aの色種数 // l: 箱Bの色種数 var dp = new Dictionary<(int i, int j, int k, int l), long>(); dp[(0, 0, 0, 0)] = 1; // c: 色の種類 for (int c = 0; c < balls.Length; c++) { var newDp = new Dictionary<(int i, int j, int k, int l), long>(); var n = balls[c]; for (int x = 0; x <= n; x++) { // ボールn個からx個、箱Aへ入れる foreach(var key in dp.Keys) { if (x == 0) { if (!newDp.ContainsKey((key.i, key.j + n, key.k, key.l + 1))) newDp[(key.i, key.j + n, key.k, key.l + 1)] = 0; newDp[(key.i, key.j + n, key.k, key.l + 1)] += dp[key] * nCk[(n, 0)]; } else if (x == n) { if (!newDp.ContainsKey((key.i + n, key.j, key.k + 1, key.l))) newDp[(key.i + n, key.j, key.k + 1, key.l)] = 0; newDp[(key.i + n, key.j, key.k + 1, key.l)] += dp[key] * nCk[(n, n)]; } else { if (!newDp.ContainsKey((key.i + x, key.j + n - x, key.k + 1, key.l + 1))) newDp[(key.i + x, key.j + n - x, key.k + 1, key.l + 1)] = 0; // 次の状態:箱Aにi+x個、箱Bにj+n-x個、箱Aにk+1種類、箱Bにl+1種類 // nからx選ぶパターン数nCxを掛けて足す newDp[(key.i + x, key.j + n - x, key.k + 1, key.l + 1)] += dp[key] * nCk[(n, x)]; } } } dp = newDp; } // 分子。条件を満たす組み合わせを合算 var numerator = 0L; foreach(var key in dp.Keys) { if (key.Item1 == key.Item2 && key.Item3 == key.Item4) numerator += dp[key]; } return (double)numerator / denominator; } static Dictionary<(int, int), long> GetNcks() { var ret = new Dictionary<(int, int), long>(); // max 6 balls in each color for(int i=1; i<=6; i++) { for(int j=0; j<=i; j++) { ret[(i, j)] = GetNck(i, j); } } return ret; } public static long GetNck(int n, int k) { long val; if (n < k) return 0; k = Math.Min(k, n - k); if (k == 0) val = 1; else val = GetNck(n - 1, k - 1) * n / k; return val; } }