Processing math: 78%

サイシードプログラミングコンテスト2021(AtCoder Beginner Contest 219)F,G,H問題メモ

F - Cleaning Robot

問題

  • グリッド上を (0,0) からスタートしてLRUDの命令に従って動くロボットがある
  • 文字列 SK 回繰り返した文字列を命令とするとき、ロボットが通過するマス数を求めよ
    • 複数回通過したマスも1個と数える
  • 1|S|2×105
  • 1K1012

解法

S 1ループで通過するマスをまず列挙する。マスの集合を P とする。

最後に到達したマスが (a,b) だったら、次はそこから始まるので、 次の1ループは P(a,b) だけ平行移動させたマスを通過する。

1ループ目      2ループ目(1ループ目はo、被ったのはXで表示)
...........    ..............
...........    ....#######G..
...........    ....#.........
.#######G..    .oooXoooS.....
.#.........    .o..#...#.....
.#...S.....    .o..#X#######.
.#...#.....    .o...o..#...#.
.#########.    .oooooooXX###.
.....#...#.    .....o...o....
.....#####.    .....ooooo....
...........    ..............

P のマスの1つを (x,y) とする。
ループを重ねるごとに、それに相当するマスは以下を通過することになる。
(ループのスタートからの相対位置が同じマスを「相当するマス」と呼ぶことにする)

  • (x+a,y+b),(x+2a,y+2b),(x+3a,y+3b),...,(x+(K1)a,x+(K1)b)

ここで、例えば (x+3a,y+3b)P に含まれるとする。

すると、(x,y) に相当するマスは、(x,y),(x+a,y+b),(x+2a,y+2b) だけが答えに寄与する。

それ以降は (x+3a,y+3b) とそれに相当するマスが既に通過済みなので、答えに寄与しない。

従って、「(a,b) を加算していくことで被りうるグループ」ごとに P の各マスを分類すればよい。

違うグループのマス同士は、どれだけループを重ねても被ることはない。

グループ内の計算

あるグループに (x,y),(x+3a,y+3b),(x+10a,y+10b) の3つが入ったとして、K=5 だとすると、

  • (x,y) は、次の (x+3a,y+3b) までの3マス
  • (x+3a,y+3b) は、次の (x+10a,y+10b) と被るまでには終わるので、5マス
  • 最後の (x+10a,y+10b) は、まるまる5マス

隣り合う2マス毎に、「その差分」と「K」の小さい方が答えに寄与する。
従ってこのグループでは、計13マスが答えに寄与する。

全てのグループについて合計した結果が答え。

グループの分類方法

(a,b) を足し引きすることで x0a1 の間にそろえればよい。

言い換えると、0xda<a となるような d を見つけて、(xda,ydb) ごとに分類すればよい。

ただし、a=0 の時は分けて考える必要がある点に注意。

Python3

G - Propagation

問題

  • N 頂点 M 辺の単純無向グラフ
  • 頂点 i には値 i がはじめ、書かれている
  • 今から Q 個のクエリを順に処理する
  • クエリ
    • 頂点 xi に隣接する全ての頂点の値を、その時点で xi に書かれている値に書き換える
  • 最終的に各頂点に書かれた値を求めよ
  • 1N,Q2×105

解法

ちょっと難しめの典型として、過去のPASTや典型90などに出題されているので、解いていれば解法は比較的思いつきやすい。 (正しく実装できるかは別問題)

隣接する頂点に影響を与えるクエリは、大きく以下の2通りの更新方法がある(名称は勝手に命名)。

  • 方法① プッシュ型
    • xi にクエリが来たら、その都度 xi の全ての隣接頂点を更新する
  • 方法② プル型、遅延更新型
    • 現時点の値が必要になった時に、隣接頂点に更新があったか問い合わせる
    • 以下のようにクエリを処理する
      • xi の現時点の値を求める
        • xi とその隣接頂点の中で、最も新しいクエリによって更新された値である
        • これを v とする
      • 「私はクエリ i で値 v に更新されました」という情報を xi に紐付けておく
      • 以降、xi の隣接頂点 y にクエリが来た際、y はこの情報を参考にする

どちらの方法も、次数の多い頂点に連続してクエリが来たら、更新 or 問い合わせる頂点数が多くなり、O(NQ) かかってしまう。

ここで、頂点を次数によって2種類に分ける。 閾値はだいたい M、今回だと最大300~500くらいに設定する。

閾値より次数が小さい頂点については、①で全隣接頂点を更新する。これは高々 O(M) で済む。

①が負担になるのは次数の大きい頂点なので、それに関しては②で遅延更新する。

すると、更新があったか問い合わせる先は、次数が閾値より大きい頂点だけに絞れるので、 多くとも (2 * 辺数 / 閾値)、つまり O(M) 個くらいになる。

よって、クエリ1回が O(M) で済み、全体で O(QM) となる。

①で隣接頂点から更新された時点・値と、②で xi に紐付ける情報の時点・値は、分けて管理する点に注意。

X-Y-Z    Yが仮に次数の大きい頂点だったとして、
1 2 3    0. 初期状態
2 2 2    1. Yにクエリが来る、X,Zが2になる(遅延更新)
5 2 2    2. 描かれてない他の部分で、X=5 になったとする
5 5 2    3. Xにクエリが来る、Yが5に書き換えられる(即更新)
         4. Zにクエリが来る

