ツバサの備忘録

主に備忘録代わりに精進記録を載せていくつもりです。

COLOCON -Colopl programming contest 2018- C - すぬけそだて――ごはん――

問題
提出コード

解法

あらかじめ、ある数xyが同時に存在しうるか、を調べておきます。
使用できる数を、前半の20個と、残りの2つの部分に分け、それぞれに処理をしていきます。
最終的には、前半の状態を1つ決めた時の、後半の場合の数を高速に計算して、前半の状態を全探索します。
後半部分について、前計算で以下のようなDPを行います。
dp[S] = 後半の数から選んだ集合Sの中でのカードの選び方
 iを、Sに含まれる最大の数とします。Pを、S \setminus \{i\}の中で、iと同時に選べる数の集合とします。すると、
dp[S] = dp[S \setminus \{i\}] + dp[P]
というようにして計算を行っていくことができます。
あとは、前半部分の集合を1つ決めたときに、その集合に対して使用できる後半部分の集合を計算し、その集合に対応するDPの配列を参照して足していくことで、答えを求めることができます。
最初に計算した同時に存在しうるかどうか、の情報をlong long型の変数で持っておくと、その後の計算が楽かつ高速に行えるので、この問題に答えることができます。

感想

模擬国内Eの復習をサボっていたら同じような問題にあたってしまいました…
と思っていたのですが、bitDPなり全探索なりでもいけるみたいですね!

AOJ 2200 - Mr. リトー郵便局

問題
提出コード

解法

まずは、陸路と海路それぞれについて、ワーシャルフロイド法を用いて、任意の町間の最短距離を計算しておきます。町iから町jの最短経路について、陸路をl_{ij}、海路をs_{ij}とします。
そして、DPを行います。
dp[i][j] = i回目の集配を終えた時点で、船が町jにあるような経路の中で、時間が最短のもの
とします。そして、i回目の集配を陸路のみで行うか、海路も含めて行うかの2パターンに分けて計算します。
初期値は、dp[1][j] = s_{z_1 \ j}となります。

  • 陸路のみを利用する場合
    i回目の集配を終えても、その前後で船の位置は移動しません。ので、
    dp[i][j] = min(dp[i][j] ,dp[i - 1][j] + l_{z_{i -1 } \ z_{i}} )
    となります。

  • 海路も利用する場合
    kから町jまで海路を利用したとします。すると、
    (z_{i - 1}からkまでの陸路) + (kからjまでの海路) + (jからz_{i}までの陸路)
    が今回かかる時間なので、
    dp[i][j] = min(dp[i][j] , dp[i - 1][k] + l_{z_{i - 1} \ k} + s_{k \ j} + l_{j \ z_{i}} )
    となります。


あとは、このDPを行い、dp[r][j]の最小値を求めれば答えとなります。

AOJ 1132 - Circle and Points

問題
提出コード

解法

1個のみ円の中に入るパターンは明らかなのでここでは2個以上のパターンのみ考えます。
円の中に入る点の個数が最大の円の配置方法があるとき、その個数を減らさないまま、ある2点を同時に円周上に乗せることができます。
ということで、これ以降は2点が円周上にあるパターンの円の配置方法について、点の個数が最大となるパターンを探せばよいです。
f:id:emtubasa:20190811170916p:plain
ある距離が1以下の2点が存在するとき、上の図のような2つの円のパターンが存在します。それぞれについて、入る点の個数を求めれば良いです。
円の中心座標が求まれば、全部の点について、中心からの距離が1以下であるかどうかを調べ、1以下であるものをカウントすれば円の中に入る点の個数がわかります。
ということで、あとは円の中心座標がわかればよいです。
f:id:emtubasa:20190811172202p:plain
今、図の線分ABが2点を結ぶ直線だとします。この際の、中心座標を求めます。
\angle ABC = 90^{\circ}です。
また、線分ACの長さは2で、ABも、2点の座標がわかっているので求めることができます。ということで、三平方の定理から、BCの長さも求めることができます。これにより、 \overrightarrow{BA},  \overrightarrow{BC}を求めることができ、円の中心はACの中点なので、上手く計算することができます。
あとは、全部の円について中心を求め、その中に入る点の個数の最大値を計算すれば答えとなります。

感想

ある点を中心に持ってきてもダメそう→2点を結ぶ直線を円内にいれたいよね→なんかある点はギリギリに持ってきた方がよさそう→だからなんだろう…?(終)
という感じで考察が終わってしまったので、幾何は苦手です(?)
周りの人々がだいたい秒殺していたので、負けないように頑張りたいです…

AOJ 1176 - 輪番停電計画

問題
提出コード

解法

