静かなる名辞

pythonとプログラミングのこと



百発百中の砲一門と百発一中の砲百門について計算してみる

はじめに

 「百発百中の砲一門は百発一中の砲百門に勝る」という東郷平八郎のものとされる発言があります。

 発言の真偽や是非、ましてや東郷や旧日本海軍について何か述べようという訳ではありません。確率論的にどんな感じになるのかを考えてみようという試みです。

 参考リンク
AXION's Physical Laboratory -Analysis(No.001) contents page-

理論的な解析

  • 問題設定

 百発百中の砲一門と百発一中の砲百門が向かい合い、どちらかが全滅するまで同じ発射速度で撃ち合います。

私が一門の側の砲手なら即逃げる
私が一門の側の砲手なら即逃げる

 百発百中の砲一門の側が圧倒的不利であることの直感的な説明は、以下の通りです。

 簡単のためにターン制のゲームとして捉えます。1ターン目で、百発百中の砲は相手の砲を1減らします。一方、百発一中の砲百門の側は期待値は1門撃破。つまり運が悪ければ百発百中の砲は一ターン目で死にます。

 運良く一ターン目をしのいだとしましょう。百発一中の砲は99門残存しています。百発一中の砲百門の側の期待値はほとんど下がりません。……ということを100ターン続けないと勝てません。

 確率で考えてみましょう。百発一中の砲100門は1ターンに1門ずつ減らされていきます。ということは、(いつまでも百発一中側が命中を出さなかったとして)各ターンの発射数は100発,99発,98発,...,1発と減っていきます。要するに最大で5050回の砲撃が可能なのですが、これがすべて外れる確率を計算すれば百発百中の砲一門の勝率が出ます。……ということで計算すると、 0.99^{5050} \fallingdotseq 9.07 \times 10^{-23}で、ほとんど勝ち目はありません。

シミュレーション

 シミュレーションやってみるか・・・と思ったのですが、なにせ 10^{-23}なので 10^{23}回くらい回さないと百発百中の砲一門が勝つ場面が見れません。仕方がないので妥協して 10^5回回してみました。

import numpy as np
import matplotlib.pyplot as plt

def sim():
    team_A = 1
    team_B = 100
    
    while team_A > 0 and team_B > 0:
        hit_B = np.random.binomial(n=team_B, p=0.01)
        team_B -= 1
        if hit_B > 0:
            team_A -= 1
            break
    return team_A, team_B

def main():
    result = []
    for _ in range(10**6):
        result.append(sim())
    print(min(result))
    print(np.mean(result, axis=0))
    plt.hist([x[1] for x in result])
    plt.yscale("log")
    plt.savefig("result1.png")

if __name__ == "__main__":
    main()

 百発百中の砲一門は敵を84門まで削ったのが最高記録でした。試合終了時の百発一中サイドの門数は98.4くらいが平均のようです。何かちゃんと計算すればこの数字は出るのでしょう(たぶん)。

終了時の百発一中側残存数の分布
終了時の百発一中側残存数の分布

 対数目盛注意。まあ、無理ゲーだよね……。

百発百中の砲を何門持ってくれば勝てるのか

 ここまではやる前からわかっていた話です。なんとか百発百中の砲が勝つのを見てみたいので、その条件を探ります。

 たとえば百発百中の砲2門vs百発一中の砲100門ではどうでしょう。百発百中の砲3門vs百発一中の砲100門では? 百発百中の砲2門vs百発一中の砲200門とかもあるかもしれません。

 たぶん、頑張れば計算できるのだと思います。が、この場合、双方の砲の数が変化していく過程を計算しないといけないので、ちょっと面倒くさいという問題があります。だるいのでシミュレーションに丸投げします。

 シミュレーションのコードを書く前に考えるべきこととしては、百発一中の砲は撃ち方を選べるということです。敵の砲が2門あったとして、50門ずつそれぞれに振り向けるか、100門を片方に指向するか。どちらが良いでしょうか?

 現実の戦争だと相手の混乱・妨害を狙って分火した方が良いという話もあるそうですが、ここでは純粋に確率論的に考えます。命中率1%を100発撃つ期待値は当然1です。ということは、期待値が出て1目標しか撃破できない訳です。

 もし期待値が2とか3なら、本来は2門とか3門撃破できる火力を1門潰すのにつぎ込む訳で、どう考えても損です。2分割、3分割して撃った方が良いでしょう。

 では百発一中の砲が50門に減ってしまったら? これは極論すればどう撃っても同じではないでしょうか。

 今回の条件では期待値が1を超えることはないので、百発一中側は毎回全砲門を1門に指向することにします。こうするとシミュレーションが単純になり、お得です。命中が1以上出れば1門撃破です。

from collections import Counter
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def sim(A_n, B_n):
    while A_n > 0 and B_n > 0:
        hit_B = np.random.binomial(n=B_n, p=0.01)
        B_n -= A_n
        if hit_B > 0:
            A_n -= 1
    if B_n < 0:
        B_n = 0
    return A_n, B_n

def winner(AB):
    if AB[0] == AB[1]:
        return "-"
    elif AB[0] > AB[1]:
        return "A"
    else:
        return "B"

def main():
    n_AB = [(1, 100), (2, 100), (3, 100), (4, 100), (5, 100), 
            (6, 100), (7, 100), (8, 100), (9, 100), (10, 100),
            (11, 100), (12, 100), (13, 100), (14, 100), (15, 100), 
            (16, 100), (17, 100), (18, 100), (19, 100), (20, 100)]

    lst = []
    for A_n, B_n in n_AB:
        result = []
        for _ in range(10**4):
            result.append(sim(A_n, B_n))
        counter = Counter([winner(x) for x in result])
        lst.append([counter[x] for x in ["A", "B", "-"]])
    df = pd.DataFrame(lst, columns=["百発百中", "百発一中", "引き分け"], index=list(zip(*n_AB))[0])/10**4
    ax = df.plot()
    ax.set_xticks(df.index)
    ax.set_xlabel("百発百中の砲の門数(百発一中の砲は100門で固定)")
    ax.set_ylabel("勝率")
    plt.savefig("result2.png")

if __name__ == "__main__":
    main()

シミュレーション結果
シミュレーション結果

 この結果からわかることは、百発百中の砲9門は百発一中の砲100門に勝てるということのようです。直感的にもなんとなくそんな気はします(うまく説明できないが、互いに敵を1割くらいずつ削れる数字がそのあたりにありそう)。

結論

 百発百中の砲で百発一中の砲100門に勝とうとすると9門要る。