トヨタ自動車プログラミングコンテスト2024#3(AtCoder Beginner Contest 344)F,G 問題メモ

F - Earn to Advance

問題

  • $N \times N$ グリッドを左上 $(1,1)$ から右下 $(N,N)$ まで移動する
  • はじめ、所持金は0
  • マス $(i,j)$ では以下の3種類の行動を行える
    • その場にとどまって $P_{i,j}$ 円得る
    • $R_{i,j}$ 円支払って右へ移動する
    • $D_{i,j}$ 円支払って下へ移動する
  • 最小の行動回数を求めよ
  • $2 \le N \le 80$

解法

移動中に所持金が足りなくなったとき、現在いるマスで補充する代わりに、通過したうち $P_{i,j}$ が最大のマスで、 「そこにいた時に、先を見越して所持金を補充しておいた」ことにしてよい。

これにより、各マスでは「所持金が $R_{i,j}$ 以上になるまで補充できたら、すぐ右に移動する」(下も同様)としてよい。

  • $DP[i,j,k,l]=(1,1)$ から $(i,j)$ への移動で、通過マスのうち $P_{k,l}$ が最大であった場合の、(最小行動回数, その時の最大所持金)

行動回数と所持金の2つの値を管理しなければならない。行動回数は少ない方がよいが、所持金は多い方がよい。
2つが相反する場合、DPにはどちらを残すべきか?

  • ①行動回数が多くなってしまうが所持金は多い状況
  • ②行動回数が少なくすむが所持金は少ない状況

答えとして求めるのは行動回数なので②は必須だが、 この先、「②では所持金が足りなくなってしまうが、①では手持ちで足りるので補充しなくて済んで、最終的に逆転する」ようなことはないか?

ないと言える。

「所持金が足りればすぐに移動する」という行動基準を考えると、 各状況の所持金を $C_①,C_②$ として、$0 \le C_①,C_② \lt P_{k,l}$ である。

$C_②+P_{k,l} \gt C_①$ なので、②で1回補充すると所持金は $C_②$ の方が大きくなる。
つまりこれ以降で、②では補充するが①ではしなくて済む(または、①の方が補充回数が少なくなる)状況が訪れるとしても、 補充回数の差はせいぜい1回で逆転まではしないし、その場合も所持金は②の方が多いので、②が完全に優位である。

(この先の通過マスで、$P_{k,l}$ がより多い他のマスに更新されているかもしれないが、いずれにしろ上記の理論は変わらない)

よって、DPは②の行動回数が最小の状態(同じなら、所持金がより多い方)を残す、として問題ない。

計算量は $O(N^4)$

Python3

G - Points and Comparison

問題

  • 2次元平面上の $N$ 個の点がある
  • $Q$ 本の直線 $y=A_i x + B_i$ が与えられる
  • 各直線につき線上またはそれより上にある点の個数を求め、$Q$ 本を通しての総和を求めよ
  • $Q$ が多いので、$A_i,B_i$ は疑似乱数生成で与えられる(元の問題文参照)
  • $1 \le N \le 5000$
  • $1 \le Q \le 10^7$
  • $-10^8 \le A_i \le 10^8$
  • $-10^{16} \le B_i \le 10^{16}$
  • 実装制限時間 10 sec

解法

PythonだとPyPyでも厳しい、Numbaでやっと通った。
ちょっと $N,Q$ を多くしすぎじゃないですかねと思ったが、 C++ だとコンパイルオプションを指定して愚直解を通されていたみたいなので、Writerさんは大変だと思い直した。

ある1クエリ $(A_i,B_i)$ だけに答えることを考えると、 点が上手い具合にソートされていると、二分探索で答えを求められそうである。

「上手い具合にソート」とはどんな状態かを考えると、 「各点を通る傾き $A_i$ の直線を引くと、それらが下から順に並んでいる」と言う状態である。
クエリの直線が、どの2点(から引かれた直線)の間にあるか二分探索で調べれば、 そこから上の点は全て条件を満たすことになる。

      /    /  //
    /    /  //
  ①    /  ④/    この傾きでは、③④②① の順に小さい
/    ②  //
    /  /③
  /  //

$A_i$ が変わると、並び順も変わる

\\\    \
  ①\\    ④      この傾きでは、①②③④ の順に小さい
    \②\    \
      \\③
        \\\

$A_i$ ごとにこれを求めていったらもちろんTLEだが、傾きがちょっと変わっただけでは大部分の順序は同じはず。

クエリを $A_i$ でソートして傾きを負の無限大から正の無限大へ徐々に変化させていったとき、 各2点組の順位は「傾きが2点を結ぶ直線の傾きに等しくなった」前後に入れ替わる。

このような変化は $O(N^2)$ 箇所あり、隣り合った要素を入れ替える操作は $O(1)$ でおこなえる。

よって、「全ての2点間の傾き」と「クエリ1で与えられる直線の傾き $A_i$」を、まとめてイベントソートして、 小さい方から「順位の入れ替え」または「二分探索による求解」をおこなっていけばよい。

ただし、

  • $X_i=X_j$ な2点は、最初から最後まで順位が入れ替わらない($Y$ が大きい方が常に上)なので、イベントに含めなくてよい
  • 最初の点の順位は、負の無限大の傾きの直線を引いたときの順位であり、$(X_i,Y_i)$ をこのままソートすればよい

$O((N^2+Q)\log{(N^2+Q)})$ で求められる。

点とクエリを別管理した方が、logの中身がばらけるので計算量は減る。ここまで $N,Q$ が大きいと、その違いも馬鹿にならないかも。

豆知識

今回の乱数生成式 $G_{n+1}=G_n \times 48271 \mod 2^{31}-1$ は線形合同法と呼ばれる疑似乱数生成式で、特に Park & Miller によって考案されたパラメータを用いている。
結果が32bitの範囲に収まり、式の単純さの割には(あくまで割には、だが)そこそこランダムっぽい値が生成されるらしい。

昔のAtCoderでは、このように乱数を用いて生成された入力だと、そのランダム性を利用する(ランダム性が見込めることで高速に動く説明が付く解法が想定解)ということも見られたが、今回はそういう意図はなく、単純に制約の都合だったのかな。

Python3

programming_algorithm/contest_history/atcoder/2024/0309_abc344.txt · 最終更新: 2024/03/15 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0