はじめに、2次元累積和を用いて、ある長方形の範囲の需要の合計をO(1)で求めることができるようにしておきます。
ここから、DPを行います。
dp[i][j][k][l] = 長方形の左上が(i,k),右下が(j,l)となるような範囲のグループの分け方の最適解
とします。最適解、なのでグループ数と予備力の2つの情報を持ち、グループ数、予備力、の優先順位でより値が大きいものが最適解となります。

そもそもですが、左上が(i,k),右下が(j,l)となるような範囲を停電させても供給力が足りない場合、すなわち s-(与えられた町全体の需要)+(今見ている範囲の需要の合計) \lt 0ならば、答えはありません。
それ以外の場合を見ていきます。
答えの候補ですが、

  • 今見ている範囲をこれ以上分割しない

  • 今見ている範囲をある縦線で区切る

  • 今見ている範囲をある横線で区切る

の3パターンになります。全ての候補を調べ上げ、その中での最適解を選べばよいです。
一番最初のパターンは、グループ数がもちろん0、予備力はs-(与えられた町全体の需要)+(今見ている範囲の需要の合計)そのものです。
また、2つめのパターンと3つめのパターンでは、縦横が異なるのみで操作自体は同じなので、今回は横線で区切るパターンを見ていきます。
左上が(i,k),右下が(j,l)となっている範囲を横線で区切ると、
左上が(i,k),右下が(x,l)と、左上が(x+1,k),右下が(j,l)
の2つの範囲に分割されます。
まずは、それぞれの範囲の最適解を調べます。これは、再帰的に調べればよいです。
あとは、2つに分けた範囲それぞれのグループ数の和と、2つに分けた範囲それぞれの予備力のうち小さい方をセットにすれば、今見ている左上が(i,k),右下が(j,l)の範囲の答えの候補になります。

後は適当にメモ化再帰を行えば、答えを求めることができます。

感想

条件がややこしかったり次元が大きかったりしましたが、丁寧に詰めたのでさらっと解くことができました。
多少複雑な問題でも頭が真っ白にならないようにしたいですね。

AOJ 0570 - ジグザグ数 (Zig-Zag Numbers)

問題
提出コード

解法

B以下のジグザグ数の個数から、A-1以下のジグザグ数の個数を引くと答えになるので、あとはX以下のジグザグ数の個数を求める方法がわかればよいです。
桁が明らかに大きいので桁DPをしていきます。
dp[i][j][k][l][o]について、
i : 今見ている桁の番号
j : X以下が確定しているかどうか(1,0で管理)
k : 今の桁と1つ前の桁について、ジグザグの増加と減少がどちらか(1,0で管理)
l : 今どの番号を当てはめるか(0 ~ 9)
o : 今の段階でのmod Mの値
を考えていきます。
状態数が多い(面倒くさい)のでざっくりでいきますが、1つ前の遷移と今の遷移を見比べたときに、kは交互になるように遷移していきます。そして、ジグザグの増加(減少)の条件を満たすように、今見ている桁の数字と1つ前の桁の数字を当てはめていきます。あとは、桁DPおなじみのX以下かどうかの判定を行いつつ、oの値をうまく計算しながらDPをしていけばよいです…
のですが、p桁の数字Xに対して、p桁未満の数字を数え上げるとき、
0000...のように0埋めされている、と考えてしまうと、これはジグザグ数ではなくなってしまいます。ので、i桁目の遷移を考えるときに、それより上の桁が0埋めされている数についてあらかじめカウントしてあげる必要があります。
このコーナーケースの処理も書くと、この問題の答えを求めることができます。

感想

桁DPに見えるので遷移を考えると、情報を適当にもっていても計算量的には大丈夫、ということで書き始めたのですが、mod Mの計算の部分でバグを埋め込んでしまい、時間を溶かしてしまいました…
桁DPのうまいデバッグ方法がわからないので、訓練したいです。

AGC036 C - GP 2

問題
提出コード

解法

