JOI情報オリンピック 公式テキスト問題『射撃王』を完全解説!

このページでは情報オリンピック 公式テキスト問題『射撃王』を完全解説します。

JOI公式テキストp114に載っている模範解答プログラムです。

n = int(input())
h = [0] * n
s = [0] * n
for i in range(n):
  h[i], s[i] = map(int, input().split())

ok = 10 ** 14

ng = -1

while ok - ng > 1:
  m = (ok + ng) // 2

t = [0] * n

for i in range(n):
  if m < h[i]:
     t[i] = -1
  else:
     t[i] = (m - h[i]) // s[i]

t = sorted(t)
is_ok = 1

for i in range(n):
  if t[i] < i:

is_ok = 0

if is_ok == 1:
 ok = m
else:
 ng = m

print(ok)

模範解答の完全解説です。

よく読んで完全に理解してください。

このプログラムは2分探索の手法(アルゴリズム)を使っています。

なぜ、2分探索で、風船の高度の最小値を求められるのか?それを理解してください。

テキストの解説は20行ほどと非常に簡素で完全な理解は難かしいです

解説

n = int(input())
まず、風船の数nを入力として受け取ります。

h = [0] * n
s = [0] * n
for i in range(n):
h[i], s[i] = map(int, input().split())

次に、各風船の初期高度h[i]と上昇速度s[i]を入力として受け取ります。

ok = 10 ** 14  注1)
上限値を考えられる限りの大きな数、兆にしました。

ng = -1  注2)

二分探索のための上限値okと下限値ngを設定します。

while ok - ng > 1:
m = (ok + ng) // 2

二分探索を利用するので中央値mを計算します。

★mは各風船が全部割れるまでの最小高度を格納するための変数です。

◆ではtとは何か?

t = [0] * n

for i in range(n):
if m < h[i]:
t[i] = -1
else:
t[i] = (m - h[i]) // s[i]

このプログラムにおけるtは、各風船が全部割れるまでの秒数を格納するためのリストです。

★mは最初はかなり大きな数です。例えば500mとかになります。

そこで、最初に1つ目の風船の高度とmを比べ、1つ目の初期高度h[0]が600mなら、これは全部の風船を割れる最小高度mを上回っているのだから、全部の風船は割れないですね。
でもmが10000mなら割れますね。

では10000mまで風船が達するのに何秒かかる?1秒でs[0]100m上がるなら

(10000-600)÷100 で94秒となる。これでt[0]が94となります。これを、風船の個数だけ繰り返します。

各風船が割れない時がt[0]などに-1と入れます。

もし、最初のt[0]が-1なら、割れない風船があるのだから、is_okを0に設定します。

t = sorted(t)
is_ok = 1
for i in range(n):
if t[i] < i:
is_ok = 0

これは割れないということですね。

これをもし、各風船が割れない時がt[0]などに0と入れると0秒で割れる意味になるので駄目ですね。

その後、tはソートされ、各風船が指定した時間内に割れるかどうかが確認されます。

全ての風船が指定した時間内に割れる場合、ok(探索範囲の上限)を更新します。そうでない場合、ng(探索範囲の下限)を更新します。

ここがポイントです!

求めるのは全部の風船が割れる最小高度です

これを二分探索でどう求めるのでしょうか?全部の風船が割れるのは、最小高度より高い距離なら全部割れますよね

だから、恐らく、最初に求まったmでも全部割れるでしょう。

では、さらに最小高度を求めるにはどうしたらよいか?

最小高度は1回目のmより左にあって小さいか?それとも右側にあって大きいか?左にありますよね。

そこで、右端=OKをmの値にして、求める範囲を左端の0からまでにするのです。

次にこの範囲の中間値を求めて、風船が全部割れるか調べます。

割れたらまた右端=OKをmにします。

割れなかったら、求める最小値はmより右にあるので左端=ngをmにするのです。

こうすれば最小高度にmの値はどんどん近づいて行きます。

右端と左端が同じ値か逆転したら、それ以上調べるのを終了して良いですね。

★繰り返し処理の条件はこうです。

while ok - ng > 1:

mに最小高度は入っていますね。

★mを2分探索の値とするのは、飛び飛びの大きな値=中央値を試して試す回数を減らすためです。ここが右端と左端を更新している部分です。

if is_ok == 1:
ok = m
else:
ng = m

★ok = mは上限値をmに、ng = mは下限値左端をmにして探索範囲を絞り込んでいって
いるのです。

(注1)

ok = 10 ** 14 の 10 ** 14 の意味は何ですか?

10 ** 14は、Pythonの演算子**を使用して10の14乗を計算する表現です。つまり、10を14回掛けることを意味します。

その結果は、1の後に14個の0が続く非常に大きな数、すなわち1兆(日本の数の単位)になります。

このプログラムでは、ok = 10 ** 14は二分探索の上限値を設定しています。つまり、探索する時間の範囲の最大値を非常に大きな値(1兆)に設定しています。
これは、探索範囲を十分に大きくすることで、全ての可能性をカバーするためです。
この値は、問題の制約に基づいて選ばれます。この場合、風船が割れるまでの時間が非常に長くなる可能性があるため、大きな値が選ばれました。

(注2)

ng = -1は、二分探索の下限値を設定しています。つまり、探索する時間の範囲の最小値を-1に設定しています。

二分探索では、探索範囲の上限値と下限値を設定し、その範囲を狭めていきます。このプログラムでは、okが上限値、ngが下限値となります。

ngの初期値を-1に設定する理由は、探索範囲を0以上の全ての整数に設定するためです。
なぜなら、この問題では求めるべき答え(風船が全て割れる最小の時間)は0以上の整数だからです。

したがって、ng = -1と設定することで、0以上の全ての整数を探索範囲とすることができます。

そして、二分探索を進めることで、この範囲を狭め、最終的に求めるべき答えを見つけることができます。

このように、二分探索は効率的に答えを見つけるための強力な手法となります。このプログラムでは、その手法を活用して、風船が全て割れる最小の時間を求めています