COLOCON -Colopl programming contest 2018- C - すぬけそだて――ごはん――
解法
あらかじめ、ある数とが同時に存在しうるか、を調べておきます。
使用できる数を、前半の20個と、残りの2つの部分に分け、それぞれに処理をしていきます。
最終的には、前半の状態を1つ決めた時の、後半の場合の数を高速に計算して、前半の状態を全探索します。
後半部分について、前計算で以下のようなDPを行います。
後半の数から選んだ集合の中でのカードの選び方
を、に含まれる最大の数とします。を、の中で、と同時に選べる数の集合とします。すると、
というようにして計算を行っていくことができます。
あとは、前半部分の集合を1つ決めたときに、その集合に対して使用できる後半部分の集合を計算し、その集合に対応するDPの配列を参照して足していくことで、答えを求めることができます。
最初に計算した同時に存在しうるかどうか、の情報をlong long型の変数で持っておくと、その後の計算が楽かつ高速に行えるので、この問題に答えることができます。
感想
模擬国内Eの復習をサボっていたら同じような問題にあたってしまいました…
と思っていたのですが、bitDPなり全探索なりでもいけるみたいですね!
AOJ 2200 - Mr. リトー郵便局
解法
まずは、陸路と海路それぞれについて、ワーシャルフロイド法を用いて、任意の町間の最短距離を計算しておきます。町から町の最短経路について、陸路を、海路をとします。
そして、DPを行います。
回目の集配を終えた時点で、船が町にあるような経路の中で、時間が最短のもの
とします。そして、回目の集配を陸路のみで行うか、海路も含めて行うかの2パターンに分けて計算します。
初期値は、となります。
陸路のみを利用する場合
回目の集配を終えても、その前後で船の位置は移動しません。ので、
となります。海路も利用する場合
町から町まで海路を利用したとします。すると、
が今回かかる時間なので、
となります。
あとは、このDPを行い、の最小値を求めれば答えとなります。
AOJ 1132 - Circle and Points
解法
1個のみ円の中に入るパターンは明らかなのでここでは2個以上のパターンのみ考えます。
円の中に入る点の個数が最大の円の配置方法があるとき、その個数を減らさないまま、ある2点を同時に円周上に乗せることができます。
ということで、これ以降は2点が円周上にあるパターンの円の配置方法について、点の個数が最大となるパターンを探せばよいです。
ある距離が1以下の2点が存在するとき、上の図のような2つの円のパターンが存在します。それぞれについて、入る点の個数を求めれば良いです。
円の中心座標が求まれば、全部の点について、中心からの距離が1以下であるかどうかを調べ、1以下であるものをカウントすれば円の中に入る点の個数がわかります。
ということで、あとは円の中心座標がわかればよいです。
今、図の線分が2点を結ぶ直線だとします。この際の、中心座標を求めます。
です。
また、線分の長さは2で、も、2点の座標がわかっているので求めることができます。ということで、三平方の定理から、の長さも求めることができます。これにより、を求めることができ、円の中心はとの中点なので、上手く計算することができます。
あとは、全部の円について中心を求め、その中に入る点の個数の最大値を計算すれば答えとなります。
感想
ある点を中心に持ってきてもダメそう→2点を結ぶ直線を円内にいれたいよね→なんかある点はギリギリに持ってきた方がよさそう→だからなんだろう…?(終)
という感じで考察が終わってしまったので、幾何は苦手です(?)
周りの人々がだいたい秒殺していたので、負けないように頑張りたいです…
AOJ 1176 - 輪番停電計画
解法
はじめに、2次元累積和を用いて、ある長方形の範囲の需要の合計をで求めることができるようにしておきます。
ここから、DPを行います。
長方形の左上が,右下がとなるような範囲のグループの分け方の最適解
とします。最適解、なのでグループ数と予備力の2つの情報を持ち、グループ数、予備力、の優先順位でより値が大きいものが最適解となります。
そもそもですが、左上が,右下がとなるような範囲を停電させても供給力が足りない場合、すなわち
ならば、答えはありません。
それ以外の場合を見ていきます。
答えの候補ですが、
今見ている範囲をこれ以上分割しない
今見ている範囲をある縦線で区切る
今見ている範囲をある横線で区切る
の3パターンになります。全ての候補を調べ上げ、その中での最適解を選べばよいです。
一番最初のパターンは、グループ数がもちろん0、予備力はそのものです。
また、2つめのパターンと3つめのパターンでは、縦横が異なるのみで操作自体は同じなので、今回は横線で区切るパターンを見ていきます。
左上が,右下がとなっている範囲を横線で区切ると、
左上が,右下がと、左上が,右下が
の2つの範囲に分割されます。
まずは、それぞれの範囲の最適解を調べます。これは、再帰的に調べればよいです。
あとは、2つに分けた範囲それぞれのグループ数の和と、2つに分けた範囲それぞれの予備力のうち小さい方をセットにすれば、今見ている左上が,右下がの範囲の答えの候補になります。
後は適当にメモ化再帰を行えば、答えを求めることができます。
感想
条件がややこしかったり次元が大きかったりしましたが、丁寧に詰めたのでさらっと解くことができました。
多少複雑な問題でも頭が真っ白にならないようにしたいですね。
AOJ 0570 - ジグザグ数 (Zig-Zag Numbers)
解法
以下のジグザグ数の個数から、以下のジグザグ数の個数を引くと答えになるので、あとは以下のジグザグ数の個数を求める方法がわかればよいです。
桁が明らかに大きいので桁DPをしていきます。
について、
: 今見ている桁の番号
: 以下が確定しているかどうか(1,0で管理)
: 今の桁と1つ前の桁について、ジグザグの増加と減少がどちらか(1,0で管理)
: 今どの番号を当てはめるか(0 ~ 9)
: 今の段階でのmod の値
を考えていきます。
状態数が多い(面倒くさい)のでざっくりでいきますが、1つ前の遷移と今の遷移を見比べたときに、は交互になるように遷移していきます。そして、ジグザグの増加(減少)の条件を満たすように、今見ている桁の数字と1つ前の桁の数字を当てはめていきます。あとは、桁DPおなじみの以下かどうかの判定を行いつつ、の値をうまく計算しながらDPをしていけばよいです…
のですが、桁の数字に対して、桁未満の数字を数え上げるとき、
のように0埋めされている、と考えてしまうと、これはジグザグ数ではなくなってしまいます。ので、桁目の遷移を考えるときに、それより上の桁が0埋めされている数についてあらかじめカウントしてあげる必要があります。
このコーナーケースの処理も書くと、この問題の答えを求めることができます。
感想
桁DPに見えるので遷移を考えると、情報を適当にもっていても計算量的には大丈夫、ということで書き始めたのですが、mod の計算の部分でバグを埋め込んでしまい、時間を溶かしてしまいました…
桁DPのうまいデバッグ方法がわからないので、訓練したいです。
AGC036 C - GP 2
解法
まず、最終的な数列の中に奇数が個存在するときについて考えます。は最大でとなります。
すると、残ったに1加算する操作を2つまとめて、に2加算する操作回分に置き換えることができます(もちろん、残った回数が奇数の際は端数が発生するので、考慮せずスルーします)。
すると、個の奇数を選ぶのは、で求めることができます。そして、に2加算する操作回を個の数字に割り振るのは、で求めることができます。
基本的にはこれでよいのですが、いくつか実現不可能なパターンが存在します。の制限があるためです。
これは主に2つのパターンがあります。数列のどこか1つが以上の奇数パターンと、以上の偶数パターンです。どちらのパターンも、そのような数字が同時に2個以上出現することはありません(全体の総和がにしかならないからです)。
以上の奇数パターン
仮にがその値だったと仮定して、最後に倍してあげることで網羅できます。が奇数なので、それ以外に個の奇数が存在するはずです。この割り振り方は、通りです。
そして、2を加算する操作は、回だけ残っています(少なくとも回はに割り振らなければなりませんが、それ以外はどこに割り振ってもいいです)。よって、割り振り方は通りとなります。以上の偶数パターン
1つめのパターンと同様に考えると、奇数の割り振りはとなります。
2を加算する操作の割り振りは、となります。今回は、に少なくとも回割り振る必要があるので、残る回数は回です。
あとは、最初に計算した全体から、実現不可能なパターンを引けばよいです。
となります。
これを全てのに対して計算を行い、総和を求めれば答えとなります。
感想
奇数の個数を決め打ちして、2に加算する回数にまとめてしまう、という考え方が天から降ってきました(わーい)。
実現できないコーナーケースを見つけて、冷静に対処できたのも良かったです。
AGC036 B - Do Not Duplicate
解法
同じ状況になる部分をどんどん取り除き、最終的に残る数回を愚直にシミュレーションします。
まずは、空の数列に対して番目の数字をに入れたときに、を取り除くことで再び数列が空になるタイミングはどこか、を調べます。
これは、の種類ごとにを昇順にまとめ、に対してはとなるような、となるのうち最小の物をメモすればよいです(もし存在しなければ、となる最小のをメモします。となることもあります)。
次に、初期状態(が空の状態で、次に挿入するものがであるタイミング)まで一回ループするのに何回操作をするか、を調べます。
これは、次に操作する数列の番号を最初にとし、先ほどのメモを見ながら、数列が空になるタイミングごとに区切って調べていけばよいです。
に対してのメモがの場合、回操作を追加すると数列が空になり、次に操作する対象は番目になります。ので、操作回数にを加算し、と再定義します。ただし、の際は、となります。
これを、再びとなるまで行います。
メモの種類はもちろん添え字の分だけなので、これは最悪でも回の操作となります。
が空かつ、で操作を開始して、再び同じ状態に戻ってくるまでの回数をとします。すると、操作をする回数は、最終的にとなります。これを、新たにと定義しなおします。
この時点でのは、最大で程度です(が全て異なるパターンです)。
ここから、さらに値を減らします。
先ほど、1回ループするまでの回数をシミュレーションをしましたが、これと同じ操作をしつつ、今度はをから直接減らしていきます。そして、を減らすとが負になる、というタイミングでやめます。このシミュレーションは、先ほどと同じく最悪でも回の操作で済みます。
こうすると、残っている操作回数は、最大でもとなります(次に操作する添え字を問わず、数列が空の状態から再び空になる際の最悪ケースは、先ほどと同じでが全て異なるパターンだからです)。
ということで、操作回数をまで減らすことができたので、あとは現状のから愚直にシミュレーションをしていけばよいです。
これは、stackを使うと、最大で回程度のプッシュ、ポップの操作になるので十分間に合います。すでにスタック内に存在するかどうかは、setを利用しました(こっちの方が計算量的には重くなると思います)。
計算量的には、おそらくです(自分は最初のごとにまとめる操作を行う際にc++のmapを使用したため)。
感想
ループをするのでその部分をどんどん減らす、という発想が割とはやいタイミングでできたので良かった…です。