目次
AtCoder Beginner Contest 426 E,F,G問題メモ
生成AIの使用ルールがまた厳しくなり、言語間の変換が禁止されてしまった!大変だ!
と思ったが、よく考えたらPythonから他の言語に変換してもらってACしたこと1回しか無かったわ。
「アルゴリズムは合ってるはずであとは速度だけなんだ! Rustなら通るんだ!」 と信じて変換した結果も敢えなくTLE、結局アルゴリズム間違ってました、というケースのなんと多いことか。
E - Closest Moment
問題文
- 高橋君と青木君が二次元平面上を歩きます。
- 高橋君のスタート地点は $(TS_X, TS_Y)$、ゴール地点は $(TG_X, TG_Y)$ です。
- 青木君のスタート地点は $(AS_X, AS_Y)$、ゴール地点は $(AG_X, AG_Y)$ です。
- 二人は同時に各々のスタート地点を出発し、各々のゴール地点に向かって真っ直ぐ速さ $1$ で歩き続け、各々のゴール地点に到達すると停止します。出発は同時ですが、停止するタイミングは同時とは限らないことに注意してください。
- 二人の間のユークリッド距離が最も短いタイミング(二人が出発する瞬間および停止した後を含む)における、二人の間の距離を求めてください。
- $T$ 個のテストケースが与えられるので、それぞれについて答えを求めてください。
制約
- $1\leq T\leq 2\times 10^5$
- $-100\leq TS_X,TS_Y,TG_X,TG_Y,AS_X,AS_Y,AG_X,AG_Y \leq 100$
- $(TS_X,TS_Y)\neq (TG_X,TG_Y)$
- $(AS_X,AS_Y)\neq (AG_X,AG_Y)$
- 入力は全て整数
解法
文字がいっぱい出てきて混乱する。粘り強さが大事。
わかりやすさのため、単位時間を[秒]とする。
問題文の表記は、文字が多くなってくると $TS_X$ などが1変数であることが分かりづらい ($T \times S_X$ のように見える)ので、以下のように変える。
- 高橋君=Aさんのスタート地点は $(X_{SA},Y_{SA})$、ゴール地点は $(X_{GA},Y_{GA})$
- 青木君=Bさんのスタート地点は $(X_{SB},Y_{SB})$、ゴール地点は $(X_{GB},Y_{GB})$
それぞれが何秒間動くかは、単純に始点から終点までのユークリッド距離と等しいので、以下で求められる。
- $T_A = \sqrt{(X_{GA}-X_{SA})^2+(Y_{GA}-Y_{SA})^2}$
- $T_B = \sqrt{(X_{GB}-X_{SB})^2+(Y_{GB}-Y_{SB})^2}$
また、$A$ が1秒あたりに動くX方向,Y方向の距離は以下のようになる。$B$ も同様。
- $V_{XA} = \frac{X_{GA}-X_{SA}}{T_A}$
- $V_{YA} = \frac{Y_{GA}-Y_{SA}}{T_A}$
一般性を失わず、$T_A \le T_B$ を仮定する。(移動時間が長くない方を $A$ とする)
開始から $0 \le t \le T_A$ のフェーズと $T_A \le t \le T_B$ のフェーズに分けて考える。
前半フェーズ
公式Editorialでは、相対的な速度を考えることで一方を停止していると見なし、点と線分の距離に落とし込んでいる。賢い。
それに気付かなくても、二次方程式や三分探索で解ける。
開始 $t$ 秒後の位置は、
- Aさんは $(X_{SA}+tV_{XA}, Y_{SA}+tV_{YA})$
- Bさんは $(X_{SB}+tV_{XB}, Y_{SB}+tV_{YB})$
よってその距離(の2乗)は $((X_{SB}+tV_{XB})-(X_{SA}+tV_{XA}))^2 + ((Y_{SB}+tV_{YB})-(Y_{SA}+tV_{YA}))^2$
整理すると、$t$ に関する二次方程式となる。
- $at^2 + bt + c$
- $a=(V_{XB}-V_{XA})^2 + (V_{YB}-V_{YA})^2$
- $b=2 ((X_{SB}-X_{SA})(V_{XB}-V_{XA}) + (Y_{SB}-Y_{SA})(V_{YB}-V_{YA}))$
- $c=(X_{SB}-X_{SA})^2 + (Y_{SB}-Y_{SA})^2$
$a \ge 0$ で下に凸なので、平方完成して最小値をとるか、三分探索で答えが求められる。
平方完成の方が $O(1)$ なので計算量は少ないが、
$a=0$ の場合分けや、$t$ が $0 \le t \le T_A$ に収まっているかの確認が必要。
三分探索の方が変な考慮漏れなく実装できる気がする。
ただし、前半後半をまとめて三分探索するのは上手くいかない。
「前半フェーズでは両者が徐々に離れるけど、後半フェーズで $A$ が止まったら徐々に近づくように変化する」など、
全体としては下に凸とは限らないので。
後半フェーズ
点と線分の距離によって求められる。
Aさんがゴールに着いた時点のBさんの位置は $P=(X_{SB}+T_AV_{XB}, Y_{SB}+T_AV_{YB})$ なので、 「$P$ と $(X_{GB},Y_{GB})$ を結ぶ線分」と $(X_{GA},Y_{GA})$ の距離を求めればよい。
$T_A=T_B$ の場合、線分の長さが0なので実装によってはゼロ除算でエラーになるので注意。 この場合は後半フェーズは計算しなくてよい。
前半と後半の小さい方が答え。
F - Clearance
問題文
- AtCoder 社のオンラインショップでは現在 $N$ 個の商品を取り扱っており、商品 $i$ の在庫は残り $A_i$ 個です。
- 以下の $Q$ 件の注文を順に処理してください。そのうち $i$ 件目は次の通りです。
- 商品 $l_i,l_i+1,\dots,r_i$ を $k_i$ 個ずつ買う。但し、 $k_i$ 個未満の商品はあるだけ全て買う。この注文で買われた商品の個数の合計を答えよ。
- $i \lt Q$ について、 $i$ 件目の注文で買われた商品の在庫を減らした状態で $i+1$ 件目の注文に進むことに注意してください。
制約
- 入力は全て整数
- $1 \le N \le 3 \times 10^5$
- $1 \le A_i \le 10^{15}$
- $1 \le Q \le 3 \times 10^5$
- $1 \le l_i \le r_i \le N$
- $1 \le k_i \le 10^9$
- 実行制限時間: 5秒
解法
遅延セグメント木を使えば区間和を取得・一律に減算することはできるが、、、
「$0$ 未満にはならない」という制約が入ると、区間内の $A_i$ それぞれの値の情報が必要となり、効率的に処理できない。
- ※SegmentTreeBeats というデータ構造で一応できるらしい。
ただ、この問題では商品は補充されない。 つまり「いったん在庫がなくなった商品はその後もずっと売り切れ」という性質がある。
「在庫がなくなった商品はセグメント木から除外する」みたいな処理ができれば、 次のクエリ以降は「その商品の在庫を減算するとき $0$ 未満にしない」という例外的な考慮をしなくて済む。 この除外処理をおこなうのは、商品数、つまり高々 $N$ 回しか発生しないので、他は素直な遅延セグメント木の処理で可能となる。
以下のように遅延セグメント木を実装すればよい。
- dataには、(区間内で在庫の残る商品の個数, その中の最小値, 最小値を取る最も左のindex) を載せる。
- lazyは、単純に 減算する値 を載せる。
最初は、data の最下段には各 $A_i$ について $(1,A_i,i)$ が対応する。
クエリ $(l,r,k)$ が来たとき、答えを $a=0$ で初期化して
- $k \gt 0$ である間、以下を繰り返す。
- dataの [l:r] の集約値 $(cnt, min, idx)$ を得る
- $cnt = 0$ なら、その区間に商品は残っていない。ループを追える。
- $k \lt min$ なら、残っている商品は全て $k$ 個ずつ買える。$a += cnt \times k$ とし、ループを追える。
- $k \ge min$ なら、とりあえず $min$ 個ずつは買える。
- $a += cnt \times min$、$k -= min$ とする。
- $[l:r]$ の在庫を $min$ 個ずつ減らす。
- その結果、商品 $idx$ の在庫は $0$ となる。$idx$ を“除外”する。
- data の idx番目 を $(0, 十分大きな値, 何でもいい)$ に更新することで除外に相当する。
十分大きな値とは、「再度 $idx$ を含むクエリが来ていくらか減算された後でも、在庫の残る正式な値より小さくはならない」ことが必要。よって、$\max(A)+Q \times \max(k) \lt 10^{16}$ 以上の値を設定しておけば十分となる。
これで、$O((N+Q) \log{N})$ で解ける。
G - Range Knapsack Query
問題文
- $1$ から $N$ までの番号が付けられた $N$ 個のアイテムがあります。
- アイテム $i$ の 重み は $W_i$、価値 は $V_i$ です。
- $Q$ 個のクエリが与えられるので、順に処理してください。$j$ 番目のクエリは以下の通りです。
- 整数 $L_j, R_j, C_j\ (1\leq L_j\leq R_j\leq N)$ が与えられる。
- アイテム $L_j,L_j+1,\dots,R_j$ のうちいくつか($0$ 個でもよい)を、重みの総和が $C_j$ を超えないように選ぶとき、選んだアイテムの価値の総和が最大でいくつになるか求めよ。
制約
- $1\leq N \leq 2\times 10^4$
- $1\leq Q \leq 2\times 10^5$
- $1\leq W_i \leq 500$
- $1\leq V_i \leq 10^9$
- $1\leq L_j \leq R_j \leq N$
- $1\leq C_j \leq 500$
- 入力は全て整数
解法
ナップサックDPは、「アイテムの除外」ができない。
つまり、「アイテム $1~i$ までを考慮したDP配列から、やっぱり $1$ は使わなかったことにする」などはできない。
DPはmaxで更新していくが、maxには逆演算が存在せず、更新前の値がわからないからである。
なので、除外が必要なMo'sとかは使えない。
DP配列自体をセグメント木に載せる、という解法も考えられるが、
マージに $O(\max(C)^2)$ かかるので、$O(Q \log{N} \max(C)^2)$ はちょっと厳しい。
ここでは、Disjoint Sparse Table の考え方が使える。
$N=16$ とし、$A_i$ の添字は0-indexedとする。
クエリ区間 $(L,R)$ が $7$ と $8$ の境界を跨ぐ場合は、 以下の区間のアイテムを使ったDPの結果が分かっていれば、左右から適切な1つずつを取ってきてマージすることで求められる。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |-||-| |----||----| |-------||-------| |----------||----------| |-------------||-------------| |----------------||----------------| |-------------------||-------------------| |----------------------||----------------------| (L,R) = (3,14) の場合、[3,7] と [8,14] をマージ
この場合のマージは、目標とする重さ $C_j$ が固定されているため、$O(\max(C)^2)$ ではなく $O(C_j)$ で求められる。
(DPには、「重さ $w$ ちょうど」ではなく「以下」で達成できる最大価値を求めておく)
左側のDPを中央から伸ばしていきながら、クエリに $L$ として指定されているindexについてDP配列を保存しておく。
その後、右側を中央から伸ばしていきながら、クエリに $R$ として指定されているindexまで来たら、
対応する $L$ 側とマージして答えが求められる。
クエリ区間 $(L,R)$ が中央を跨がない場合は、左右に分割した区間で同様に求めていける。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |-||-| |-||-| |----||----| |----||----| |-------||-------| |-------||-------| |----------||----------||----------||----------|
コーナーケースとして、クエリ区間の長さが $1$($L=R$)の場合はこの方法では処理できない。
ただ、その場合の答えは $W_L$ と $C_j$ の大小関係より明らかなので、個別処理する。
分割の最大回数は $O(\log{N})$。
それぞれで、DP配列にアイテムを追加・DP配列を保存していくのに1アイテム $O(\max(C))$ かかり、計 $N$ アイテムが追加される。
クエリに答える際には、1クエリ $O(\max(C))$ かかる。
よって、合わせて計算量は $O(\max(C) \times (Q + N \log{N}))$ となる。
制約を代入すると $2.5 \times 10^8$ となり
「さすがに厳しいので他の解法があるのか……?」と躊躇してしまったが、
実装してみると意外と間に合う。
オーダー上の計算量にとらわれない、実際の処理時間の見積もりが難しい。
各クエリがどの階層で境界を跨ぐかを簡単に求める方法
$N$ を2の冪乗とする。(足りなくても、本来の $N$ を超える部分に対する処理が走ることはないので気にしなくていい)
$A_i$ の添字およびクエリ区間 $L,R$ を 0-indexed とする。
この時、$L \oplus R$ の bit_length が、「下から何番目の階層か(1-indexed)」に相当する。($\oplus$ はXOR演算を表す)
また、bit_length を $b$ として、$L >> b$(右bitshift)が、「その階層で左から何番目の境界を跨ぐか(0-indexed)」に相当する。
000 001 010 011 100 101 110 111 階層3 ┃ 階層2 ┃ | ┃ 階層1 ┃ | ┃ | ┃ | ┃ (L,R)=(2,7) の場合 010^111 = 101 より、b=3、階層3での分割。 L=010 を3つ右bitshiftすると 0 より、左から0番目の┃を跨ぐ。 (L,R)=(4,6) の場合 100^110 = 10 より、b=2、階層2での分割。 L=100 を2つ右bitshiftすると 1 より、左から1番目の┃を跨ぐ。
こうすると、同じ┃を跨ぐクエリごとに分類しやすくなる。