ガチャの当選確率と必要回数

排出率と回数から当たる確率を、目標の確率から必要な回数を出す。

2進小数の誤差0.5 の丸め方

言語ごとの結果

正解表 16件のうち、正解と一致した件数です。

PHPPHP 8.4.26

よくある書き方11 / 16

直した書き方16 / 16

PythonPython 3.14.7

よくある書き方8 / 16

直した書き方16 / 16

GoGo 1.27.2

よくある書き方9 / 16

直した書き方16 / 16

JavaScriptNode 24.15.0

よくある書き方11 / 16

直した書き方16 / 16

正解表と、よくある書き方の答え

正解は実装より先に決めて固定しています。言語の列の ✓ は正解と一致、赤い値はその言語が実際に返した間違った答えです。

ケース入力正解 PHPPythonGoJavaScript
c001基本・1% を100回
mode
prob
rate
1
trials
100
at_least_one
63.397
none
36.603
expected
1.000
✓✓✓✓
c00225% を3回・ちょうど .xxx5 57.8125% と 42.1875% はどちらも2進で正確に表せる .0005 ちょうど。四捨五入なら 57.813、偶数丸めなら 57.812
mode
prob
rate
25
trials
3
at_least_one
57.813
none
42.188
expected
0.750
✓at_least_one"57.812"at_least_one"57.812"✓
c0030.5% を2回・.xxx5 が2進で少し小さくなる 正確には 0.9975%。浮動小数点では 0.99749999… になり 0.997 と出る
mode
prob
rate
0.5
trials
2
at_least_one
0.998
none
99.003
expected
0.010
at_least_one"0.997"at_least_one"0.997"none"99.002"at_least_one"0.997"none"99.002"at_least_one"0.997"none"99.002"
c00450% を7回・.xxx75 99.21875% は2進で正確。四捨五入で 99.219、偶数丸めで 99.218
mode
prob
rate
50
trials
7
at_least_one
99.219
none
0.781
expected
3.500
✓✓✓✓
c005100% 未満だが丸めると 100.000 99.99990…%。元ツールは 99.999%超 と表示するが、本 spec は丸めた値を出す
mode
prob
rate
50
trials
20
at_least_one
100.000
none
0.000
expected
10.000
✓✓✓✓
c006排出率100% 境界値
mode
prob
rate
100
trials
1
at_least_one
100.000
none
0.000
expected
1.000
✓✓✓✓
c0070.01% を10000回 試行回数が大きい。累乗の精度
mode
prob
rate
0.01
trials
10000
at_least_one
63.214
none
36.786
expected
1.000
✓✓✓✓
c008期待回数が .xxx5 ちょうど 5 × 1.25% = 0.0625 回 → 0.063。偶数丸めだと 0.062
mode
prob
rate
1.25
trials
5
at_least_one
6.096
none
93.904
expected
0.063
✓expected"0.062"expected"0.062"✓
c009期待回数 .xxx5 が2進で少し小さくなる 0.0035 回 → 0.004。浮動小数点では 0.0034999… になり 0.003
mode
prob
rate
0.35
trials
1
at_least_one
0.350
none
99.650
expected
0.004
expected"0.003"expected"0.003"expected"0.003"expected"0.003"
c010逆算・20%で36%にちょうど届く 1 − 0.8² = 0.36 ちょうど → 2回。log(0.64)/log(0.8) を浮動小数点で計算すると 2.000…1 になり切り上げて3回
mode
count
rate
20
target
36
trials
2
actual
36.000
previous
20.000
expected
0.400
trials3actual"48.800"previous"36.000"expected"0.600"trials3actual"48.800"previous"36.000"expected"0.600"trials3actual"48.800"previous"36.000"expected"0.600"trials3actual"48.800"previous"36.000"expected"0.600"
c011逆算・10%で27.1%にちょうど届く 1 − 0.9³ = 0.271 ちょうど → 3回。浮動小数点だと4回
mode
count
rate
10
target
27.1
trials
3
actual
27.100
previous
19.000
expected
0.300
trials4actual"34.390"previous"27.100"expected"0.400"trials4actual"34.390"previous"27.100"expected"0.400"trials4actual"34.390"previous"27.100"expected"0.400"trials4actual"34.390"previous"27.100"expected"0.400"
c012逆算・90%で99.9%にちょうど届く 1 − 0.1³ = 0.999 ちょうど → 3回。浮動小数点だと4回
mode
count
rate
90
target
99.9
trials
3
actual
99.900
previous
99.000
expected
2.700
trials4actual"99.990"previous"99.900"expected"3.600"trials4actual"99.990"previous"99.900"expected"3.600"trials4actual"99.990"previous"99.900"expected"3.600"trials4actual"99.990"previous"99.900"expected"3.600"
c013逆算・50%で75%にちょうど届く 1 − 0.5² = 0.75 ちょうど → 2回
mode
count
rate
50
target
75
trials
2
actual
75.000
previous
50.000
expected
1.000
✓✓✓✓
c014逆算・基本(1%で50%)
mode
count
rate
1
target
50
trials
69
actual
50.016
previous
49.511
expected
0.690
✓✓✓✓
c015逆算・基本(3%で90%)
mode
count
rate
3
target
90
trials
76
actual
90.122
previous
89.817
expected
2.280
✓✓✓✓
c016逆算・排出率100% 1 − p = 0。log(0) = −∞ を経由する実装は割り算で −0 などを出しうる。答えは1回
mode
count
rate
100
target
99
trials
1
actual
100.000
previous
0.000
expected
1.000
✓エラー✓✓

