−目次
パナソニックグループ プログラミングコンテスト2025(AtCoder Beginner Contest 406)F,G問題メモ
F - Compare Tree Weights
問題文
- N 頂点の木 T があり、辺 i (1≤i≤N−1) は頂点 Ui と頂点 Vi を結んでいます。
- また、各頂点には重みが定められており、最初、各頂点の重みはすべて 1 です。
- Q 個のクエリが与えられるので、順に処理してください。各クエリは次の 2 種類のいずれかです。
'1 x w
' : 頂点 x の重みを w 増加させる。'2 y
' : 辺 y を削除すると、T は 2 つの部分木(連結成分)に分かれる。それぞれの部分木に含まれる頂点の重みの総和をその部分木の重みとするとき、2 つの部分木の重みの差を出力する。
- 2 種類目のクエリについて、T から任意の辺を 1 つ選んで削除したとき、T は必ず 2 つの部分木に分かれることが証明できます。
- また、2 種類目のクエリでは、実際に辺を削除しているわけではないことに注意してください。
制約
- 2≤N≤3×105
- 1≤Ui,Vi≤N
- 1≤Q≤3×105
- 1≤x≤N
- 1≤w≤1000
- 1≤y≤N−1
- 入力はすべて整数
- 与えられるグラフは木である。
- 2 種類目のクエリが少なくとも 1 つ存在する。
解法
各頂点に付き以下が分かれば、
- Sv:=(頂点 1 を根とした時の)頂点 v の部分木の重み
辺 (u,v) を削除したとき、親を u とすると、以下のように計算できる。
- v 側の重みは Sv
- u 側の重みは S1−Sv
- よってクエリ2の答えは、|S1−2Sv|
クエリ1で頂点 v の重みを w 増やした時、1~v 間を結ぶパス上の各頂点の Sv が w ずつ増加する。
HL分解すれば木のパスは O(logN) 個のセグメントで表現できるので、 双対セグ木やFenwickTreeに載せておけば、O(log2N) で加算処理を行える。
よって、O(N+Qlog2N) でおこなえる。
G - Travelling Salesman Problem
問題文
- 数直線上にあなたと N 人の商人がいます。商人には 1,2,…,N の番号が付けられています。
- はじめ、あなたは座標 0 におり、商人 i は座標 Xi にいます。また、各商人は品物を 1 つずつ持っており、商人 i が持っている品物を品物 i と表記します。
- あなたの目的は、品物 1,2,…,N をこの順に受け取ることです。
- あなたは、以下の 3 つの操作を好きな順序で好きな回数繰り返すことができます。
- 自分が 1 移動する。この操作にはコストが C かかる。
- 商人を 1 人選び、選んだ商人に 1 移動してもらう。この操作にはコストが D かかる。
- 商人を 1 人選び、選んだ商人を商人 i とする。あなたと商人 i のいる座標が一致しており、あなたが商人 i から品物を受け取ったことがない場合、商人 i から品物 i を受け取る。そうでない場合、何もしない。この操作にはコストが 0 かかる。
- 目的を達成するためにかかるコストの合計の最小値を求めてください。
- また、目的を達成するためにかかるコストの合計を最小化したときに各商人から品物を受け取る座標として考えられるもののうちのひとつを求めてください。
制約
- 1≤N≤2×105
- 1≤C,D≤105
- −105≤Xi≤105
- 入力される値はすべて整数
解法
最小値だけなら、まだ Slope Trick を知っていれば典型に当てはめやすいが、座標も合わせて答えなければならないのが難しい。
ひとまず、TLE/MLEは気にせず、素直なわかりやすい求め方を考える。
- Fi,j:=i 個目の商品を座標 j で受け取るまでにかかる最小コスト
直前にいた座標 k を全探索すると、遷移は以下のようになる。
- Fi,j=mink(Fi−1,k+|k−j|×C+|Xi−j|×D)
ただしこのままでは O(NX2max) かかるのでダメ。
足されるのが絶対値付きの関数であることからもわかるように、
Fi,j は「j についての区分線形凸関数」となる。
区分線形凸関数は、SlopeTrickで遷移を高速化できる。
遷移を2段階に分けて考える。
- ① F′i,j=mink(Fi−1,k+|k−j|×C)
- ② Fi,j=F′i,j+|Xi−j|×D
①はどのような意味を持つのか少し分かりづらい。
①は、「関数の傾きを [−C,C] に制限する」という役割を持つ。
\ / ~-_↙ x / x:傾きの変化点 ~-_ \ / ↘ _-~ 傾き -C ~-_\ / _-~傾き C ~-x--___ / _-~ ~~~x-------x-~
「傾きが C より大きければ、|k−j| 個離れた位置から 1あたり C かけて移動してきた方がコストが安くなる」と考えればわかりやすい。
その上で、②で Xi を中心に両側の傾きをそれぞれ D ずつ増やすと、SlopeTrick上での遷移が完了する。
で、最小値だけなら Fi,j を適切に更新していくだけでよいのだが、
座標の復元には、FN,∗ の最小値を取る箇所から F0,0 まで、遷移を逆にたどる必要がある。
最後になるまで、途中の i でどの j にいるのが最適かは確定しない。
N=6 C=6 D=8 2 -1 5 -2 -2 2
j -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 [ ∞, ∞, ∞, ∞, ∞, ∞, 0, ∞, ∞, ∞, ∞, ∞, ∞] ↑ i=1 ① [ 36, 30, 24, 18, 12, 6, 0, 6, 12, 18, 24, 30, 36] ② [100, 86, 72, 58, 44, 30, 16, 14, 12, 26, 40, 54, 68] ,----' i=2 ① [ 52, 46, 40, 34, 28, 22, 16, 14, 12, 18, 24, 30, 36] ② [ 92, 78, 64, 50, 36, 22, 24, 30, 36, 50, 64, 78, 92] | i=3 ① [ 52, 46, 40, 34, 28, 22, 24, 30, 36, 42, 48, 54, 60] ② [140, 126, 112, 98, 84, 70, 64, 62, 60, 58, 56, 54, 68] ,----' i=4 ① [100, 94, 88, 82, 76, 70, 64, 62, 60, 58, 56, 54, 60] ② [132, 118, 104, 90, 76, 78, 80, 86, 92, 98, 104, 110, 124] | i=5 ① [100, 94, 88, 82, 76, 78, 80, 86, 92, 98, 104, 110, 116] ② [132, 118, 104, 90, 76, 86, 96, 110, 124, 138, 152, 166, 180] `------------------, i=6 ① [100, 94, 88, 82, 76, 82, 88, 94, 100, 106, 112, 118, 124] ② [164, 150, 136, 122, 108, 106, 104, 102, 100, 114, 128, 142, 156] BEST
仮に、上図のような Fi,j の途中段階を全て保存しておければ、 「F6,2=100 となるためには i=5 ではどの j から遷移してきたか、i=4 では、i=3 では、、、」 と遡って調べていけるが、当然、これを全て持つわけにはいかない。
ところで、②の更新では各 j に値が加算されるのみで特に j 自身は動かない(遷移先 j の遷移元は j のみ)。
j が動く(遷移先 j に j 以外からも遷移してくる)のは①の更新のみである。
で、i から i+1 への①による更新は、左から以下の3区間に分けられる。
- a) 傾きが −C 未満のため、−C に揃えられた区間
- b) 傾きが −C 以上 C 以下のため、値が変わらない区間
- c) 傾きが C より大きいため、C に揃えられた区間
j -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 i=2 ② [ 92, 78, 64, 50, 36,| 22, 24, 30, 36,| 50, 64, 78, 92] a) | b) | c) i=3 ① [ 52, 46, 40, 34, 28,| 22, 24, 30, 36,| 42, 48, 54, 60]
上例のように a),b),c) が分けられるとき、下段の各 j に対する、遷移元となる上段の j は、以下のようになっている。
j -6 -5 -4 -3 -2 | -1 0 1 2 | 3 4 5 6 遷移元 [ -1, -1, -1, -1, -1,| -1, 0, 1, 2,| 2, 2, 2, 2]
つまり、最も近い b) 内の j となる。
よって、各 i→i+1 の①の更新における b) の両端座標さえ記録しておけば、
FN,j が最小値となる j から遡りながらclipすることで、受け取る座標の復元が可能となる。
実装
どのようなデータ構造で上記の処理を実現するかという点も、悩ましい。
SlopeTrickは、「傾き 0 を中心に両側の変化点の x 座標を HeapQueue 2本で管理する」という実装もあるが、 今回は「変化点の両端から傾きを C に揃える」という操作がある。これはHeapQueueでは奥の方にある要素なので取り出しづらい。
- s: (傾きの変化点, 変化差分) を値に持つSortedListまたはSortedDict
- v: ↑の中で最も左側の変化点における暫定コスト
- left[i]: i 番目の処理後の b) の左端index
- right[i]: i 番目の処理後の b) の右端index
を持って更新していけばよい。
この問題では、両端無限遠点の傾きは①の後では −C,C、②の後では −(C+D),C+D に揃えられるので、傾きの絶対値は敢えて持たなくてもよい。
更新もやや重く、境界あたりをちゃんと実装するのが難しい。
①の時、元の傾きが −C,C ちょうどになる場合は、変化点ではなくなるので s から削除はするが、b) の範囲内ではあるのでleft,rightには登録する、などでハマった。
SortedDictへの値の追加・削除・参照がそれぞれ O(log要素数) として、 要素数は②でクエリに付き1個ずつ追加されるのみなので、全体で O(NlogN) でできる。