連撃の火力範囲の計算時に中心極限定理を考えないと危ないよという話
(2019.1.10 23時追記):
ご指摘でダメージ計算ミスってることが判明しました。本記事では
攻撃力×1.2倍(エクセ補正)×1.2倍(自力補正)×0.85倍(難関難易度補正)
で計算していますが、正しくは
攻撃力×1.2倍(エクセ補正)×1.2倍(自力補正)×1.80倍(難関難易度補正)×0.75倍(対戦補正)
のようです。そのため、この記事の結論と異なり、たぬたはパレンケを確定で突破できるみたいです。 この記事の計算式は間違っていますが、中心極限定理の重要性を知るには有用だと思うので、あえて間違ったまま記載しておきます。
連撃の確定確率の計算
連撃の確定確率の計算の例をpythonを使って示します。 スキル込み完凸たぬたでスキル込み完凸パレンケ殴った時の確定確率を求めてみます。 自力込み、エクセ回答、攻撃倍率85%とします。
# 必要パッケージのインポート import math from scipy import stats import numpy as np import matplotlib.pyplot as plt from numpy.random import * # 各種変数の設定 ## 自力ボーナス倍率 own_effort_bonus = 1.2 ## エクセボーナス倍率 excellent_bonus = 1.2 ## 攻撃倍率 atack_rate = 0.85 # ゆるの設定 ## たぬたの攻撃力 tanuta_atack = 2726 ## たぬたの連撃数 tanuta_atack_num = 9 ## パレンケのHP parenke_hp = 5258 ## パレンケのカット率 parenke_cut_rate = 0.75 # 属性相性の設定 element_bonus = 0.666
まず、たぬたの9連撃後の攻撃力範囲とパレンケの耐久を示します。 ここで、たぬたの攻撃が一様乱数により0.9~1.1倍の間でばらつくと仮定します。
tanuta_normal_atack = tanuta_atack * own_effort_bonus * excellent_bonus * element_bonus * atack_rate tanuta_min_atack = tanuta_normal_atack * 0.9 tanuta_max_atack = tanuta_normal_atack * 1.1 tanuta_atack_all_min = tanuta_min_atack * tanuta_atack_num tanuta_atack_all_max = tanuta_max_atack * tanuta_atack_num parenke_hp_all = parenke_hp / (1 - parenke_cut_rate) print("たぬたの攻撃範囲:" + str(tanuta_atack_all_min) + "~" + str(tanuta_atack_all_max)) print("パレンケの耐久力:" + str(parenke_hp_all))
たぬたの攻撃範囲:17999.751830399997~21999.6966816
パレンケの耐久力:21032.0
低確率で1撃で倒せそうです。 たぬたの総攻撃力が一様に分布すると仮定した場合の確定確率を示します。
oneshot_prob = (tanuta_atack_all_max - parenke_hp_all) / (tanuta_atack_all_max - tanuta_atack_all_min) print("確定確率:" + str(oneshot_prob * 100) + "%")
確定確率:24.192750590290892%
約24.2%となりました。ここから、連撃によりばらつきが中央に収束することを考慮した確率を求めて比較してみましょう。
まず、分散を求めてみます。一様乱数の定義により、分散は以下で与えられます。
σ2 = (b - a)2 / 12
ここで、bは一様乱数の最大値、aは一様乱数の最小値を表します。 実際に計算してみると、
sigma2 = (tanuta_max_atack - tanuta_min_atack) ** 2 / 12 print("分散:" + str(sigma2))
分散:16460.451453334776
分散が求められたので、中心極限定理を適用してみましょう。中心極限定理では、分散σ2に従う分布から生成される確率変数の部分和が従う近似正規分布の分散は以下の数式で与えられます。
n * σ2
ここでnは標本数、今回の例でいうと連撃数です。では実際に求めてみましょう。
sigma2_gaus = sigma2 * tanuta_atack_num print("疑似正規分布の分散:" + str(sigma2_gaus))
疑似正規分布の分散:148144.06308001297
ここで、平均はたぬたの元の攻撃力×連撃数であるため、合計攻撃力は以下のような正規分布にほぼ従うことになります。
norm_mean = tanuta_normal_atack * tanuta_atack_num norm_var = sigma2_gaus print("N(" + str(norm_mean) + ", " + str(norm_var) + ")")
N(19999.724255999998, 148144.06308001297)
この正規分布の確率密度関数をプロットすると以下のような形になります。
X = np.arange(tanuta_atack_all_min,tanuta_atack_all_max,10) Y= stats.norm.pdf(X, norm_mean, math.sqrt(norm_var)) plt.plot(X,Y,color='r') plt.show()
結構平均値に集中してます。 最後に、この確率分布を用いてパレンケのHPを超える確率を求めてみます。
# pythonではscipyを用いることで一発で累積分布関数を求めることができる # パレンケの耐久を超える確率を取得 norm_sf = stats.norm.sf(parenke_hp_all, norm_mean, math.sqrt(norm_var)) print("確定確率" + str(norm_sf * 100) + "%")
確定確率0.3659522686349882%
1%以下と、かなり低い確率になりました。
最後にこの確率が本当にあっているのか、実際にシミュレーションしてみましょう。試しに10万回パレンケを殴ってみます。
# パレンケ殴った結果を取得する関数 def atack_parenke(): damages = rand(tanuta_atack_num) * (tanuta_max_atack - tanuta_min_atack) + tanuta_min_atack return(parenke_hp_all - sum(damages)) # 10万回パレンケを殴ってみる max_loop = 100000 atack_result = np.array([]) for i_loop in range(max_loop): atack_result = np.append(atack_result, atack_parenke()) one_shot_num = sum(atack_result <= 0) one_shot_prob = one_shot_num / max_loop print("パレンケ1撃だった回数:" + str(one_shot_num) + "回 / " + str(max_loop) + "回") print("パレンケ1撃の確率:" + str(one_shot_prob * 100) + "%")
パレンケ1撃だった回数:292回 / 100000回
パレンケ1撃の確率:0.292%
多少誤差がありますが、大体0.3%くらいとほぼあっています。やはりたぬたでパレンケを割ることはできなさそうです。
取りうる範囲を一様乱数として扱うと、24.2%の確率で1撃と計算されましたが、実際に求めてみると0.3%とほぼ無理な確率でした。 やはり連撃に関しては、ばらつきが中心に収束するということを考慮して計算しないと危険ですね。