まず、最終的な数列の中に奇数がp個存在するときについて考えます。pは最大でmin(m,n)となります。
すると、残ったx_{j}に1加算する操作を2つまとめて、x_{i}に2加算する操作(m-p)/2回分に置き換えることができます(もちろん、残った回数が奇数の際は端数が発生するので、考慮せずスルーします)。
すると、p個の奇数を選ぶのは、_{N}C_{p}で求めることができます。そして、x_{i}に2加算する操作m + (m-p)/2回をN個の数字に割り振るのは、_{m + (m-p)/2}H_{N} = _{m + (m-p)/2 + N -1}C_{N-1}で求めることができます。
基本的にはこれでよいのですが、いくつか実現不可能なパターンが存在します。i \neq jの制限があるためです。
これは主に2つのパターンがあります。数列のどこか1つが2M+1以上の奇数パターンと、2(M+1)以上の偶数パターンです。どちらのパターンも、そのような数字が同時に2個以上出現することはありません(全体の総和が3Mにしかならないからです)。

  • 2M+1以上の奇数パターン
    仮にx_{0}がその値だったと仮定して、最後にN倍してあげることで網羅できます。x_{0}が奇数なので、それ以外にp-1個の奇数が存在するはずです。この割り振り方は、_{n-1}C_{p-1}通りです。
    そして、2を加算する操作は、(m-p)/2回だけ残っています(少なくともM回はx_{0}に割り振らなければなりませんが、それ以外はどこに割り振ってもいいです)。よって、割り振り方は_{(m-p)/2}H_{N}通りとなります。

  • 2(M+1)以上の偶数パターン
    1つめのパターンと同様に考えると、奇数の割り振りは_{n-1}C_{p}となります。
    2を加算する操作の割り振りは、_{(m-p)/2-1}H_{N}となります。今回は、x_{0}に少なくともM+1回割り振る必要があるので、残る回数は(m-p)/2 - 1回です。

あとは、最初に計算した全体から、実現不可能なパターンを引けばよいです。
_{N}C_{p} \times _{m + (m-p)/2}H_{N} - N ( _{n-1}C_{p-1} \times _{(m-p)/2}H_{N} - _{n-1}C_{p} \times _{(m-p)/2-1}H_{N})
となります。
これを全てのpに対して計算を行い、総和を求めれば答えとなります。

感想

奇数の個数を決め打ちして、2に加算する回数にまとめてしまう、という考え方が天から降ってきました(わーい)。
実現できないコーナーケースを見つけて、冷静に対処できたのも良かったです。

AGC036 B - Do Not Duplicate

問題
提出コード

解法

同じ状況になる部分をどんどん取り除き、最終的に残る数回を愚直にシミュレーションします。
まずは、空の数列sに対してi番目の数字A_{i}sに入れたときに、A_{i}を取り除くことで再び数列が空になるタイミングはどこか、を調べます。
これは、A_{i}の種類ごとにiを昇順にまとめ、iに対してはA_{i} = A_{j}となるような、i \lt jとなるjのうち最小の物をメモすればよいです(もし存在しなければ、A_{i} = A_{p}となる最小のpをメモします。i = pとなることもあります)。
次に、初期状態(sが空の状態で、次に挿入するものがA_{0}であるタイミング)まで一回ループするのに何回操作をするか、を調べます。
これは、次に操作する数列の番号iを最初にi = 0とし、先ほどのメモを見ながら、数列が空になるタイミングごとに区切って調べていけばよいです。
iに対してのメモがjの場合、j - i + 1回操作を追加すると数列が空になり、次に操作する対象はj+1番目になります。ので、操作回数にj - i + 1を加算し、i = j + 1と再定義します。ただし、i \geqq jの際は、j - i + 1 + Nとなります。
これを、再びi = 0となるまで行います。
メモの種類はもちろん添え字iの分だけなので、これは最悪でもN回の操作となります。
sが空かつ、i = 0で操作を開始して、再び同じ状態に戻ってくるまでの回数をLとします。すると、操作をする回数NKは、最終的に NK \ mod \ Lとなります。これを、新たにKと定義しなおします。
この時点でのKは、最大でN^{2}程度です(A_{i}が全て異なるパターンです)。
ここから、さらに値を減らします。
先ほど、1回ループするまでの回数をシミュレーションをしましたが、これと同じ操作をしつつ、今度はj - i + 1Kから直接減らしていきます。そして、j - i + 1を減らすとKが負になる、というタイミングでやめます。このシミュレーションは、先ほどと同じく最悪でもN回の操作で済みます。
こうすると、残っている操作回数Kは、最大でもNとなります(次に操作する添え字を問わず、数列が空の状態から再び空になる際の最悪ケースは、先ほどと同じでA_{i}が全て異なるパターンだからです)。
ということで、操作回数をNまで減らすことができたので、あとは現状のiから愚直にシミュレーションをしていけばよいです。
これは、stackを使うと、最大で2N回程度のプッシュ、ポップの操作になるので十分間に合います。すでにスタック内に存在するかどうかは、setを利用しました(こっちの方が計算量的には重くなると思います)。
計算量的には、おそらくO(N log N)です(自分は最初のA_{N}ごとにまとめる操作を行う際にc++のmapを使用したため)。

感想

ループをするのでその部分をどんどん減らす、という発想が割とはやいタイミングでできたので良かった…です。