AtCoder Beginner Contest 278 備忘録
コンテストページ:AtCoder Beginner Contest 278 - AtCoder
コンテスト中AC:A〜D
E,Fはコンテスト後に自力AC
E - Grid Filling
このユーザ解説と同じ方法で解きました。
想定解法は二次元累積和だったようです。
実装:Editorial - AtCoder Beginner Contest 278
二次元累積和の構造体を作って解いてみました。
実装:Submission #36758941 - AtCoder Beginner Contest 278
F - Shiritori
後退解析+bitDP(巡回セールスマン問題)の考え方で解けます。
問題を有向グラフに言い換えておきます。
- を頂点とする。
- を言った後にを言える場合、頂点から頂点へ向かう有向辺があるとする。
- 有向グラフ上を交互に移動することを繰り返し、移動できなくなった方が負け
制約上、bitDPが疑われるので、その要領で考えてみます。
これまでに訪問した頂点集合がで頂点で自分のターンを終えたとき、勝てるなら1、負けるなら0
まず、"次のターンでこれ以上移動できる頂点がない"となった時点で勝ちが確定します。そこで、訪問した頂点数が多い順から勝敗を確定していけそうです。
これが後退解析にあたります(おそらく)。
では、勝敗の確定条件を考えます。
自分が頂点で移動を終え、から移動可能であって、かつ未訪問の頂点がであるとします。
このうち、いずれか1つでも勝ちとなる頂点が存在する場合、相手はその頂点に移動すれば勝てるので、自分は負けとなります。逆に負けの頂点しかなければ、勝てます。
また、訪問可能な頂点がない場合も自分が勝ちとなる条件となります。
後はこれに従って、bitDPを逆に回していきます。
最終的に"頂点が1つだけ訪問済みである"という状態のDP値(例えば、N=3ならdp[001][0], dp[010][1], dp[100][2]。なお、1つ目のインデックスはbit列)について、頂点のうち、ただ1つでも勝ちがあれば太郎くんが勝ちます。
実装: Submission #36721268 - AtCoder Beginner Contest 278
余談で、DPの別の言い方を示しておきます。
これまでに訪問した頂点集合がで頂点で自分のターンが回ってきた時、勝てるなら0、負けるなら1
この場合、移動できなくなったら負けるので、訪問可能な頂点がなければ負けです。また、相手のターンになったときに負けとなる頂点が1つでもあれば自分は勝てます。
最終的に"頂点が1つだけ訪問済みである"という状態のDP値について、頂点のうち、ただ1つでも負けがあれば、太郎くんが勝ちます(1つだけ訪問済みの状態で順番がくるのは次郎くんであるため)。
実装:Submission #36708784 - AtCoder Beginner Contest 278
AtCoder Beginner Contest 277 備忘録
コンテスト中AC:A〜E
D - Takahashi's Solitaire
コンテスト後に考え直したので、メモしておきます。
一旦modを無視します。
以下の問題を解けばこの問題に答えられます。
から要素を順に選び、とする。但し、は以下の条件を満たさなければならない。
について、
要素の和が最大となるを求めよ。
ここで、を昇順ソートしておくと、はの連続部分列となります。
また、和を最大化したいので、連続部分列は条件を満たす限り伸長させた方が良いです。そのため、各要素は2つ以上の連続部分列に属する必要がありません。
そこで、の連続部分列を探索する際、例えば、が可能な限り伸長した連続部分列であるなら、次の連続部分列の探索はから再開すれば良いです。 探索範囲を右に進めるだけとなるので、となります。
modの部分ですが、これはかつの場合に、これらを含む連続部分列を繋げるようにすれば解決になるわけですが、以下の2パターンで実装してみました。
解決方法1
配列が円環となることが想定される場合の常套手段ですが、を昇順ソートしたものを2つ繋げます。これに対して連続部分列を探索すれば、modを考慮した場合と同様の結果が得られます。
但し、にの全てが含まれる場合は、の要素の和を超えてしまうので注意が必要です。
実装例:Submission #36484745 - Daiwa Securities Co. Ltd. Programming Contest 2022 Autumn (AtCoder Beginner Contest 277)
解決方法2
Union-Findを用いた解法です。0-indexedとします。
を昇順ソートし、について、または、が成り立つなら、Union-Findで同一連結成分とします。
Union-Findにおける連結成分の和を計算していけば、答えを得ることができます。
E - Crystal Switches
解法はこちらの公式解説とほぼ同じなので、省略します。
実装では、を、頂点でスイッチの状態がの時の移動回数の最小値、としました。
また、スイッチを押す操作もダイクストラ法で頂点からに移動する際に考慮されるようにしました。
提出コード:Submission #36446787 - Daiwa Securities Co. Ltd. Programming Contest 2022 Autumn (AtCoder Beginner Contest 277)
スイッチの状態の表現が公式解説と逆なので、ご注意ください。
AtCoder Beginner Contest 275 備忘録
コンテストページ:AtCoder Beginner Contest 275 - AtCoder
コンテスト中AC:A,B,D
コンテスト後AC:E,F(自力)
Cでハマったのと、Eで回数制限があることを忘れて考察してしまいました。
C - Counting Squares
- 点が、正方形の頂点を半時計回りに順番に並べた点である
これが成立するには以下の条件を満たす 必要があります。
- 辺の長さが等しい
- 凸多角形
- 辺のなす角が全て90°
これは、の全てについて、ベクトル
が以下の3つを満たすことと同値です。
2は外積()、3は内積()です。
1、3は正方形なので自明かと思います。2は1本目の辺と2本目の辺の位置関係が半時計回りである必要があるということです。
2は要らなくて3だけでよいのではと思ってしまいますが、内積だけだと時計回りの角度が半時計回りの角度がわからないので必要になります。
全ての点のあり得る4つの組に全てについて、上の条件を満たすものをカウントすると、同じ正方形が4回数えられるので、条件を満たす個数を1/4したのが答えとなります。
コンテスト中は3の条件が必要だと思っておらず、解けませんでした。
提出コード:Submission #36108492 - AtCoder Beginner Contest 275
D - Yet Another Recursive Function
メモ化再帰の問題。種類数の見積もり方法がわからずにとりあえず通しました。見積もり方法は公式解説参照。
提出コード:Submission #36064977 - AtCoder Beginner Contest 275
E - Sugoroku 4
操作回数を考慮しない点で異なりますが、期待値DPと同じような考え方ができます。
の例で考えてみます。
まず、DPを以下のようにしてみます。
:=マスからゴールできる確率
例えば、なら、
という風に計算できます。
なので、後ろから順に更新できそうと思ってしまいますが、今回はを超える場合には戻ってくるので、例えばの場合、
となり計算できません。
問題文をよく読むと操作回数に限度があります。操作を1回すると残りの操作回数が1減るので、残りの操作回数が0となってしまえばそれ以上の操作はできず、確率が確定します。
そこで、DPに操作回数の情報を持たせ、以下のように変更します。
:=残りの操作回数が回で、マスからゴールできる確率
初期値は、
- について、
- について、
と、設定します(その他もすべて0で初期化します) 。
テーブルの更新はメモ化再帰でもいけそうですが、実装では普通のループにしました。
操作回数の順に、マスについて、
と更新します。
なお、の場合には戻るようにする必要があります。
答えはの値です。
計算量はです。これでも通りましたが、和を求める部分は区間和となっているので、累積和を使えば、に落とせそうです。
提出コード:Submission #36076090 - AtCoder Beginner Contest 275
F - Erase Subarrays
部分和DPを応用します。今回DPテーブルには操作回数の最小値を記録します。
普通の部分和DPのように考えると、
:=の番目までの要素まででできる部分列の和がであるときの操作回数の最小値
となりますが、このままだと操作回数の最小値は求められないので、どうするかを考えます。
例えば、で番目の要素で部分列を作ったとします。番目を選んだら1、選ばなかったら0とした数列を考えます。
となり、この時の操作回数の最小値は、閉区間に操作したときの 4回です。
実は、最初の0を除いてとなる数をカウントすれば最小の操作回数に一致します。最初の0のみ、例外として+1しておきます。
以上より、番目の要素を部分列として選んだ状態と選んでいない状態の2つをDPの情報として追加すれば良いです。すなわち、
:=の番目までの要素まででできる部分列の和がであり、かつ状態がの時の操作回数の最小値
但し、で、なら番目を選んでおり、なら選んでいないことを表す。
初期値は、
とし、他はにしておきます。
遷移については実装を見てもらう方が早いと思います。
ポイントとしては、番目がの状態から番目がの状態に遷移する時に操作回数に+1します。
なお、3次元配列をという感じで領域を確保すると、の場合に配列外参照となるので注意が必要です。
提出コード:Submission #36130326 - AtCoder Beginner Contest 275
AtCoder Beginner Contest 274 メモ
コンテスト中AC:A〜D
コンテスト後AC:E,F (いずれも自力)
D - Robot Arm 2
過去問に類題があります。
過去問のリンク (ネタバレ注意)
は、
としてありうる座標は、
としてありうる座標は、
としてありうる座標は、
ここまでで、であるについて、以下の法則が考えられます。
- が奇数なら点を配置できる座標のみが変化し、偶数なら座標のみが変化する。
- 配置可能な座標の集合を、座標の集合をとすると、とから得られる全ての組が示す座標に点を配置できる。
これが一般ので成り立つことは帰納法などによって証明できそうですが、ここでは省略します。2番目の法則はからの変化を図に書いてみるとわかりやすいです。
2番目の法則から、配置可能な座標について、座標を独立に考えれば良いことがわかります。
では、配置可能な座標をどう計算していくかですが、これは部分和DPの応用で解決できます。
すなわち、について座標のに配置可能であればとなるような配列を用意し、これを更新していきます。1番目の法則から偶奇に合わせて更新する座標を変更します。
実装のポイントとして、負の座標は配列に直接保存できないので、定数を加算して平行移動し、全て非負の座標となるようにします。
今回の場合、座標は大雑把に見積もっても〜の範囲内です。そこで、配列にの2倍+αの領域を確保し、原点がとなるように座標にを加算すると良いです。
DPテーブルの更新時に配列外参照をしないようにするチェックを忘れてWAを量産していました。
Submission #35893208 - キーエンスプログラミングコンテスト2022(AtCoder Beginner Contest 274)
E - Booster
巡回セールスマン問題の応用問題です。巡回セールスマン問題はbitDPで解くことができますが、ネットで調べると出てくるので、ここでは詳細は割愛します。
今回は、頂点であるとみなして、bitDPを解きます。普通の巡回セールスマン問題と異なる部分は、以下のとおりです。
- 宝箱に訪れた個数によって、移動速度が異なる
- 宝箱には行かなくてもよい
- 原点から出発し、原点に戻る
それぞれの自分の解決策を示します。
宝箱に訪れた個数によって、移動速度が異なる
これからの説明では0-indexedとします。
まず、DPテーブルは以下のようになります。
訪問状態を示す冪集合の状態で頂点に居る時の移動時間の最小値
なら、ビット目を街に、ビット目を宝箱に割り当てます。
例を示すと、街と宝箱に訪問済みなら、その状態を表すビット列は、11001
です。
これからの説明でDPテーブルの行のインデックスはビット列とします。
DPテーブルの更新の際に、宝箱に対応するビットの1の個数をカウントし、頂点間の距離を個数+1で割れば良いです。
例えば、上の例で頂点の座標がそれぞれであり、訪問状態を表すビット列が11001
の状態で、頂点からに移動する場合、
と更新します。
宝箱には行かなくてもよい
例えば、先程の例と同じ場合、街に対応するビット目がなら、宝箱に対応するビット目はなんでもよいです。
従って、??111
となるような全ての状態に対して、原点に戻るまでの最短時間を調べれば良いです。
原点を出発し、原点に戻る
原点を出発する代わりに、初期値として各頂点のみが訪問済みの状態に、原点からの移動時間を設定すれば良いです。
例えば、で、街の座標がなら、
という感じで設定します。これを各頂点について設定します。
原点に戻るのは、各状態の最小の移動時間を求めた後、原点への移動時間を考慮すれば良いです。
例えば、上の例と同じ条件で考えると、はすべての街(+宝箱1)を訪れ、最後に街にいる時の最小移動時間です。
しかし、このままこの状態の最小移動時間にするのではなく、を、この状態における最終的な最小移動時間とします。
なお、原点を頂点として追加した実装だとTLEになりました。
https://atcoder.jp/contests/abc274/submissions/35914594
F - Fishing
最適解で得られた魚を座標の昇順に並べたものをとします。
これらの魚を捕獲する際、始点となる実数をとして差し支えありません。
これより、魚を始点として捕獲した場合の通り調べれば良さそうだとわかります。
ただし、前提として、各魚を始点として捕獲する場合の最適解が求められる必要があります。
最適解の求め方を示します。
魚を始点として魚捕獲する場合、魚が時間からまで捕獲可能であるという閉区間が、例外を除いて存在します。
そのため、以下の問題を解ければよいです。
正方向に伸びる数直線上に複数の区間と、区間に対応するスコアが与えられる。座標を1つ選ぶと、その座標を含む区間のスコアが得られる。スコアを最大化せよ。
これはいもす法によって解けます。
充分に小さい時間をとします。区間とその区間で得られるスコアが与えられた場合に配列にとという組を追加します。
全ての区間について追加し終えた後、組の第一要素をキーとして昇順にソートします。その後、第二要素の累積和を計算します。
累積和を計算した時に最大となった値が得られる最大のスコアであり、これが最適解です。
このいもす法の計算量はソート部分がボトルネックとなってです。各魚についてこれを計算するので、計算量はとなり間に合います。
区間の求め方ですが、魚と魚の最初の座標と速度の関係で場合分けが必要です。また、区間そのものが存在しない場合もあります。
始点については、
- なら、
- 魚が魚に追いつくなら、を解いて、
- 魚が魚に追いつくなら、を解いて、
終点については、
- でかつなら、は存在しないことにするか、とする
- 魚が魚を引き離すなら、を解いて、
- 魚が魚を引き離すなら、を解いて、
という感じで場合分けします。
Submission #35965103 - キーエンスプログラミングコンテスト2022(AtCoder Beginner Contest 274)
AtCoder Beginner Contest 273 参加記録
2022/10/20 Eに永続スタックによる解答を追記
コンテストページ:Panasonic Programming Contest 2022(AtCoder Beginner Contest 273) - AtCoder
コンテスト中AC:A〜D
Eもコンテスト後に自力で解けました。
D - LRUD Instructions
壁を行と列にわけて保存し、移動方向に合わせて直近の壁を二分探索する、という手法は典型と言えると思います。
今回は、行及び列が非常に多いので、単純に行または列の番号をインデックスとした二次元配列に壁を保存していくことができません。
公式解説では、mapが遅いなと思うことがあり、座標圧縮で対応しました。しかし、その分、実装が複雑になってしまい手間取りました。
また、壁の記録には自作の平衡二分探索木(スプレー木)を使用しました(値の重複を許す、多重集合で実装しています)。ここでは詳細は書きませんが、「以下/未満の最大値を取得する」という操作をサポートします。
動作は保証できませんが、自由に使用して問題ありません。
Submission #35688615 - Panasonic Programming Contest 2022(AtCoder Beginner Contest 273)
E - Notebook
木っぽい感じだと思ったのですが、ポインタを使うような問題をあまり見たことがなかったので、ポインタを使用しない実装方法で解きました。
公式解説の補足を見て、永続スタックについて知りましたが、記載する実装方法はポインタを使用していないだけで、内容は永続スタックと同じでした。
永続スタックの参考↓
あなたの庭に永続データ構造を飾りませんか? - noshi91のメモ
では、実装方法を書きます。
まず、使用する配列や変数です。
- 配列:クエリで追加した値を追加順に保存する
- 変数:各クエリ処理において、の末尾の値が保存されているのインデックスを保存する
- 配列:にはが追加される直前のが保存されるような配列
- ハッシュマップ:ページをkeyとしてを保存する
と初期化しておきます。これはが空であることを示す番兵となります。
後述するを戻す処理をどれだけ行ってもに留まり続けることが可能です。
各クエリの処理です。
ADD x
の末尾にを追加します。の直前にの末尾であった値ののインデックスはなので、の末尾にを追加します。新たなはの末尾のインデックスとなるよう更新します。DELETE
現在のの末尾の値を削除する代わりに、1つ前に末尾だった値に戻ればよいです。すなわち、と更新します。SAVE y
ハッシュマップにをkeyとして現在のを保存します。すなわち、とします。LOAD z
ハッシュマップに保存した位置となるようにを更新します。すなわち、とします。なお、保存されていない場合は、としておけば良いです。
これで各クエリはで処理できます。
Submission #35754741 - Panasonic Programming Contest 2022(AtCoder Beginner Contest 273)
↓永続スタックの解答
Submission #35797075 - Panasonic Programming Contest 2022(AtCoder Beginner Contest 273)
AtCoder Beginner Contest 270 参加記録
コンテストページ:https://atcoder.jp/contests/abc270
コンテスト中AC:A〜D
Eはコンテスト中は実装ミスで間に合いませんでしたが、コンテスト後に解けたので書いておきます。
D - Stones
石が個の状態からは、石が個の状態に遷移します。
そのため、石が少ない時の状態から順に高橋くんが取れる石の個数を決定していくことができれば、くらいで解けそうだということがわかります。
普通のループを使ったDPでも解けそうですが、コンテストではメモ化再帰を使いました。
メモ化再帰にあたり、
石が残り個の状態で、高橋くんが取れる石の最大個数
とおいて計算していきたいところですが、高橋くんのターンか青木くんのターンかで、戦略が異なります。
すなわち、
- 高橋くんは、高橋くんが取れる石の個数を最大化したい
- 青木くんは、高橋くんが取れる石の個数を最小化したい
そのため、どちらのターンなのかという情報を持たせます。
石が残り個、ターンの状態がで、高橋くんが取れる石の最大個数。但し、で、の時は高橋くんのターンで、の時は青木くんのターン。
それでは、遷移を考えます。
各ターンにおいて、をかつ、を満たす最大値とします。
高橋くんのターンの場合
取れる石の個数を最大化します。また、石を取った時に、高橋くんがとれる石の個数に加算できるので、以下のようになります。
青木くんのターンの場合
取れる石の個数を最小化するので、以下のようになります。
遷移は以上です。
また、再帰の終了条件として、石が個の時の値、を設定しておきます。
計算量は最初にも書きましたが、各個数において回の計算となるので、と思います。
Submission #35156369 - TOYOTA MOTOR CORPORATION Programming Contest 2022(AtCoder Beginner Contest 270)
↑はコンテスト後に微修正した実装です。
E - Apple Baskets on Circle
最終的な移動回数をとし、をで割った商を、余りをとします。これは、周してから残り回移動するという風に考えられます。
であることから、を求められれば、残り回はシミュレーションできます。
を求める方法を考えます。
例としてを考えます。
1週目
となり、食べたりんごの数に個加算される。2週目
となり、食べたりんごの数に個加算される。3週目
となり、食べたりんごの数に個加算される。4週目
となり、食べたりんごの数に個加算される。5週目
となり、食べたりんごの数に個加算される。
加算するりんごの数の変化点に注目すると、
- 最初のの最小値であるがとなるまで、空でないかごの個数分個を加算する
- が空になった時点のの最小値であるが空となるまで、空でないかごの個数分個加算する
- が空になった時点のの最小値であるが空となるまで、空でないかごの個数分個加算する
このように、りんごの数が最小となるかごが空になるまでは同じ分だけりんごの数が加算されるので、まとめて考えることができます。
そこで、以下のように操作していくと、食べたりんごの数と現在何周したかを計算していくことができます。
から最小値を取得する。ここから周して、合計個のりんごを食べる。その後、の番目の要素を削除し、全ての要素からを引く。
最小値の取得や値の削除は優先度付きキューにより実現できます。
あとは、を引く操作に時間がかかるので、今までに何周したのかというのをに記録します。すると、以下のようにできます。
から最小値を取得する。周して、個のりんごを食べたことにする。その後、と更新し、の番目の要素を削除する。
さて、この操作を繰り返すと、どこかの時点で「次の操作をすると食べたりんごの数が以上となる」状態になります。
この状態からあと何周必要かを計算します。残りの周回数は、この時点でのの最小値以下であることが保証されます。
この時点からの残りの周回数をとおくと、以下の不等式が成り立てばよいです。
よって、
です。
よって、先ほどの操作と今回の操作で得られた値を合計すれば、が求まります。
あとは、周した状態(元ののすべての要素からを引いた状態)からりんごの数がとなるまでシミュレーションすればよいです。
長くなりましたが、要点をまとめるとこのような流れになります。
- の最小値が0になるまで周回するという操作を繰り返す
- 上記の操作でりんごの食べた数が以上となる直前で操作を終了し、残り何周必要かを計算する
- 全体で何周すれば良いかがわかったので、その分だけ周回した後から、残りをシミュレーションする
AtCoder Beginner Contest 267 F - Exactly K Steps
最遠点を求めた後にLink-Cut Treeで回移動する解法で解いてみました。
ここでは、Link-Cut Treeの詳細な説明は割愛し、必要な部分だけを記載します。
詳細は以下のサイトを見るのが良いと思います。
Link-Cut Treeの実装メモ - 日々drdrする人のメモ
Link-Cut木では、以下のような操作ができます。
- :頂点を根にする
- :根から頂点までのパスをHeavy-edgeで繋ぐ(パス上の頂点から出るその他の辺は全てLight-edgeになる)
頂点を根とし、頂点までのパスをHeavy-edgeで繋ぐと、間に含まれる全ての頂点が同一の平衡二分探索木によって管理されます。
この平衡二分探索木の頂点を通りがけ(in-order)順に並べると間のパス順になるようになっています。
続いて、平衡二分探索木を通りがけ順に並べた時の番目の頂点を高速に得る方法を考えます。
まず、平衡二分探索木に部分木のサイズを持たせます。すると、通りがけ順で番目となる頂点が、今いる頂点の左部分木か、右部分木か、自身かというのが、以下の場合分けで判定できます。 なお、1-indexedで並べるものとします。
=頂点の左部分木のサイズ+1
とします。+1は頂点の分です。
の場合
通りがけ順では、左部分木を全て並べた後、頂点を並べます。
左部分木のの頂点を並べた後に、番目としてを並べるので、このケースではを返せば良いです。の場合
左部分木に番目が存在するので、の左の子の番目を調べます。の場合
右部分木に番目が存在するので、の右の子について、個の頂点を除いた番目を調べます。
このように根から順に二分探索のような感じで調べていくことで番目の頂点を得ることが可能です。
計算量は、平衡二分探索木の高さ分の探索をするので、平衡二分探索木であることを考えると、ならしlog時間くらいになると思われます。
以上から、元の問題を以下のように解くことができます。
- 頂点からの最遠点を、すべての頂点について求める
- 各クエリで与えられるの頂点をLink-Cut Treeの根とし、最遠点を根と繋ぐ
- からまでのパス上における番目の頂点をLink-Cut Treeの平衡二分探索木から求める
計算量を考えます。
最遠点は、全方位木DPにより求めたので、です。
Link-Cut Treeで行う各処理は、で処理できると思うので、です(おそらく)。
よって、全体でだと思います。
提出コード:Submission #34979495 - NEC Programming Contest 2022 (AtCoder Beginner Contest 267)
Link-Cut Treeにはモノイドを乗せるようにしてあるので、構造体等が定義されていますが、この問題を解くにあたっては不要です。