Hatena::ブログ(Diary)

はてなの鴨澤 Twitter

2017-07-19 百発百中の砲なぞで戦おうとしてはいけない このエントリーを含むブックマーク このエントリーのブックマークコメント

日露戦争終結時の連合艦隊解散の辞に、こういう一節がある:

武力なるものは艦船兵器等のみにあらすして之を活用する無形の実力に在り百發百中の一砲能く百發一中の敵砲百門に對抗し得るを覺らは我等軍人は主として武力を形而上に求めさるへからす

武力とは船や兵器だけじゃなくこれを活用する無形の実力にあり、百発百中の砲一門は百発一中の敵砲百門に対抗しうることを自覚し、われわれ軍人は武力を主として形而上的なものとして求めるべきで、そうでなくてはならないのだ。」という意味である。


後年の帝国海軍の「月月火水木金金」といわれた猛訓練の精神的基礎となったこの言葉は、言わんとするところは理解できるものの、全体としては小学生でもわかるくらい間違ってる。百発百中の砲1門と百発一中の砲100門が撃ち合えば、味方の砲は1度の射撃で破壊され、敵の砲を1門破壊したとしても残りが99門もあって、戦力は1%しかダウンしない。完敗である。


これを知ったときは小学生だったので、一番強い動物はライオンか虎かと知りたがるがごとく、「じゃあ何門あったら勝てるんだろう」と思ったりしたものである。そして先日ちょっとこの話題が出たので、長年の疑問を解いてみようと思ってシミュレーションプログラム(末尾掲載)を書いてみた。


このプログラムは以下のようなロジックになっている:

  1. Cannonオブジェクトは命中率と門数(と名前)を持つ
  2. 1回射撃するごとに命中率*門数で的中数が出る
  3. 的中数を敵の数から引く
  4. 一方が0門以下になるまで続ける
  5. 0門以下になったら勝敗、結果が出るまでの射撃回数、勝者の残数がカウントされる
  6. 終わったら集計して率で表示

補給とか疲労とか士気とかは考えず、どちらかが全滅するまでひたすら撃ち合うだけだ。


それではまずは「百發百中の一砲能く百發一中の敵砲百門に對抗し得る」かどうか:

>>> rating(百発百中一門, 百発一中百門, 100)

[0.0, 0.0, 1.0, 1.56, 0.0, 98.44, 100]


結果の読み方は

[引分率, 前者勝率, 後者勝率, 平均射撃数, 前者平均残数, 後者平均残数, 試行回数]

である。つまりこの戦いは、

  • 100回戦って百発一中の砲100門が全勝
  • 平均1.56射で決着
  • 百発一中100門は平均1.56門の損害。

ということになる。まったく対抗しえないw


どれだけあれば勝てるのだろう。砲を増やしてみる。


百発一中の砲は100門のままで、百発百中の砲を2門にする:

>>> rating(百発百中二門, 百発一中百門, 100)

[0.0, 0.0, 1.0, 2.46, 0.0, 95.93, 100]


やはり百発一中側が完勝。でも2.46回撃ち合って4門くらい損害が出るようになった。


3門:

>>> rating(百発百中三門, 百発一中百門, 100)

[0.0, 0.0, 1.0, 3.22, 0.0, 92.78, 100]


百発一中がやはり完勝だけど、3回ちょっと撃ち合える。一中の損害も8門くらい。


4門5門6門までだいたい同じ傾向だけど、7門で変化が出る:

>>> rating(百発百中七門, 百発一中百門, 100)

[0.0, 0.02, 0.98, 11.25, 0.06, 58.09, 100]


なんと、この段階でもう百発百中の砲が2勝してる。平均的には全滅するが、百発一中側も40門も死ぬ。


8門:

>>> rating(百発百中八門, 百発一中百門, 100)

[0.0, 0.19, 0.81, 13.46, 0.63, 42.95, 100]


2割くらい勝つようになり、1門近く生き残る。

「この戦いで我々はほとんど死ぬが、誰かはきっと生き残る。」

