目次

AtCoder Beginner Contest 262 F,G問題メモ

AtCoder Beginner Contest 262

学生大会の予選を兼ねていたからか、いつもより難しめに感じた。

F - Erase and Rotate

F - Erase and Rotate

問題

解法

場合分け時の挙動の違いに気がついたり、indexを正しく管理したり、といった注意力と実装力が求められる。

まず、最終形の先頭要素を決めてしまいたい。
$K$ 回以下の操作で先頭に持ってこれるのは、以下の範囲にある数字である。

N=9 K=3

8 5 3 1 9 4 2 6 7
~~~~~~~     ~~~~~    先頭からK+1個 or 末尾からK個

①先頭から $K+1$ 個以内にあれば、先頭~直前までを全て削除することで先頭に持ってこれる。
②末尾から $K$ 個以内にあれば、直後~末尾を全て削除or回転させた上で、その数字を回転させれば先頭に持ってこれる。

②は自身も回転操作しなければいけないので $K$ 個だが、 ①は自身より前を削除してしまえばよいので $K+1$ 個目まで先頭になれる点に注意。

で、同じ数字は現れないので、辞書順最小を作るにはこの中の最小値を先頭として採用確定してしまってよい。

最適な先頭要素が決まったら、もう回転を行う必要は無い。
よって、後は削除だけで、どこまで最終形の2項目以降を辞書順で小さくできるか、という問題となる。

ここで、最小要素を先頭に持ってくるのに①を使ったか②を使ったかで、処理の一部が異なる。

①の場合

     K=5    5 3 1 9 4 2 6 8 7 10 11
     ↓
残り K=3    1 9 4 2 6 8 7 10 11      確定したところの次以降、
            ^ ~~~~~~~                残K+1 個の中の最小値を次に採用
     ↓
残り K=1    1 2 6 8 7 10 11
              ^ ~~~
     ↓
残り K=1    1 2 6 8 7 10 11
                ^ ~~~
     ↓
残り K=0    1 2 6 7 10 11      K=0 になったら終了
                  ^

②の場合

     K=7    5 3 9 7 6 1 8 2 4
     ↓                          とりあえず"1"以降は全て回転させたものとして扱う
残り K=3    1 8 2 4|5 3 9 7 6

この後、①と同様に2項目以降を確定させていくのだが、 回転によって前に持ってきたものを削除する場合は、 回転させる代わりに削除したと考えればよいので、残りKは減らさなくてよい。

最小値を取得すべき区間の右端は、「確定したところの次か、回転させた境界のうち、より右にある方から $K+1$ 個」ということになる。

残り K=3    1 8 2 4|5 3 9 7 6
            ^ ~~~~~~~~~~~~~
↓
残り K=3    1 2 4|5 3 9 7 6
              ^ ~~~~~~~~~
     ↓
残り K=2    1 2 3 9 7 6
                ^ ~~~~~
     ↓
残り K=0    1 2 3 6
                  ^

こちらの方は、$K=0$ になったら終了、とは限らない。
まだ回転させた要素が残っていた場合は、それをコスト0で削除できるからである。

「確定したところが回転させた境界を越えた上で、$K=0$ になった」ら終了だが、 正しい判定(またはそれが正しい証明)を考えるのが難しければ「最後まで確定したら」でもよい。

補足事項など

$N$ と $K$ の関係によっては①も②も使える場合があるが、そのときは両方試してみればよい。
まぁ、上手くやればこの2つはまとめることもできるか。

区間最小値の取得はセグメント木が使える。これを使うのが混乱しづらいと思う。
または、区間の左端・右端はともに常に進んでいくので、Dequeを使って尺取法のように最小値を管理する「スライド最小値」を少し応用したものを使えば、logが取れて若干高速になる。

残り $K$ が残ったまま右端まで確定してしまうことがある。
その場合、確定した要素は全て昇順に並んでいるはずである。
長さが短い方が辞書順は小さくなるので、確定した数列の末尾から残り $K$ だけ、消す必要がある。
(そのため、操作回数は必ず $K$ 回、フルで使われる)

残K = 2    1 2 3 6 7 9
↓
残K = 0    1 2 3 6

Python3

G - LIS with Stack

G - LIS with Stack

問題

解法

解法が分かってみれば確かに自然に見えるけど、、、性質を理解するのはなかなか難しい。

いくつかの単純なケースを考えてみる。

元々の A が単調減少な場合、全てを S に積んでから X に移動させるとよい
A: [5 4 3 2 1]  S: []  X: []
  ↓
