言語ごとの結果
正解表 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
正解表と、よくある書き方の答え
正解は実装より先に決めて固定しています。言語の列の ✓ は正解と一致、赤い値はその言語が実際に返した間違った答えです。
| ケース | 入力 | 正解 | PHP | Python | Go | JavaScript |
|---|---|---|---|---|---|---|
| c001基本・1% を100回 |
|
|
✓ | ✓ | ✓ | ✓ |
| c00225% を3回・ちょうど .xxx5 57.8125% と 42.1875% はどちらも2進で正確に表せる .0005 ちょうど。四捨五入なら 57.813、偶数丸めなら 57.812 |
|
|
✓ | at_least_one"57.812" | at_least_one"57.812" | ✓ |
| c0030.5% を2回・.xxx5 が2進で少し小さくなる 正確には 0.9975%。浮動小数点では 0.99749999… になり 0.997 と出る |
|
|
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 |
|
|
✓ | ✓ | ✓ | ✓ |
| c005100% 未満だが丸めると 100.000 99.99990…%。元ツールは 99.999%超 と表示するが、本 spec は丸めた値を出す |
|
|
✓ | ✓ | ✓ | ✓ |
| c006排出率100% 境界値 |
|
|
✓ | ✓ | ✓ | ✓ |
| c0070.01% を10000回 試行回数が大きい。累乗の精度 |
|
|
✓ | ✓ | ✓ | ✓ |
| c008期待回数が .xxx5 ちょうど 5 × 1.25% = 0.0625 回 → 0.063。偶数丸めだと 0.062 |
|
|
✓ | expected"0.062" | expected"0.062" | ✓ |
| c009期待回数 .xxx5 が2進で少し小さくなる 0.0035 回 → 0.004。浮動小数点では 0.0034999… になり 0.003 |
|
|
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回 |
|
|
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回 |
|
|
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回 |
|
|
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回 |
|
|
✓ | ✓ | ✓ | ✓ |
| c014逆算・基本(1%で50%) |
|
|
✓ | ✓ | ✓ | ✓ |
| c015逆算・基本(3%で90%) |
|
|
✓ | ✓ | ✓ | ✓ |
| c016逆算・排出率100% 1 − p = 0。log(0) = −∞ を経由する実装は割り算で −0 などを出しうる。答えは1回 |
|
|
✓ | エラー | ✓ | ✓ |
コード
「違い」では、よくある書き方から直した書き方へ、どの行をどう変えたかを示しています(赤が消した行、緑が足した行、濃い色が行の中で変わった部分)。計算の部分だけを載せ、入出力を受け渡す部分は除いています。
Gacha.php+47 −13よくある書き方(3f45505)→ 直した書き方
| @@ 変更前 3行目〜 / 変更後 3行目〜 @@ | |||
| 3 | 3 | ||
| 4 | 4 | /** | |
| 5 | 5 | * ガチャの当選確率と必要回数。 | |
| 6 | − | * v1: 素直に書いた版(float と pow / log / ceil、表示は number_format)。 | |
| 6 | + | * v2: 確率を「整数 / 10^k」の分数として GMP で正確に計算し、小数3桁への四捨五入も自前で行う。 | |
| 7 | + | * 必要回数は log で見当を付け、正確な比較で ±1 補正する(log だけだと境界で1回ずれる)。 | |
| 7 | 8 | */ | |
| 8 | − | function fmt3(float $x): string | |
| 9 | + | ||
| 10 | + | /** "27.1" → [271, 10] */ | |
| 11 | + | function parseDecimal(string $s): array | |
| 9 | 12 | { | |
| 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); | |
| 11 | 25 | } | |
| 12 | 26 | ||
| 13 | 27 | /** @return array{at_least_one:string, none:string, expected:string} */ | |
| 14 | 28 | function gachaProb(string $rate, int $trials): array | |
| 15 | 29 | { | |
| 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 | + | ]; | |
| 19 | 39 | } | |
| 20 | 40 | ||
| 21 | 41 | /** @return array{trials:int, actual:string, previous:string, expected:string} */ | |
| 22 | 42 | function gachaCount(string $rate, string $target): array | |
| 23 | 43 | { | |
| 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) { | |
| 28 | 53 | $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 | + | } | |
| 29 | 64 | } | |
| 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)]; | |
| 33 | 67 | } |
Gacha.php
<?php
declare(strict_types=1);
/**
* ガチャの当選確率と必要回数。
* v2: 確率を「整数 / 10^k」の分数として GMP で正確に計算し、小数3桁への四捨五入も自前で行う。
* 必要回数は log で見当を付け、正確な比較で ±1 補正する(log だけだと境界で1回ずれる)。
*/
/** "27.1" → [271, 10] */
function parseDecimal(string $s): array
{
if (!preg_match('/^(\d+)(?:\.(\d+))?$/', $s, $m)) {
throw new InvalidArgumentException("invalid decimal: {$s}");
}
$frac = $m[2] ?? '';
return [gmp_init($m[1] . $frac, 10), gmp_pow(10, strlen($frac))];
}
/** 0 以上の num/den を小数3桁に四捨五入した文字列にする */
function dec3(GMP $num, GMP $den): string
{
$n = gmp_div_q(2 * $num * 1000 + $den, 2 * $den); // floor(x*1000 + 1/2)
return gmp_strval(gmp_div_q($n, 1000)) . '.' . str_pad(gmp_strval(gmp_mod($n, 1000)), 3, '0', STR_PAD_LEFT);
}
/** @return array{at_least_one:string, none:string, expected:string} */
function gachaProb(string $rate, int $trials): array
{
[$rn, $rd] = parseDecimal($rate); // 排出率(%) = rn/rd
$den = $rd * 100; // p = rn/den
$noneNum = gmp_pow($den - $rn, $trials); // (1-p)^N = noneNum/noneDen
$noneDen = gmp_pow($den, $trials);
return [
'at_least_one' => dec3(($noneDen - $noneNum) * 100, $noneDen),
'none' => dec3($noneNum * 100, $noneDen),
'expected' => dec3($rn * $trials, $den),
];
}
/** @return array{trials:int, actual:string, previous:string, expected:string} */
function gachaCount(string $rate, string $target): array
{
[$rn, $rd] = parseDecimal($rate);
[$tn, $td] = parseDecimal($target);
$den = $rd * 100;
$tden = $td * 100;
$q = $den - $rn; // 1-p = q/den
// (1-p)^n <= 1-t ⇔ q^n * tden <= (tden - tn) * den^n
$reaches = fn (int $n): bool => gmp_pow($q, $n) * $tden <= ($tden - $tn) * gmp_pow($den, $n);
if (gmp_cmp($q, 0) === 0) {
$n = 1;
} else {
$p = gmp_intval($rn) / gmp_intval($den);
$t = gmp_intval($tn) / gmp_intval($tden);
$n = max((int) ceil(log(1 - $t) / log(1 - $p)), 1);
while ($n > 1 && $reaches($n - 1)) {
$n--;
}
while (!$reaches($n)) {
$n++;
}
}
$prob = fn (int $k): string => $k === 0 ? '0.000' : dec3((gmp_pow($den, $k) - gmp_pow($q, $k)) * 100, gmp_pow($den, $k));
return ['trials' => $n, 'actual' => $prob($n), 'previous' => $prob($n - 1), 'expected' => dec3($rn * $n, $den)];
}
Gacha.phpコミット 3f45505
<?php
declare(strict_types=1);
/**
* ガチャの当選確率と必要回数。
* v1: 素直に書いた版(float と pow / log / ceil、表示は number_format)。
*/
function fmt3(float $x): string
{
return number_format($x, 3, '.', '');
}
/** @return array{at_least_one:string, none:string, expected:string} */
function gachaProb(string $rate, int $trials): array
{
$p = (float) $rate / 100;
$none = pow(1 - $p, $trials);
return ['at_least_one' => fmt3((1 - $none) * 100), 'none' => fmt3($none * 100), 'expected' => fmt3($trials * $p)];
}
/** @return array{trials:int, actual:string, previous:string, expected:string} */
function gachaCount(string $rate, string $target): array
{
$p = (float) $rate / 100;
$t = (float) $target / 100;
$n = (int) ceil(log(1 - $t) / log(1 - $p));
if ($n < 1) {
$n = 1;
}
$actual = 1 - pow(1 - $p, $n);
$previous = $n > 1 ? 1 - pow(1 - $p, $n - 1) : 0.0;
return ['trials' => $n, 'actual' => fmt3($actual * 100), 'previous' => fmt3($previous * 100), 'expected' => fmt3($n * $p)];
}
gacha.py+28 −9よくある書き方(3f45505)→ 直した書き方
| @@ 変更前 1行目〜 / 変更後 1行目〜 @@ | |||
| 1 | 1 | """ガチャの当選確率と必要回数。 | |
| 2 | 2 | ||
| 3 | − | v1: 素直に書いた版(float と ** / math.log / math.ceil、表示は f"{x:.3f}")。 | |
| 3 | + | v2: fractions.Fraction で正確に計算し、小数3桁への四捨五入も自前で行う | |
| 4 | + | (f"{x:.3f}" は2進の値を偶数丸めするため使わない)。 | |
| 5 | + | 必要回数は log で見当を付け、正確な比較で ±1 補正する(log だけだと境界で1回ずれる)。 | |
| 4 | 6 | """ | |
| 5 | 7 | import math | |
| 8 | + | from fractions import Fraction | |
| 9 | + | ||
| 10 | + | ||
| 11 | + | def dec3(x: Fraction) -> str: | |
| 12 | + | """0 以上の有理数を小数3桁に四捨五入した文字列にする。""" | |
| 13 | + | n = math.floor(x * 1000 + Fraction(1, 2)) | |
| 14 | + | return f"{n // 1000}.{n % 1000:03d}" | |
| 6 | 15 | ||
| 7 | 16 | ||
| 8 | 17 | def gacha_prob(rate: str, trials: int) -> dict: | |
| 9 | − | p = float(rate) / 100 | |
| 18 | + | p = Fraction(rate) / 100 | |
| 10 | 19 | none = (1 - p) ** trials | |
| 11 | − | return {"at_least_one": f"{(1 - none) * 100:.3f}", "none": f"{none * 100:.3f}", "expected": f"{trials * p:.3f}"} | |
| 20 | + | return {"at_least_one": dec3((1 - none) * 100), "none": dec3(none * 100), "expected": dec3(trials * p)} | |
| 12 | 21 | ||
| 13 | 22 | ||
| 14 | 23 | def gacha_count(rate: str, target: str) -> dict: | |
| 15 | − | p = float(rate) / 100 | |
| 16 | − | t = float(target) / 100 | |
| 17 | − | n = max(math.ceil(math.log(1 - t) / math.log(1 - p)), 1) | |
| 18 | − | actual = 1 - (1 - p) ** n | |
| 19 | − | previous = 1 - (1 - p) ** (n - 1) if n > 1 else 0.0 | |
| 20 | − | return {"trials": n, "actual": f"{actual * 100:.3f}", "previous": f"{previous * 100:.3f}", "expected": f"{n * p:.3f}"} | |
| 24 | + | p = Fraction(rate) / 100 | |
| 25 | + | t = Fraction(target) / 100 | |
| 26 | + | ||
| 27 | + | def reaches(n: int) -> bool: | |
| 28 | + | return (1 - p) ** n <= 1 - t | |
| 29 | + | ||
| 30 | + | if p == 1: | |
| 31 | + | n = 1 | |
| 32 | + | else: | |
| 33 | + | n = max(math.ceil(math.log(1 - float(t)) / math.log(1 - float(p))), 1) | |
| 34 | + | while n > 1 and reaches(n - 1): | |
| 35 | + | n -= 1 | |
| 36 | + | while not reaches(n): | |
| 37 | + | n += 1 | |
| 38 | + | previous = 1 - (1 - p) ** (n - 1) if n > 1 else Fraction(0) | |
| 39 | + | return {"trials": n, "actual": dec3((1 - (1 - p) ** n) * 100), "previous": dec3(previous * 100), "expected": dec3(n * p)} |
gacha.py
"""ガチャの当選確率と必要回数。
v2: fractions.Fraction で正確に計算し、小数3桁への四捨五入も自前で行う
(f"{x:.3f}" は2進の値を偶数丸めするため使わない)。
必要回数は log で見当を付け、正確な比較で ±1 補正する(log だけだと境界で1回ずれる)。
"""
import math
from fractions import Fraction
def dec3(x: Fraction) -> str:
"""0 以上の有理数を小数3桁に四捨五入した文字列にする。"""
n = math.floor(x * 1000 + Fraction(1, 2))
return f"{n // 1000}.{n % 1000:03d}"
def gacha_prob(rate: str, trials: int) -> dict:
p = Fraction(rate) / 100
none = (1 - p) ** trials
return {"at_least_one": dec3((1 - none) * 100), "none": dec3(none * 100), "expected": dec3(trials * p)}
def gacha_count(rate: str, target: str) -> dict:
p = Fraction(rate) / 100
t = Fraction(target) / 100
def reaches(n: int) -> bool:
return (1 - p) ** n <= 1 - t
if p == 1:
n = 1
else:
n = max(math.ceil(math.log(1 - float(t)) / math.log(1 - float(p))), 1)
while n > 1 and reaches(n - 1):
n -= 1
while not reaches(n):
n += 1
previous = 1 - (1 - p) ** (n - 1) if n > 1 else Fraction(0)
return {"trials": n, "actual": dec3((1 - (1 - p) ** n) * 100), "previous": dec3(previous * 100), "expected": dec3(n * p)}
gacha.pyコミット 3f45505
"""ガチャの当選確率と必要回数。
v1: 素直に書いた版(float と ** / math.log / math.ceil、表示は f"{x:.3f}")。
"""
import math
def gacha_prob(rate: str, trials: int) -> dict:
p = float(rate) / 100
none = (1 - p) ** trials
return {"at_least_one": f"{(1 - none) * 100:.3f}", "none": f"{none * 100:.3f}", "expected": f"{trials * p:.3f}"}
def gacha_count(rate: str, target: str) -> dict:
p = float(rate) / 100
t = float(target) / 100
n = max(math.ceil(math.log(1 - t) / math.log(1 - p)), 1)
actual = 1 - (1 - p) ** n
previous = 1 - (1 - p) ** (n - 1) if n > 1 else 0.0
return {"trials": n, "actual": f"{actual * 100:.3f}", "previous": f"{previous * 100:.3f}", "expected": f"{n * p:.3f}"}
gacha.go+55 −19よくある書き方(3f45505)→ 直した書き方
| @@ 変更前 1行目〜 / 変更後 1行目〜 @@ | |||
| 1 | 1 | package main | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "math" | |
| 5 | − | "strconv" | |
| 6 | + | "math/big" | |
| 6 | 7 | ) | |
| 7 | 8 | ||
| 8 | − | // v1: 素直に書いた版(float64 と math.Pow / math.Log / math.Ceil、表示は strconv.FormatFloat)。 | |
| 9 | − | func fmt3(x float64) string { return strconv.FormatFloat(x, 'f', 3, 64) } | |
| 9 | + | // v2: math/big.Rat で正確に計算し、小数3桁への四捨五入も自前で行う | |
| 10 | + | // (strconv.FormatFloat は2進の値を偶数丸めするため使わない)。 | |
| 11 | + | // 必要回数は math.Log で見当を付け、正確な比較で ±1 補正する(log だけだと境界で1回ずれる)。 | |
| 12 | + | ||
| 13 | + | // dec3 は 0 以上の有理数を小数3桁に四捨五入した文字列にする。 | |
| 14 | + | func dec3(x *big.Rat) string { | |
| 15 | + | v := new(big.Rat).Mul(x, big.NewRat(1000, 1)) | |
| 16 | + | v.Add(v, big.NewRat(1, 2)) | |
| 17 | + | n := new(big.Int).Quo(v.Num(), v.Denom()) // 0 以上なので Quo は floor | |
| 18 | + | q, r := new(big.Int).QuoRem(n, big.NewInt(1000), new(big.Int)) | |
| 19 | + | return fmt.Sprintf("%s.%03d", q, r.Int64()) | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | func parseRate(s string) (*big.Rat, error) { | |
| 23 | + | r, ok := new(big.Rat).SetString(s) | |
| 24 | + | if !ok { | |
| 25 | + | return nil, fmt.Errorf("invalid decimal: %q", s) | |
| 26 | + | } | |
| 27 | + | return r.Quo(r, big.NewRat(100, 1)), nil | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | func powRat(x *big.Rat, n int) *big.Rat { | |
| 31 | + | e := big.NewInt(int64(n)) | |
| 32 | + | return new(big.Rat).SetFrac(new(big.Int).Exp(x.Num(), e, nil), new(big.Int).Exp(x.Denom(), e, nil)) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | var one = big.NewRat(1, 1) | |
| 36 | + | ||
| 37 | + | func sub(a, b *big.Rat) *big.Rat { return new(big.Rat).Sub(a, b) } | |
| 38 | + | func percent(x *big.Rat) string { return dec3(new(big.Rat).Mul(x, big.NewRat(100, 1))) } | |
| 10 | 39 | ||
| 11 | 40 | // GachaProb は N 回引いて1回以上当たる確率を求める。 | |
| 12 | 41 | func GachaProb(rate string, trials int) (map[string]any, error) { | |
| 13 | − | r, err := strconv.ParseFloat(rate, 64) | |
| 42 | + | p, err := parseRate(rate) | |
| 14 | 43 | if err != nil { | |
| 15 | 44 | return nil, err | |
| 16 | 45 | } | |
| 17 | − | p := r / 100 | |
| 18 | − | none := math.Pow(1-p, float64(trials)) | |
| 19 | − | return map[string]any{"at_least_one": fmt3((1 - none) * 100), "none": fmt3(none * 100), "expected": fmt3(float64(trials) * p)}, nil | |
| 46 | + | none := powRat(sub(one, p), trials) | |
| 47 | + | expected := new(big.Rat).Mul(p, big.NewRat(int64(trials), 1)) | |
| 48 | + | return map[string]any{"at_least_one": percent(sub(one, none)), "none": percent(none), "expected": dec3(expected)}, nil | |
| 20 | 49 | } | |
| 21 | 50 | ||
| 22 | 51 | // GachaCount は目標確率以上で当てるのに必要な回数を求める。 | |
| 23 | 52 | func GachaCount(rate, target string) (map[string]any, error) { | |
| 24 | − | r, err := strconv.ParseFloat(rate, 64) | |
| 53 | + | p, err := parseRate(rate) | |
| 25 | 54 | if err != nil { | |
| 26 | 55 | return nil, err | |
| 27 | 56 | } | |
| 28 | − | tp, err := strconv.ParseFloat(target, 64) | |
| 57 | + | t, err := parseRate(target) | |
| 29 | 58 | if err != nil { | |
| 30 | 59 | return nil, err | |
| 31 | 60 | } | |
| 32 | − | p, t := r/100, tp/100 | |
| 33 | − | n := int(math.Ceil(math.Log(1-t) / math.Log(1-p))) | |
| 34 | − | if n < 1 { | |
| 35 | − | n = 1 | |
| 61 | + | q, rest := sub(one, p), sub(one, t) | |
| 62 | + | reaches := func(n int) bool { return powRat(q, n).Cmp(rest) <= 0 } | |
| 63 | + | ||
| 64 | + | n := 1 | |
| 65 | + | if q.Sign() != 0 { | |
| 66 | + | pf, _ := p.Float64() | |
| 67 | + | tf, _ := t.Float64() | |
| 68 | + | n = max(int(math.Ceil(math.Log(1-tf)/math.Log(1-pf))), 1) | |
| 69 | + | for n > 1 && reaches(n-1) { | |
| 70 | + | n-- | |
| 71 | + | } | |
| 72 | + | for !reaches(n) { | |
| 73 | + | n++ | |
| 74 | + | } | |
| 36 | 75 | } | |
| 37 | − | actual := 1 - math.Pow(1-p, float64(n)) | |
| 38 | − | previous := 0.0 | |
| 39 | − | if n > 1 { | |
| 40 | − | previous = 1 - math.Pow(1-p, float64(n-1)) | |
| 41 | − | } | |
| 42 | − | return map[string]any{"trials": n, "actual": fmt3(actual * 100), "previous": fmt3(previous * 100), "expected": fmt3(float64(n) * p)}, nil | |
| 76 | + | prob := func(k int) string { return percent(sub(one, powRat(q, k))) } | |
| 77 | + | expected := new(big.Rat).Mul(p, big.NewRat(int64(n), 1)) | |
| 78 | + | return map[string]any{"trials": n, "actual": prob(n), "previous": prob(n - 1), "expected": dec3(expected)}, nil | |
| 43 | 79 | } |
gacha.go
package main
import (
"fmt"
"math"
"math/big"
)
// v2: math/big.Rat で正確に計算し、小数3桁への四捨五入も自前で行う
// (strconv.FormatFloat は2進の値を偶数丸めするため使わない)。
// 必要回数は math.Log で見当を付け、正確な比較で ±1 補正する(log だけだと境界で1回ずれる)。
// dec3 は 0 以上の有理数を小数3桁に四捨五入した文字列にする。
func dec3(x *big.Rat) string {
v := new(big.Rat).Mul(x, big.NewRat(1000, 1))
v.Add(v, big.NewRat(1, 2))
n := new(big.Int).Quo(v.Num(), v.Denom()) // 0 以上なので Quo は floor
q, r := new(big.Int).QuoRem(n, big.NewInt(1000), new(big.Int))
return fmt.Sprintf("%s.%03d", q, r.Int64())
}
func parseRate(s string) (*big.Rat, error) {
r, ok := new(big.Rat).SetString(s)
if !ok {
return nil, fmt.Errorf("invalid decimal: %q", s)
}
return r.Quo(r, big.NewRat(100, 1)), nil
}
func powRat(x *big.Rat, n int) *big.Rat {
e := big.NewInt(int64(n))
return new(big.Rat).SetFrac(new(big.Int).Exp(x.Num(), e, nil), new(big.Int).Exp(x.Denom(), e, nil))
}
var one = big.NewRat(1, 1)
func sub(a, b *big.Rat) *big.Rat { return new(big.Rat).Sub(a, b) }
func percent(x *big.Rat) string { return dec3(new(big.Rat).Mul(x, big.NewRat(100, 1))) }
// GachaProb は N 回引いて1回以上当たる確率を求める。
func GachaProb(rate string, trials int) (map[string]any, error) {
p, err := parseRate(rate)
if err != nil {
return nil, err
}
none := powRat(sub(one, p), trials)
expected := new(big.Rat).Mul(p, big.NewRat(int64(trials), 1))
return map[string]any{"at_least_one": percent(sub(one, none)), "none": percent(none), "expected": dec3(expected)}, nil
}
// GachaCount は目標確率以上で当てるのに必要な回数を求める。
func GachaCount(rate, target string) (map[string]any, error) {
p, err := parseRate(rate)
if err != nil {
return nil, err
}
t, err := parseRate(target)
if err != nil {
return nil, err
}
q, rest := sub(one, p), sub(one, t)
reaches := func(n int) bool { return powRat(q, n).Cmp(rest) <= 0 }
n := 1
if q.Sign() != 0 {
pf, _ := p.Float64()
tf, _ := t.Float64()
n = max(int(math.Ceil(math.Log(1-tf)/math.Log(1-pf))), 1)
for n > 1 && reaches(n-1) {
n--
}
for !reaches(n) {
n++
}
}
prob := func(k int) string { return percent(sub(one, powRat(q, k))) }
expected := new(big.Rat).Mul(p, big.NewRat(int64(n), 1))
return map[string]any{"trials": n, "actual": prob(n), "previous": prob(n - 1), "expected": dec3(expected)}, nil
}
gacha.goコミット 3f45505
package main
import (
"math"
"strconv"
)
// v1: 素直に書いた版(float64 と math.Pow / math.Log / math.Ceil、表示は strconv.FormatFloat)。
func fmt3(x float64) string { return strconv.FormatFloat(x, 'f', 3, 64) }
// GachaProb は N 回引いて1回以上当たる確率を求める。
func GachaProb(rate string, trials int) (map[string]any, error) {
r, err := strconv.ParseFloat(rate, 64)
if err != nil {
return nil, err
}
p := r / 100
none := math.Pow(1-p, float64(trials))
return map[string]any{"at_least_one": fmt3((1 - none) * 100), "none": fmt3(none * 100), "expected": fmt3(float64(trials) * p)}, nil
}
// GachaCount は目標確率以上で当てるのに必要な回数を求める。
func GachaCount(rate, target string) (map[string]any, error) {
r, err := strconv.ParseFloat(rate, 64)
if err != nil {
return nil, err
}
tp, err := strconv.ParseFloat(target, 64)
if err != nil {
return nil, err
}
p, t := r/100, tp/100
n := int(math.Ceil(math.Log(1-t) / math.Log(1-p)))
if n < 1 {
n = 1
}
actual := 1 - math.Pow(1-p, float64(n))
previous := 0.0
if n > 1 {
previous = 1 - math.Pow(1-p, float64(n-1))
}
return map[string]any{"trials": n, "actual": fmt3(actual * 100), "previous": fmt3(previous * 100), "expected": fmt3(float64(n) * p)}, nil
}
gacha.mjs+47 −11よくある書き方(3f45505)→ 直した書き方
| @@ 変更前 1行目〜 / 変更後 1行目〜 @@ | |||
| 1 | 1 | // ガチャの当選確率と必要回数。 | |
| 2 | − | // v1: 素直に書いた版(Number と Math.pow / Math.log / Math.ceil、表示は toFixed(3))。 | |
| 2 | + | // v2: 確率を「BigInt / 10^k」の分数で正確に計算し、小数3桁への四捨五入も自前で行う | |
| 3 | + | // (toFixed は2進の値を丸めるため 0.9975 を 0.997 にする)。 | |
| 4 | + | // 必要回数は Math.log で見当を付け、正確な比較で ±1 補正する(log だけだと境界で1回ずれる)。 | |
| 5 | + | ||
| 6 | + | /** "27.1" → { n: 271n, d: 10n } */ | |
| 7 | + | function parseDecimal(s) { | |
| 8 | + | const m = /^(\d+)(?:\.(\d+))?$/.exec(s); | |
| 9 | + | if (!m) throw new RangeError(`invalid decimal: ${s}`); | |
| 10 | + | const frac = m[2] ?? ""; | |
| 11 | + | return { n: BigInt(m[1] + frac), d: 10n ** BigInt(frac.length) }; | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | /** 0 以上の num/den を小数3桁に四捨五入した文字列にする */ | |
| 15 | + | function dec3(num, den) { | |
| 16 | + | const n = (2n * num * 1000n + den) / (2n * den); // floor(x*1000 + 1/2) | |
| 17 | + | return `${n / 1000n}.${String(n % 1000n).padStart(3, "0")}`; | |
| 18 | + | } | |
| 3 | 19 | ||
| 4 | 20 | export function gachaProb({ rate, trials }) { | |
| 5 | − | const p = Number(rate) / 100; | |
| 6 | − | const none = Math.pow(1 - p, trials); | |
| 7 | − | return { at_least_one: ((1 - none) * 100).toFixed(3), none: (none * 100).toFixed(3), expected: (trials * p).toFixed(3) }; | |
| 21 | + | const r = parseDecimal(rate); | |
| 22 | + | const den = r.d * 100n; // p = r.n / den | |
| 23 | + | const N = BigInt(trials); | |
| 24 | + | const noneNum = (den - r.n) ** N; | |
| 25 | + | const noneDen = den ** N; | |
| 26 | + | return { | |
| 27 | + | at_least_one: dec3((noneDen - noneNum) * 100n, noneDen), | |
| 28 | + | none: dec3(noneNum * 100n, noneDen), | |
| 29 | + | expected: dec3(r.n * N, den), | |
| 30 | + | }; | |
| 8 | 31 | } | |
| 9 | 32 | ||
| 10 | 33 | export function gachaCount({ rate, target }) { | |
| 11 | − | const p = Number(rate) / 100; | |
| 12 | − | const t = Number(target) / 100; | |
| 13 | − | let n = Math.ceil(Math.log(1 - t) / Math.log(1 - p)); | |
| 14 | − | if (n < 1) n = 1; | |
| 15 | − | const actual = 1 - Math.pow(1 - p, n); | |
| 16 | − | const previous = n > 1 ? 1 - Math.pow(1 - p, n - 1) : 0; | |
| 17 | − | return { trials: n, actual: (actual * 100).toFixed(3), previous: (previous * 100).toFixed(3), expected: (n * p).toFixed(3) }; | |
| 34 | + | const r = parseDecimal(rate); | |
| 35 | + | const t = parseDecimal(target); | |
| 36 | + | const den = r.d * 100n; | |
| 37 | + | const tden = t.d * 100n; | |
| 38 | + | const q = den - r.n; // 1-p = q/den | |
| 39 | + | // (1-p)^n <= 1-t ⇔ q^n * tden <= (tden - t.n) * den^n | |
| 40 | + | const reaches = (n) => q ** BigInt(n) * tden <= (tden - t.n) * den ** BigInt(n); | |
| 41 | + | ||
| 42 | + | let n; | |
| 43 | + | if (q === 0n) { | |
| 44 | + | n = 1; | |
| 45 | + | } else { | |
| 46 | + | const pf = Number(r.n) / Number(den); | |
| 47 | + | const tf = Number(t.n) / Number(tden); | |
| 48 | + | n = Math.max(Math.ceil(Math.log(1 - tf) / Math.log(1 - pf)), 1); | |
| 49 | + | while (n > 1 && reaches(n - 1)) n--; | |
| 50 | + | while (!reaches(n)) n++; | |
| 51 | + | } | |
| 52 | + | const prob = (k) => (k === 0 ? "0.000" : dec3((den ** BigInt(k) - q ** BigInt(k)) * 100n, den ** BigInt(k))); | |
| 53 | + | return { trials: n, actual: prob(n), previous: prob(n - 1), expected: dec3(r.n * BigInt(n), den) }; | |
| 18 | 54 | } |
gacha.mjs
// ガチャの当選確率と必要回数。
// v2: 確率を「BigInt / 10^k」の分数で正確に計算し、小数3桁への四捨五入も自前で行う
// (toFixed は2進の値を丸めるため 0.9975 を 0.997 にする)。
// 必要回数は Math.log で見当を付け、正確な比較で ±1 補正する(log だけだと境界で1回ずれる)。
/** "27.1" → { n: 271n, d: 10n } */
function parseDecimal(s) {
const m = /^(\d+)(?:\.(\d+))?$/.exec(s);
if (!m) throw new RangeError(`invalid decimal: ${s}`);
const frac = m[2] ?? "";
return { n: BigInt(m[1] + frac), d: 10n ** BigInt(frac.length) };
}
/** 0 以上の num/den を小数3桁に四捨五入した文字列にする */
function dec3(num, den) {
const n = (2n * num * 1000n + den) / (2n * den); // floor(x*1000 + 1/2)
return `${n / 1000n}.${String(n % 1000n).padStart(3, "0")}`;
}
export function gachaProb({ rate, trials }) {
const r = parseDecimal(rate);
const den = r.d * 100n; // p = r.n / den
const N = BigInt(trials);
const noneNum = (den - r.n) ** N;
const noneDen = den ** N;
return {
at_least_one: dec3((noneDen - noneNum) * 100n, noneDen),
none: dec3(noneNum * 100n, noneDen),
expected: dec3(r.n * N, den),
};
}
export function gachaCount({ rate, target }) {
const r = parseDecimal(rate);
const t = parseDecimal(target);
const den = r.d * 100n;
const tden = t.d * 100n;
const q = den - r.n; // 1-p = q/den
// (1-p)^n <= 1-t ⇔ q^n * tden <= (tden - t.n) * den^n
const reaches = (n) => q ** BigInt(n) * tden <= (tden - t.n) * den ** BigInt(n);
let n;
if (q === 0n) {
n = 1;
} else {
const pf = Number(r.n) / Number(den);
const tf = Number(t.n) / Number(tden);
n = Math.max(Math.ceil(Math.log(1 - tf) / Math.log(1 - pf)), 1);
while (n > 1 && reaches(n - 1)) n--;
while (!reaches(n)) n++;
}
const prob = (k) => (k === 0 ? "0.000" : dec3((den ** BigInt(k) - q ** BigInt(k)) * 100n, den ** BigInt(k)));
return { trials: n, actual: prob(n), previous: prob(n - 1), expected: dec3(r.n * BigInt(n), den) };
}
gacha.mjsコミット 3f45505
// ガチャの当選確率と必要回数。
// v1: 素直に書いた版(Number と Math.pow / Math.log / Math.ceil、表示は toFixed(3))。
export function gachaProb({ rate, trials }) {
const p = Number(rate) / 100;
const none = Math.pow(1 - p, trials);
return { at_least_one: ((1 - none) * 100).toFixed(3), none: (none * 100).toFixed(3), expected: (trials * p).toFixed(3) };
}
export function gachaCount({ rate, target }) {
const p = Number(rate) / 100;
const t = Number(target) / 100;
let n = Math.ceil(Math.log(1 - t) / Math.log(1 - p));
if (n < 1) n = 1;
const actual = 1 - Math.pow(1 - p, n);
const previous = n > 1 ? 1 - Math.pow(1 - p, n - 1) : 0;
return { trials: n, actual: (actual * 100).toFixed(3), previous: (previous * 100).toFixed(3), expected: (n * p).toFixed(3) };
}
仕様
モード
確率モード(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 は使わない。各言語の実装はこのファイルを見ずに書く。