という感じだが、2割の勝利のときに平均5門くらいが生き残るんだと思う。


9門:

>>> rating(百発百中九門, 百発一中百門, 100)

[0.0, 0.27, 0.73, 13.78, 1.04, 36.48, 100]


2割7分の勝利。生残率は文字通り「九死に一生を得る」という状態。


10門:

>>> rating(百発百中十門, 百発一中百門, 100)

[0.0, 0.52, 0.48, 14.9, 2.7, 19.11, 100]


突然勝率が5割を超える。生き残りも3割近い。偶然かな? と何度か繰り返してみたところで、普通に試行数を上げればいいのに気づく。100倍の試行数にしてみよう。


>>> rating(百発百中十門, 百発一中百門, 10000)

[0.0002, 0.5365, 0.4633, 15.0892, 2.6123, 18.8742, 10000]


勝率は54%弱らしい。10人中7、8人ほど死んでしまうが、相手を平均80人殺しているので「大戦果だ」と言うかもしれない。


11門になると、かなり勝つ:

>>> rating(百発百中十一門, 百発一中百門, 10000)

[0.0002, 0.747, 0.2528, 14.3739, 4.3114, 9.2199, 10000]


7割5分。平均射撃回数が減ってる。ただし注意してほしいのは、ここまで来ても生残数は半分以下なのである。


12門:

>>> rating(百発百中十二門, 百発一中百門, 10000)

[0.0001, 0.8818, 0.1181, 12.8034, 6.1041, 3.8431, 10000]


9割近く勝てるようになって、ようやく半分以上が生き残るようになった。


…しかし、もちろん米軍は素人でも勝てる作戦をやってくるものである:

>>> rating(百発百中十二門, 百発一中千門, 10000)

[0.0, 0.0, 1.0, 1.7185, 0.0, 985.4562, 10000]


1000門もあれば1:100だった時と変わらず、すばやく掃除されてしまう。


百発百中の砲を練りに練って投入しても、数が少なければすぐに消耗してしまうのに対し、下手くそでも数を十分に揃えれば損害も少ない。


がんばりすぎればリスクが上がるばかりだという感じ。


人間の脳には、新しいものを得るよりも、いまあるものを完璧にすることを選んでしまうバイアスがあるが、軍隊でこれをやるのはヤバい、ということがわかる。


単純なシミュレーションだが、子供の頃からの疑問が解けたばかりか、意外なほどいろいろなことがわかった。たいへん満足である。


cannon.py

#!/usr/bin/env python3.4

import random

class Cannon:
  """cannons with accuracy"""
  def __init__(self, accuracy, num_of_fire, name):
    self.accuracy = accuracy
    self.fire = num_of_fire
    self.name = name

def shoot(cannon, num_of_fire):
  hits=0
  for i in range(num_of_fire):
    if (cannon.accuracy >= random.random()) : hits += 1
  return hits

def battle(a, b):
  _numA = _fireA = a.fire
  _numB = _fireB = b.fire
  shots = 0
  while (_numA > 0 and _numB > 0):
    _numA = _numA - shoot(b, _fireB)
    _numB = _numB - shoot(a, _fireA)
    shots += 1
    _fireA = _numA
    _fireB = _numB
  winner = 0
  if (_numA > 0): winner = 1
  elif (_numB > 0): winner = -1
  return winner, shots, _numA, _numB

def fieldtest(a, b, rep=100):
  results = []
  battles = 1
  while(battles <= rep):
    results.append(battle(a, b))
    battles += 1
  return results


def rating(a, b, rep=1000):
  results = 0,0,0], 0, 0, 0]
  for win, shots, remA, remB in fieldtest(a, b, rep):
    if (remA <= 0): remA = 0
    if (remB <= 0): remB = 0
    results[0][win] += 1
    results[1] += shots
    results[2] += remA
    results[3] += remB
  result=[ numwin / rep for numwin in results[0
  result.extend([ elem / rep for elem in results[1:)
  result.append(rep)
  return result