A: []  S: [5 4 3 2 1]  X: []
  ↓
A: []  S: []  X: [1 2 3 4 5]

スタックを介すると並び順は逆転するため、元々のAは単調減少な成分が多い方がよさそう

単調増加でも、1個ずつ確定させれば可能
A: [1 2 3 4]  S: []  X:[]
  ↓
A: [2 3 4]  S: [1]  X:[]
  ↓
A: [2 3 4]  S: []  X:[1]
  ↓
... 1個ずつ A→S→X への移動を繰り返す

ただ、$A$ で増加している2つを両方とも利用するなら、途中で $X$ に確定させることが必要なので、 これより後に小さい値が来ても採用できなくなる。

A: [2 3 1]  S: []  X:[]
  ↓                     2をA→S→Xに移し、3をA→Sに移動
A: [1]  S: [3]  X:[2]

この時、Aに残る1は、現時点で末尾が 2 である X に積むことはできないので、捨てるしかない
一気に積んでからちょっとずつ取り出すこともできる

A: [10 9 6 5 2 1 4 3 8 7]
  ↓                                   先頭6つをSに積む
A: [4 3 8 7]  S: [10 9 6 5 2 1]
  ↓                                   1,2をXに移す
A: [4 3 8 7]  S: [10 9 6 5]  X: [1 2]
  ↓                                   Aの2つをSに積む
A: [8 7]  S: [10 9 6 5 4 3]  X: [1 2]
  ↓                                   3~6をXに移す
A: [8 7]  S: [10 9]  X: [1 2 3 4 5 6]
  ↓                                   Aの2つをSに積む
A: []  S: [10 9 8 7]  X: [1 2 3 4 5 6]
  ↓                                   7~10をXに移す
A: []  S: []  X: [1 2 3 4 5 6 7 8 9 10]

区間DP

制約が小さいこともあり、区間DPでできないか考える。

ある隣接区間 $[l,m)$ と $[m,r)$ からそれぞれ何らかの増加部分列が得られるとして、 それを区間 $[l,r)$ 全体を通して操作した際にも、個々から得られるものをつなげたものが実現できるか?

これを「結合可能」と呼ぶとする。
結合可能かどうかの要件として、少なくとも増加部分列として使われる値の範囲がかぶっていないことが必要となる。

よって、増加部分列として使う値の最小と最大を含めたDPを考える。
他にもいくつか遷移できる条件(後述)があって、結局、以下のようなDPを構築すればよい。

初期化は、区間の長さが1($l+1=r$)の箇所に対して、以下のように初期化できる。
1要素だけなら当然、長さ1の最長増加部分列を作れる。またそれは $S$ に積んだままにできるので、$k=0$ である。

いま、$DP[l,r]$ を求めたくて、それより短い区間のDPの値は全て求まっているとする。

端の要素を使わない場合

各 $k,a,b$ に対して、左か右を1個削ったものと等しくなる。

端の要素を使うかも知れない場合

$[l,r)$ を2つに分割して、結合可能なら結合した値で更新する。

遷移元として、以下の3種類の変数がある。

つまり、$l,r$ ごとに以下の計算が必要になる。$l,r$ の総当たりも含め、だいたい $O(N^6)$ かかることになる。

定数倍が軽く、数十分の1の係数が付くため、$N=50$ でも間に合うことを期待して考察を続ける。

遷移としては、$m,a,b,c$ を決めた上で、遷移元の $k$ と値範囲の大小に対し8通りの組み合わせがあるため、それぞれの遷移先を考える。

これを区間の短い方から埋めていくと、$DP[1,N+1,k,1,50]$ での $k=0/1$ の大きい方が答え。

補足

DPで、$a,b$ を「最小値がちょうど $a$ で最大値がちょうど $b$」ではなく「$a$ 以上 $b$ 以下」としているのは、 値の範囲に関して $a,b,c$ を固定した遷移が必要なためである。

「以上、以下」で実装していれば、上記の通り $a \le b \le c$ なる3組の探索 $O(A_{max}^3)$ でよい。

一方、もし「ちょうど」だけをDPに記録していたなら、 「前半区間の最小・最大」「後半区間の最小・最大」を探索せねばならず4重ループとなり、$O(A_{max}^4)$ となって計算量が増えてしまう。

このDPを「以上、以下」で実装するうえで必要なのは、 初期値で $DP[i,i+1,0,A_i以下,A_i以上]$ のように、ジャスト $A_i$ の箇所以外も埋めておくことのみであり、 後は特に意識しなくても、定義通りの更新がされていく。

Python3