コード

「違い」では、よくある書き方から直した書き方へ、どの行をどう変えたかを示しています(赤が消した行、緑が足した行、濃い色が行の中で変わった部分)。計算の部分だけを載せ、入出力を受け渡す部分は除いています。

Gacha.php+47 −13よくある書き方(3f45505)→ 直した書き方

@@ 変更前 3行目〜 / 変更後 3行目〜 @@
33
44 /**
55 * ガチャの当選確率と必要回数。
6− * v1: 素直に書いた版(float と pow / log / ceil、表示は number_format)。
6+ * v2: 確率を「整数 / 10^k」の分数として GMP で正確に計算し、小数3桁への四捨五入も自前で行う。
7+ * 必要回数は log で見当を付け、正確な比較で ±1 補正する(log だけだと境界で1回ずれる)。
78 */
8−function fmt3(float $x): string
9+
10+/** "27.1" → [271, 10] */
11+function parseDecimal(string $s): array
912 {
10− return number_format($x, 3, '.', '');
13+ if (!preg_match('/^(\d+)(?:\.(\d+))?$/', $s, $m)) {
14+ throw new InvalidArgumentException("invalid decimal: {$s}");
15+ }
16+ $frac = $m[2] ?? '';
17+ return [gmp_init($m[1] . $frac, 10), gmp_pow(10, strlen($frac))];
18+}
19+
20+/** 0 以上の num/den を小数3桁に四捨五入した文字列にする */
21+function dec3(GMP $num, GMP $den): string
22+{
23+ $n = gmp_div_q(2 * $num * 1000 + $den, 2 * $den); // floor(x*1000 + 1/2)
24+ return gmp_strval(gmp_div_q($n, 1000)) . '.' . str_pad(gmp_strval(gmp_mod($n, 1000)), 3, '0', STR_PAD_LEFT);
1125 }
1226
1327 /** @return array{at_least_one:string, none:string, expected:string} */
1428 function gachaProb(string $rate, int $trials): array
1529 {
16− $p = (float) $rate / 100;
17− $none = pow(1 - $p, $trials);
18− return ['at_least_one' => fmt3((1 - $none) * 100), 'none' => fmt3($none * 100), 'expected' => fmt3($trials * $p)];
30+ [$rn, $rd] = parseDecimal($rate); // 排出率(%) = rn/rd
31+ $den = $rd * 100; // p = rn/den
32+ $noneNum = gmp_pow($den - $rn, $trials); // (1-p)^N = noneNum/noneDen
33+ $noneDen = gmp_pow($den, $trials);
34+ return [
35+ 'at_least_one' => dec3(($noneDen - $noneNum) * 100, $noneDen),
36+ 'none' => dec3($noneNum * 100, $noneDen),
37+ 'expected' => dec3($rn * $trials, $den),
38+ ];
1939 }
2040
2141 /** @return array{trials:int, actual:string, previous:string, expected:string} */
2242 function gachaCount(string $rate, string $target): array
2343 {
24− $p = (float) $rate / 100;
25− $t = (float) $target / 100;
26− $n = (int) ceil(log(1 - $t) / log(1 - $p));
27− if ($n < 1) {
44+ [$rn, $rd] = parseDecimal($rate);
45+ [$tn, $td] = parseDecimal($target);
46+ $den = $rd * 100;
47+ $tden = $td * 100;
48+ $q = $den - $rn; // 1-p = q/den
49+ // (1-p)^n <= 1-t ⇔ q^n * tden <= (tden - tn) * den^n
50+ $reaches = fn (int $n): bool => gmp_pow($q, $n) * $tden <= ($tden - $tn) * gmp_pow($den, $n);
51+
52+ if (gmp_cmp($q, 0) === 0) {
2853 $n = 1;
54+ } else {
55+ $p = gmp_intval($rn) / gmp_intval($den);
56+ $t = gmp_intval($tn) / gmp_intval($tden);
57+ $n = max((int) ceil(log(1 - $t) / log(1 - $p)), 1);
58+ while ($n > 1 && $reaches($n - 1)) {
59+ $n--;
60+ }
61+ while (!$reaches($n)) {
62+ $n++;
63+ }
2964 }
30− $actual = 1 - pow(1 - $p, $n);
31− $previous = $n > 1 ? 1 - pow(1 - $p, $n - 1) : 0.0;
32− return ['trials' => $n, 'actual' => fmt3($actual * 100), 'previous' => fmt3($previous * 100), 'expected' => fmt3($n * $p)];
65+ $prob = fn (int $k): string => $k === 0 ? '0.000' : dec3((gmp_pow($den, $k) - gmp_pow($q, $k)) * 100, gmp_pow($den, $k));
66+ return ['trials' => $n, 'actual' => $prob($n), 'previous' => $prob($n - 1), 'expected' => dec3($rn * $n, $den)];
3367 }

仕様

モード

確率モード(mode = "prob"): N 回引いて1回以上当たる確率

入力型説明
rate十進文字列排出率(%)。0 より大きく 100 以下
trials整数試行回数 N。1 以上
出力型定義
at_least_one十進文字列(%・小数3桁)1 − (1 − p)^N
none十進文字列(%・小数3桁)(1 − p)^N
expected十進文字列(回・小数3桁)N × p(当たりの期待回数)

逆算モード(mode = "count"): 目標確率以上で1回当てるのに必要な回数

入力型説明
rate十進文字列排出率(%)。0 より大きく 100 以下
target十進文字列目標確率(%)。0 より大きく 100 未満
出力型定義
trials整数1 − (1 − p)^N ≥ t を満たす最小の N(N ≥ 1)
actual十進文字列(%・小数3桁)その N での 1 − (1 − p)^N
previous十進文字列(%・小数3桁)N − 1 回での確率(N = 1 なら 0)
expected十進文字列(回・小数3桁)N × p

p = rate / 100、t = target / 100。

計算の約束

  • 確率は正確な値で比べ、正確な値を丸める。 必要回数は「ちょうど目標に届く」場合を含む(≥)。 例: 排出率20%・目標36% は 1 − 0.8² = 0.36 でちょうど届くので 2回
  • 元ツールは ⌈log(1 − t) / log(1 − p)⌉ で求めている。数学的には同じ値だが、2進浮動小数点で計算すると境界で1回ずれることがある
  • 丸めは小数3桁への四捨五入(0.0005 ちょうどは繰り上げ。偶数丸めではない)。出力は常に小数3桁の文字列("36.000")
  • 排出率100%: 確率モードは 100.000 / 0.000、逆算モードは 1回

元ツールとの違い

  • 元ツールは表示で 99.999%超 / 0.001%未満 に置き換える(UI の工夫)。本 spec は丸めた数値そのものを出力する (例: 50% × 20回 = 99.99990…% → "100.000")
  • 3桁区切り・% 記号は付けない

期待値の作り方

expect は oracle.py で有理数(fractions.Fraction)による定義の直訳から生成した(2026-10-09)。これは正解を決めるための計算であって実装ではない。runner は使わない。各言語の実装はこのファイルを見ずに書く。