このとき、Zの現時点の値は2だが、遅延更新のため配列上はまだ初期の3が入っている。
Yは、現時点の値は5だが、Zに伝播させる過去の遅延更新の値2は、どこかで保持しておく必要がある

Python3

H - Candles

問題

  • 数直線上にろうそくが N 本並んでいる
    • ろうそく i は座標 Xi に長さ Ai で火の付いた状態で立っている
    • 火の付いたろうそくは1分に1cmずつ短くなっていく
    • 長さ0でろうそくは燃え尽き、火が消える
    • 火の消えたろうそくの長さは変化しない
  • あなたは座標0にいて、数直線上を左右に分速1で移動できる
    • ろうそくと同じ座標に来たとき、ろうそくの火を消せる
  • 上手く移動して火を消して、残るろうそくの長さの総和を最大化せよ
  • 1N300
  • 109Xi109
  • 1Ai109

解法

8問体制になってからのABC最終問題って、比較的高度な定理などの知識が要求される教育的な問題が多いイメージだったけど、 今回はどちらかというと知識よりは考え方を工夫すれば解ける問題だった。(まぁその工夫がなかなか高度だけど)

小さい例を手元で考えると、どうも左右に行ったり来たりするのが最適となることもあるらしく、貪欲が難しそう。

以下のようなDPを考えるにしても、

  • DP[l][r][k]= 座標0から左に l 個、右に r 個のろうそくを止めて、最後に止めたのが k=0:,1: だった時に、火を消して確定したろうそく長合計の最大値

実際は長さだけでなく時間も絡んでくるため、これでは上手くいかない。
「途中までのスコアは高いが時間がかかってしまう方法」と「スコアは劣るけど時間が早い方法」だと、 DPには前者しか記録されないが、後から後者に逆転されることもありうる。

かといって、ろうそく長も時間も 109 オーダーの値を取るため、どちらかをさらにDPのキーに加えるのは難しい。

減少量・減少速度で考える

この問題は「全ての火を消すまでの、ろうそくの長さの減少量を最小化」させる問題ともいえる。

どうも「ろうそくの長さが0になったらそれ以上は減らない」ことで、 時間によって個々のろうそくが燃え尽きてるのか尽きてないのか判断する必要があることが、 DPの遷移に必要な情報量を増やしてしまっている気がする。

もし、ろうそくの長さが負になっても一律に減り続けるなら計算は少し容易になる。

総減少量に対する1分あたりの減少速度=残っているろうそくの本数であり、最初は N、1本火を止める毎に1ずつ減っていく。

冒頭の DP と同じ感じで、減少量を管理することを考える。

  • DP 座標0から左に l 個、右に r 個のろうそくを止めた時点までの、全ろうそくの長さの総減少量

l,r が同じ組は残っている本数が等しいので、仮にその状態になるまでの所要時間が異なっても、その時点での減少速度は等しい。

よって、次の遷移(左または右で次に近いろうそくを止める)までの減少量はそれまでの所要時間に依らずに計算できる。

時間に依らず遷移が等しいなら、減少量さえ管理しておけば十分となり、この DP'' で長さが負になり得る場合の正しい答えを求められることがわかる。

ろうそくを無かったことにする

実際には長さは0で打ち止めのため、上記の方法では答えが過小になってしまう。

長さが0以下になるろうそくは、はじめから無かったことにしたい。

最初にろうそくの部分集合を決め打って、それらだけで答えを求めることはできる。

  • (採用するろうそくの A_i の総和)-(それら全ての火を止めるまでの減少量の最小値)

全ての部分集合での最大値を調べれば正しい答えとなる。
部分集合によっては最適な方法に長さが0以下となるろうそくが含まれることもあり得るが、 その場合はそのろうそくが含まれない部分集合からスタートした場合でより高いスコアとなるので、最大値になることは無い。

しかし、全ての部分集合を調べるのはさすがに間に合わない。

ここで、最初に決めておくべきはろうそくの本数(初期の減少速度)だけであり、 どのろうそくを採用するかしないかは、ろうそくにたどり着いた時点で決めることができると考える。

あるろうそくにたどり着いたとき、

  • もしそのろうそくを採用するなら、スコアに A_i を加算し、次からの減少速度は1減る
  • 採用しない(はじめから無かったことにする)なら、減少速度はそのまま

これで減少速度が0になるまでのスコアが答えとなる。

ここで、「スコア」とは「既に採用することが確定した A_i の合計 - まだ確定していないろうそく分も含めたその時点までの減少量」を意味する。

これを実装するにはDPにもう1次元追加する必要があるが、 時間やろうそく長と違って N 通りの値しか取らないため、全部で O(N^3) で間に合う。

  • DP[l][r][k][c]= 座標0から左に l 個、右に r 個までのろうそくに到達していて、これから採用するのが c 本で、いまいる位置が k=0:左端, 1:右端 だった時の最大スコア

初期値として DP[0][0][:][:]0 に、他を -INF にしておくことで、様々な c からスタートした場合の答えをまとめて計算できる。

Python3

programming_algorithm/contest_history/atcoder/2021/0918_abc219.txt · 最終更新: 2021/10/01 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0