-
Notifications
You must be signed in to change notification settings - Fork 0
ABC lessons learned2
ABC 6,8問体制(126..最新)のD問題をだいたい解いたので、そこから得た教訓をまとめていきます。
コードはこちら
マスを頂点をみなして、あるマスから他のマスへの最短距離を求める。126-Dと同様にBFSで求まる。木ではないので、既知の最短距離ではないマスは移動しない(移動すると無限ループになる)。
コードはこちら
組み合わせ総数と累積和を上手く貯える。
- 両端を選んで残り
$k-2$ 個選ぶ - 先頭(1番目)と最後の前(n-1番目)を選んで最後(n番目)は選ばない。他を
$k-2$ 個選ぶ。 - 先頭(1番目)と最後の前の前(n-2番目)を選んでそれ以後は選ばない。他を
$k-2$ 個選ぶ。
... という方法を考える。要するに最初と最後の要素の添え字の差(幅-1)が
最初と最後の要素の添え字の差が
よって添え字の幅が
コードはこちら
Nが200000と読んで、いろいろ探索するより数え上げた方が早く実装できる、と勘づく。
先頭と末尾の組は9x9=81通りしかないので、
コードはこちら
手抜きも時に重要。
std::lcm()
を使うとオーバーフローするので、
ここで
コードはこちら
上手く状態遷移をまとめれば、状態数が指数になるのを防げる。
モンスターをどういう順番で攻撃しても、体力が0になるまでの体力の減り方は同じ、ということが重要である。よって体力の減り方の経路は一通りだけ(2で繰り返し割る)で、ある体力のモンスターが何体あるかだけ数えれば攻撃回数になる。
コードはこちら
Greedyだと思ったらDPだった。
ということでDPで解く。このとき、
コードはこちら
遅延評価いもす法
解き方は複数あるが、遅延評価いもす法が分かりやすい。モンスターの座標
コードはこちら
期待値の加法性を使う。
隣接するK個のサイコロとあるので、サイコロK個の期待値は、サイコロを1個加えてK+1個になったら、最後に加えたサイコロの期待値を減らす。期待値の加法性からこれが成り立つ。移動平均の求め方と一緒である。
コードはこちら
桁を上から見るか、下から見るか。
下の桁から見るのが正解である。もしかしたら上の桁から見ても解けるのかもしれないが、私には解けなかった。公式解説を見ると再帰で解けると解るが、もう少し読み解くと以下のようになる。数字列
-
$k=0$ なら$a=b=c=d=0$ の1通り -
$k<4$ つまり$k$ が$abcd$ より短すぎるなら0通り
を特別扱いする。次に下の桁
-
$d=0$ かつ$inCarry=1$ なら、下の桁$...$ への繰り下がりがあり、かつ上の桁$c$ からの繰り下がりが必ず起きる($outCarry=1$ )。それ以外で、上の桁からの繰り下がりが起きるかどうかは状況次第である($outCarry=0$ )。 -
$inCarry=1$ なら$d$ を1引く。つまり$d:=(d+9) mod 10$ にする。
ここで
-
$d$ があるこの桁は0ということにして、$k$ を消費しない。この組み合わせの数は、末尾を削除した数字の並び、繰り下がり、非0の数字の数がそれぞれ$(abc, outCarry, k)$ の場合について求めたものである(なので再帰的定義である)。 -
$d>0$ ならこの桁は非0の値$1..d$ のいずれかにして、$k$ を1消費する。この組み合わせの数は、同様に$(abc, outCarry, k-1)$ の場合について求めたものを$d$ 倍した値である。 -
$d<9$ ならこの桁は非0の値$d+1..9$ のいずれかにして、$k$ を1消費する。この組み合わせの数は、同様に$(abc, 1, k-1)$ の場合について求めたものを$9-d$ 倍した値である。上の桁から繰り下がりがないと、$d$ より大きな数字をこの桁に設定できない。
再帰呼び出しで引数で何度も呼び出すことに備えてメモ化するとよい。再帰とメモ化するためのキーは、
コードはこちら
困ったら二分探索。
二数を書けた結果が、正、ゼロ、負の三通りに分けて、
コードはこちら
154-Eと似ている。
基本的には154-Eと同様なのだが、入力が大きいのでDFSは使えず、BFSするとTLEした。なので単純にメモ化しながらループする。メモ化するのは、(下から何桁目までに、下の桁から繰上りがあったかどうか) のとき使う紙幣の最小枚数
154-Eの繰り下がりと同様に、ある桁
-
$d:=d+inCarry$ について -
$d<10$ ならその場で$d$ 枚紙幣を払い -
$d>0$ なら上の桁から借りてお釣りをもらうことにして$10-d$ 枚紙幣を払う
最上位桁が繰り上がったときを考慮する必要がある(そうしないと91の答えが3でなく2になってしまう)。このためには、
コードはこちら
二項定理に対する基本的な知識が問われる。それとダブリングの使いどころである。
Nから0..N通り選ぶ組み合わせの総和は
nが
ここまできたら
コードはこちら
k個のお菓子をn人に分ける。
自力では解けず、こちらの解説を用いた。この解説通り実装すればよい。
解説中、
コードはこちら
Union-find木を知っていると解ける、知らないと解けないか実装が大変な問題である。
友達の友達は友達という推移関係は、友達関係をグラフで表現した時の連結成分である。なのでそれぞれの連結成分の頂点数から、既に友達である関係の数と、 同じ連結成分で ブロックしている関係の数を引く。異なる連結成分でブロックしている関係は、友達の友達は友達という推移関係は成り立たないので引かない。
コードはこちら
力業すぎる。
type 2クエリからセグメント木を連想する。文字は英小文字しかないのだから、26本のセグメント木を作り、ある区間にa,b, ..., z があるかどうか更新して調べればよい。type 1 クエリで変更前の文字が何であったかは、
コードはこちら
コレクションを反転するのはコストが高いが、反転したとみなすのフラグ1個書き換えるだけである。手元の列に対して、
- 手元の列が反転していないときに先頭に追加するのは、列の先頭に追加する
- 手元の列が反転していないときに末尾に追加するのは、列の末尾に追加する
- 手元の列が反転しているときに先頭に追加するのは、列の末尾に追加する
- 手元の列が反転しているときに末尾に追加するのは、列の先頭に追加する
反転しているか否か、先頭と末尾のどちらに追加するか if文で4通りに分けてもいいし、xorを取って2通りにまとめてもよい。
コードはこちら
全部の組み合わせの個数を挙げてから、ある種の組み合わせの個数を引く。
整数
後はボールが一個 (
コードはこちら
制約をよく見る。
例外処理として、
- 切れ端に含まれる 1が
$K$ 個を超えたら、$i$ 列の右側ではなく左側で切る。$i$ 列を起点に調べなおす。 - そうでなければ切らずに、
$i+1$ 列の右側で切れるかどうか調べる。
横方向+縦方向に切った回数の最小値が答えである。
コードはこちら
入力サイズから計算量を見切ることが大事である。
頂点の位置を、Xより右、X-Y間、Yより左に場合分けして解くととてもめんどくさい。BFSで距離を求めると簡単である。こういうときは
辺の長さが固定のときにBFSで距離を求める方法は既出なので省略する。
コードはこちら
std::vector::resize()
を使う
問題文から X<A, Y<B なので場合分けを減らせる。無色のリンゴを補充しなければならない状況はなく、置き換えるだけでよいことが分かる。リンゴを色別に、美味しさの降順にソートして、赤いリンゴは元々 std::vector::resize()
は余分な要素を切り捨てるので便利である。
置き換えるなら最もおいしくない赤と緑のリンゴを、最も美味しいリンゴに置き換えればよいだろう。これは尺取り法でできる。赤と緑のリンゴをすべて置き換え終わったらもうできることはない(もっと美味しい無色のリンゴに置き換えたはずだから)ので、尺取り法のインデックスに気を付ける。
コードはこちら
これも 160-Dと同様に、入力サイズから計算量を見切ることが大事である。
再帰とメモ化で取りうる組み合わせを累計するとめんどくさそうである。しかし10進数5桁しかないのだから、上の桁に下の桁(0には0と1、9には8と9、それ以外の
コードはこちら
ゴールから見た方が簡単である。
まず
念のため std::set
で重複を除いている。
コードはこちら
これも 160-D, 161-Dと同様に、入力サイズから計算量を見切ることが大事である。配列の添え字がはみ出さないように全探索すればよい。
コードはこちら
巨大な数
それさえわかれば、
- 和の最小値は何も選ばない0、最大値は全部選ぶ
$sum(i=0..{N})$ である。 -
$s = sum(i=0..{K-1})$ が最小値なので、それより小さい数$S$ 個は和になりえない。 - 足す数をそれぞれ
$N-i$ とすれば左右反転して、$sum(i=0..{N})$ までの$S$ 個は和になりえない。 - それ以外の和は表現可能である(和にする数の集合に対して、ある数とそれより1多い数を交換すればよい。このような数は必ずみつかる)
コードはこちら
- 鉄則本の「8.6 文字列のハッシュ」問題 A56を知っていると解ける
- 累積和から部分列の総和を取る方法が使える
文字列から部分文字列を取っていくときに、部分文字列から得られるハッシュのような情報を得る。ある数が2019で割れるかどうかは、2019のすべての素因数(3と673)で割れるかどうかと同じである。つまり3で割った余りと673で割った余りを、文字列の末尾から先頭に向かって取っていく。説明の都合上、文字列を std::reverse
で逆順にして最も下の桁を文字列の先頭とする。
...abcdef と桁がたくさんある整数のうち、下位1,2,3...桁を673で割った余り(mod 673)について考える。3で割った余りも同様である。
- f mod 673は、実際に割れば分かる
- ef mod 673は、(e*10 + (f mod 673)) mod 673
- def mod 673で割った余りは、(d*100 + (ef mod 673)) mod 673
- cdef mod 673で割った余りは、(c*1000 + (def mod 673)) mod 673
こうして最上位の桁まで、最も下の桁から連続する桁を673で割った余りを計算できる。10, 100, ... と
次に下の桁から順に0に置き換えていく。
- 最初は何も置き換えない。このときf, ef, def, ... を673で割り切れるかどうかは既に調べてある
- 最下位の桁を0に置き換える。例えばcdefをcde0に置き換える。cde0が673で割り切れるかどうかは、
$(cdef - f) : mod : 637 \equiv 0$ つまり$f$ を673で割った余りと$cdef$ を673で割った余りが等しいということである。ここで f mod 7 を固定して、f mod 7 と等しい f, ef, def, ... を数えれば、題意の総数の一部が求まる。 - 一般的には、下の桁から連続する
$i (\geq 0)$ 個の0を置き換える。下i桁 mod 7 と等しい f, ef, def, ... を数えれば、題意の総数が求まる。下i桁の値は初期値を0とし、$i$ を更新するごとに$10^{i}$ * i桁目の数字 mod 673 を足す。 - f, ef, def, ... mod 673 と f, ef, def, ... mod 3 から、 f, ef, def, ... mod 2019 が求まる。これは
$i$ と関係ないので、mod 2019をキーとして該当する余りの出現回数を一度数えて連想配列に保存しておく。
この考え方は、部分列の総和を求めるのに累積和を使う方法とよく似ている。和を余りに置き換えただけなので。
Mod 3とmod 673はACLを使うと書きやすい。
#include <atcoder/all>
using ModInt3 = atcoder::static_modint<3>;
using ModInt673 = atcoder::static_modint<673>;
コードはこちら
コードはこちら
直観的には、Aを増やすほどBで割った余りが溜まっていって、Bになると余りが0に戻る、ということである。
コードはこちら
三次以上の多項式の問題は、n条根を取ると変数の取る領域がそれほど大きくないので総当たりできる。
約数を求めるは頻出なのでコードスニペットを書いておく。平方数の約数を二度数えてしまいがちである。
コードはこちら
絶対値は順序を固定すれば外せる。
組み合わせを列挙するので、一般性を失わず
題意から
実際できるのである。
コードはこちら
動的計画法できなさそう、という見切りが必要。
同じ色が並んでいるブロックが残り
これをすべての
コードはこちら
部屋1から他の部屋の距離をBFSで求める。方法は既出の通り。
コードはこちら
2時間近くかかったが、久しぶりに青diffを解けた。
自明な答えとして、イワシは一匹なら答えは1である。
原点なイワシ std::map<std::pair<Num, Num>, Num>
に収める。ここで0種類増やす、つまり連想配列のエントリだけ作っておくと後の処理が楽である。
- 既約なベクトル
$(A_i, B_i)$ を1種類増やす - 対称なベクトル
$(-A_i, -B_i)$ を0種類増やす - 直交するベクトル
$(-B_i, A_i)$ $(B_i, -A_i)$ を0種類増やす
次に登場したベクトル std::vector<std::pair<Num, Num>>
に追加する。ここで std::set<std::pair<Num, Num>>
で管理する。
ベクトル
ベクトル
ここで重要なこととして、原点なイワシ
1260 ms掛かっているのを見ると、ベクトルの種類を数え上げるにはもう少し洗練された方法がありそうだ。
コードはこちら
素因数分解して、素数pで1回、2回、3回... 割ってみる。素因数とその数をコードスニペットを書いておく。
コードはこちら
二分探索ではなかった。
小数が出ると厄介なので、入力値を二倍して答える前に半分にする。
コードはこちら
任意の組み合わせを数え上げるのに、ソートしても一般性を失わないだろうか。
小さな数でそれより大きな数を割ることはできない。よって昇順にソートして、自分より大きな数で割り切れるかどうか確かめる。そのまま実装すると
なお同じ値の数が複数ある場合は互いに割り切れるので、答えの回数には含めない。
コードはこちら
力業すぎる。
C++の速度でゴリ押すなら、幼稚園をキー、園児のレートを値とする std::map<Num, std::multiset<Num>>
を作ればよい。min(全幼稚園のmaxレート)は、全幼稚園を載せたセグメント木で管理する。関数を std::max
, 元(モノイド)を inf にしておけば、2秒を切ってACできる。1-based indexで通すなら入力を読み込むときもそうする。
コードはこちら
要素が何個あるかは気にしない。
なので
コードはこちら
E問題が10分以下で解けることもある。
めったにないE問題茶diffである。XORパリティと言えばわかる。自分以外の数字のXORを取るということは、数字の各桁について、自分以外の数字で1が出た回数が奇数回なら1、偶数回なら0を意味する。
ということで数字の各桁について、
コードはこちら
Aをから何冊読むか決めたら、Bから最大何冊読むかは一意に決まるので、尺取り法で求まる。
コードはこちら
縦の物を横にする。
ある数に約数が何個あるかを調べるのは大変である。しかし
コードはこちら
あれこれ悩むより、手を動かして試した方が早い。
最後の一人は誰かにとって心地よさは与えないので、フレンドリーさが最大の人から降順到着するのがよいと分かる。どこに入れるかであるが、Nが十分大きいとき、フレンドリーさが上位二名の間に入れればそこの心地よさが最小になる。つまり N, N-1, N-1, N-2, N-2, ... となる。
コードはこちら
コードはこちら
選択肢が二つあるが、片方があればもう片方は要らないということがある。
RWの並びはよいがWRの並びはよくないので、WRを減らせばいい。ならばRだけ左にずらっと並べて、Wだけ右にずらっと並ぶよう、Wを右に寄せればいいと分かる。つまり最左のWと最右のR入れ替えればよく、この回数を求める。これは尺取り法でできるので O(N)で解ける。
石を入れ替えれば災いは1または2減るするかもしれないが、石の色を変えても1しか減らないので、石の色を変える意味はない。
コードはこちら
二分探索である。
丸太の最大長を決め打ち、
なのだが、探索幅の下限を total > k
ではない最小の値を返す。二分探索はこれが常にややこしい。
Mo's Algorithm で解く。Mo's Algorithmを知らないとどうにもならない。
コードはこちら
解の方針は立つが、実装が非常に辛い。
まず状態遷移をループ成分に分解する。DFSでもよさそうだが、union-find木の代表元を使うと簡単である。それぞれのループ成分ごとにスコアを求めて、その最大値が答えである。
問題はスコアの求め方である。一周するとスコアが正の値になるなら、できるだけたくさん周回する。そうでそうでなければ一周しなければよい。一周に満たない場合のスコアは起点を終点の組を総当たりして
と書くと簡単に見えるが、WAしないように実装するのがものすごく大変である。二周分のスコアの累積和を求めて、すべての起点と長さを総当たりすればいいはずなのだが、それだけではどうしてもWAを取り切れない。
コードはこちら
三次元DP
三次元の動的計画法を作る。行、列、ある行でのアイテム取得回数 0..3 を上から下、左から右にスキャンする。0行0列は価値0のアイテムがあるとみなす、つまり入力アイテムの位置を1-base indexingすると、スタート地点のアイテムを拾うかどうかを例外処理しなくて済む。
左から右にスキャンしてアイテムを拾うとアイテムの数が1増えるが、行を下に移ったときに拾ったアイテムの数が0に戻ることに注意する。
コードはこちら
普通にBFSで解ける。
と、公式解説に書いてある。おそらくその方が実装が簡単なのだろう。私の解き方は少し回りくどいが以下の通りである。
- すべてのマスを、ワープなしで行ける範囲で連結成分分解する。Union-find木を使うとできる。
- 連結成分を頂点、ワープで行ける連結成分を辺とするグラフを構成する。頂点番号は連結成分の代表元(
$x+y*1000$ でいい)、ワープで行けるかどうかは各頂点から25通り試せばよい。同一連結成分同士を辺でつながないように注意する。 - このグラフについて、出発点の連結成分から目的地の連結成分までに経由する、最短距離つまり最小の辺の数をBFSで求める。到達不可能なら距離は無限大である。
多重辺を作らないようグラフは std::map<Num, std::set<Num>>
にするが、頂点から延びる辺は edges[current]
で取り出す。 edges.at(current)
だと、辺がないときに例外が飛んでプロセスが終了してしまう。
コードはこちら
縦横を独立に考える。
破壊対象が最も多い行と、破壊対象が最も多い列はそれぞれ独立に考えることができる。よって破壊対象が最多の
実は全探索しても大丈夫である。もし交点に破壊対象がないのであれば答えは
コードはこちら。
グループ分けといえばunion-find木である。
友達の友達は友達という推移関係は連結成分に分解できる。これをバラバラにするには、連結成分の数だけグループを作って、疎な連結成分の人をそれぞれのグループに集めればいい。よって最小のグループ数は最大の連結成分の要素数である。これは気が付かなかった。
当時は自前で実装したUnion-find木を使っていた。今はACLを使っている。
コードはこちら
用語の定義を落ち着いて理解すれば分かる。
Pairwise coprime なら必ず setwise coprime だが逆は成り立たないので、 pairwise coprime ではない例を一つでもみつけたら setwise coprime と出力し、そうでなければ pairwise coprime である。これは
コードはこちら
DFS(幅優先探索)はスタックの深さに注意すればきれいに再帰で書ける。
数列は順序が異なれば違うものと数えるので、3以上の数を
コードはこちら
45度回転。
しかし式変形をしなくても直観的に理解する方法がある。xが左、yが上に増える座標系で、x+y等高線は左下-右上関係をとらえ、x-y等高線は右下-左上関係を捉えると思えば、二点のマンハッタン距離は、二点における等高線の差の大きい方と一致する。
コードはこちら
貰うDPで解く。
配るDPでも解けるようだが、貰うDPで解いた。まず区間を
- 区間の終わり
$i - R < 1$ ならおわり - そうでなければ区間
$\lbrack max(1, i-L), i-R \rbrack$ のマスに行く方法を全部足す
区間にある貰うマスに行く方法を累積和で管理すれば、区間は10以下なので計算量は
なおこの問題は遅延評価セグメント木でも解ける。実装は こちら 。ただしこの問題では
コードはこちら
厳密解を求めるより、鳩の巣原理で解く。
余りは
- 周期に入る前
- 周期を繰り返し
- 周期の途中で
$n$ に達する
について和を求める。ただし周期に入る前に
コードはこちら
ジムに通うと強さがどう変わるかは今の強さと今通うジムだけで決まり、これまでどう鍛えたかには関係ない。これをマルコフ性という。
なので両方のジムに通ったとして強さが低くなる方を選べばよい。弱いうちは強さをA倍にし、その後はB増やすとよさそうだが、そこまで考察しなくても解ける。
コードはこちら
三角不等式
三角不等式を考えない一般的な巡回路については、 ある都市を訪れたかどうかが
しかし三角不等式が成り立つときは同じ都市を二度訪れる必要はない。入力例2が引っかけである。このときは訪れた都市の数が1,2,...,Nになるように集合を拡張すればいい。つまり(訪れた都市の集合, 今いる都市)をキー、値を最小コストとするDPを行い、すべての都市を訪れて今都市1にいるときの値を答えにする。
コードはこちら
算数的な方法で解ける。Nが大きいので、順列を数え上げるとTLEする。
算数的な方法は、数字をいい感じに組み合わせると、下から3桁目が偶数か奇数かと、下から1,2桁目の組を使って8の倍数になるかどうか調べる。要は000, 008, ..., 192 かどうか調べる。間違い無いがコード量が大きい。
実は下三桁を求める方が簡単である。こちらの記事を参照。
コードはこちら
数学的考察が必要。
解を満たす組は、小さい値から順に二つずつ取る、である。最も大きい値と最も小さい値を組む、ではない。この前提が間違っているとどうにもならない。これが分かれば累積和をつかって計算量を減らせる。
コードはこちら
問題をよく読む必要がある。問われているのは最大瞬間座標であって、
累積和を累積するのは頭がこんがらがるが、コードに落とし込めばそのままである。
- 出発点(右側にしか行かない場合)
-
$A_i$ だけ進まずに行ける場所(これまでの最大瞬間座標) -
$A_i$ 進むことで初めて行ける最大瞬間座標
この後、位置に累積和と
コードはこちら
帰納法。縦横を独立な問題として考える。
縦方向に光が届くかと、横方向に光が届くは独立なので別々に考える。公式解説にある通り、すでに光が当たっているマスはその隣もあっていることを利用して計算量を減らせる。
コードはこちら
いもす法(Imos method)を使う。
NとSとTが大きいので、すべての整数時刻についてお湯の量を求めるとTLEする。だが変化点だけその累積和を求めることで、計算量を
- S, T, P を得て、入りの変化点Sでお湯の量を +P, 出の変化点Tでお湯の量を -P するイベントを作る
- イベントを時刻の昇順に並べ替える
- 同一時刻のイベントについてお湯の量をまとめる
- ある時刻のお湯の量が容量Wを超えてたら供給不能、そうでなければ供給可能である
コードはこちら
引くDPは累積和にもできる。
配るDPを素朴に実装すると
コードはこちら
std::move
を使う。
生徒の集団をunion-find木で管理し、集団に属する生徒についてクラスをキー、生徒数を値とする連想配列 std::map<Num, Num>
で管理する。生徒aを含む集団と、生徒bを含む集団が新たに合流するときに、union-find木で管理する生徒の集合と、クラス-生徒数の連想配列もマージする。
このときこの連想配列を、union-find木の代表元(root, leader)に持たせ、代表元以外には持たせないようにする。そうすれば誰が連想配列を持っているか迷わなくて済む。union-find木をマージしたときに代表元が変わったら、以前の代表元が持っていた連想配列を新しい代表元に std::move
する。
コードはこちら
まず原点に移動する。
原点に移動すれば式はすっきりする。公式解説通りの場合分け通りに実装したはずなのに、ついにACできなかった。
コードはこちら
確率DPと呼ばれる。
期待値の加法性から、分岐点のその先に行く確率を逐次的に求める。DPの更新式は条件付き確率そのもの(A,B,C枚あるときにA,B,Cを一枚引く確率はA:B:C)である。私の解法は出発点(A,B,C枚)から配るDP、公式解説は終着点(いずれかの硬貨が100枚)からメモ化再帰で実装している。
コードはこちら
TLE対策が必要
01-BFSで最短距離を求める、というのはすぐわかるのだが、ワープゾーンがあるマス同士を枝で結ぶとTLEする。ワープゾーンa..zという頂点を一つずつ、計26個作ればいいことは思いつく。しかし実際に実装するのが大変である。解説通り、普通のマスからワープゾーンに距離1、ワープゾーンから普通のマスは距離0にすると上手くいく。ワープゾーンへの距離を持たせて、枝を刈らないとTLEする。
BFSのループ内に上下左右の頂点、障害物と枠外には進めない、ワープゾーンの出入りと直接構成する必要がある。とにかく、ワープゾーンの出入りを枝で表現すると、TLE, RE, MLEとあらゆることが起きうる。
コードはこちら
半分全探索
半分全探索を完全に忘れていた。これは JOI 本選 2008Cとしてあまりにも有名である。
コードはこちら
問題文を制約に落とし込む。
赤いハンコが青いハンコに重なってはいけないが、赤いハンコを押す場所同士が重なってもいい。ということは以下の制約になる。
- 連続する白のマスの最小値が、赤いハンコの長さの最大値
$l$ になる - 連続する白の
$n$ マスに押す回数は、$\lfloor l/n \rfloor$ である
コードはこちら
編集距離
題意は編集距離、またの名をレーベンシュタイン距離の定義そのものである。
- 削除はそのまま
- 要素を挿入するのは、他方の列から要素を削除するのと同じこと
- 編集後の要素の違いは、要素を置換するコストと同じ
レーベンシュタイン距離の求め方はこちら が参考になる。初期化と更新式を併せて10行程度で書ける。
コードはこちら
セグメント木を使う。
加法 +
と同様に XOR もセグメント木に載せられる。自分で実装したセグメント木を使ったが、ACLを使ってもよい。
コードはこちら
絶対値があると上手く値をまとめられないので、絶対値を外す。
-
$i < j$ かつ$A_i < A_j$ ならソートしても$i < j$ なので、$i,j$ の相対的な位置は同じ。 -
$i < j$ かつ$A_i \geq A_j$ なら位置が入れ替わるのでソート前の$|A_j - A_i|$ を足すことに等しいが、絶対値なので値は変わらない
計算量を減らすために累積和を取りたい。要素の差だけに意味があるので、最小値を0にしても同じである。よって
二重
コードはこちら
拡張ユークリッドの互除法
より具体的には以下の通りである。
拡張ユークリッドの互除法を用いて、
-1
を出力する。そうでなければ、
こちらの 記事 の通り、中国余剰定理で 解く こともできる。
コードはこちら
多数決は、得票数ではなく、得票数の差で考える。
得票差は全く演説しないときが最小(最も不利)である。得票数の差を増やす方法を考える。演説すると
コードはこちら
実装が大変
おおよその解法はすぐ思いついたが、110分掛かった。諦めない根性も必要である。まずデータ構造を示そう。
- 辺
$1..N-1$ の始終点std::vector<std::pair<Num, Num>> abset
- 木をDFSして根から葉、左から右に頂点番号を振りなおしたID
std::vector<Num> ids
- 頂点より根方向の部分木のうち、頂点番号を振りなおしたIDの最大値(葉なら0)
std::vector<Num> max_id
- ID
$[L,R]$ に$x$ を加算するという区間の集合std::vector<std::tuple<Num, Num, Num>> spans
- 上記区間の集合から求めた、IDに対応する整数
std::vector<Num> score
解き方の概要を示す。
- 木をDFSして根から葉、左から右に頂点番号を振りなおす。
- クエリは
abset
を参照する。クエリ2は頂点a,bを逆に読み替えるだけなのでstd::swap
すればよい - 頂点from に対応するID
$id_{from}$ 、頂点to に対応するID$id_{to}$ があったとする。toがfromより根に近いつまり浅いなら、$id_{from}$ >$id_{to}$ である。そのようにIDを振ったのである。このとき頂点fromの部分木にしか到達できない。IDでいうと、$id_{from}$ 以上、部分木の最大頂点(max_id(from)
から分かる)以下である。 - fromがtoより根に近いつまり浅いなら。このとき頂点toの部分木には到達できない。よって到達可能な頂点は、
$id_{from}$ 以下、頂点toの部分木の最大頂点(max_id(to)
)より番号の大きな頂点である。 - クエリに対応して、頂点
$[L,R]$ に$x$ を加算するようspans
に積んでおく - すべてのクエリを読んだら、いもす法で区間の値
spans
を統合してscore
にする。こうすると頂点IDに対応する値が求まる。 - 最後に頂点
$1..N$ に対応するIDをキーとする値を出力する
fromがtoより根に近いつまり浅いときは、部分木に
コードはこちら
いもす法を使う。
既述の通り時刻ごとのイベントを作り、同一時刻のイベントをまとめる。後はイベントを時刻順にみて、前のイベントからの期間について、1日あたりプライム料金を最大として支払えばよい。
コードはこちら
DAGである。
ということで、in-degreeが0の頂点から順番に最安値を伝搬すればいい。トポロジカルソートを使ったが、元々入力がトポロジカルソート済なのだからもっとすっきりした線形時間の実装があった。
コードはこちら
ランレングス圧縮を実装する。その上で、ビット操作の意味を考える。
- ビット操作のANDは、出力をTrueにしたいなら、入力をTrueにせよ、と捉える
- ビット操作のORは、入力がなんであれ、出力をTrueにしたいならそうできる、と捉える
- この run で
$y_N$ をTrueにするので、それ以前の$y$ は何でもよい。組み合わせは$(2^{len-1}) * 2^{pos}$ 通り - このrunではTrueにせず、それ以前の run でTrueにする。このrunについては全Falseの一通りで、組み合わせの数は以前のrunで決まる。
ので、後ろのrunから順に調べて組み合わせを加算していく。
コードはこちら
アフィン変換である。
座標に行列を掛けるとクエリに対する座標変換ができる。これをアフィン変換という。クエリに対応する行列を見ていく。
90度時計回り
90度反時計回り
変換行列
コードはこちら
等差数列の公式を因数分解する。
等差数列の公式 std::set
で集計する。
コードはこちら
転倒数と言えばセグメント木
この問題は座標圧縮が要らないので Fenwick Tree でもよい。いずれにせよ
これで
コードはこちら
辺ではなく頂点に注目する。
辺に注目すると、凹の部分で上手く値が求まらなくなる。よって頂点が90度曲がる箇所を数える。ある点の周辺4マスについて、白黒のマスの数が1または3ならそこで辺が曲がるとわかる。
コードはこちら
浮動小数で簡単に解けると思ったら、精度に注意する。
X座標が求まればY座標は三角関数から求まるので、Y座標の上下限を引いて(両端が整数の場合に注意する)数える。のだが、浮動小数で実装すると丸め誤差が原因でWAする。問題文をよく読み返すと、
$|X|,|Y|,|R| \leq 10^5$ - X,Y,R は高々小数第4位まで与えられる
なので、1+52ビット精度の浮動小数では精度が足りない。しかし64-bitではギリギリ精度が足りる。そのため浮動小数を固定小数(整数部x10000+浮動小数部)として扱う。これで10進数18桁(9桁の二乗)を誤差なく扱うことができる。
コードはこちら
久しぶりにダイクストラ法
それぞれの出発点について、ダイクストラ法を使って最短距離を求めればいい。多少工夫がいる。
- 同じ出発点から終着点への道路については、最短経路の道路だけ残して他は捨てる
- ダイクストラ法のキューは、初期状態で出発点の隣を積む。出発点を積むのではない。
- 距離の初期値は、出発点-出発点のループがあればその距離、そうでなければ無限大にする
公式解説では、道路の向きを反転して出発点に向かう距離を求めているが、上記の方法であれば道路を反転したグラフは作らなくてよい。
コードはこちら
答えが整数
整数の二分探索は、素朴に実装すると区間が2以下のときに無限ループに陥りやすいので、パターンを確立する。
コードはこちら
ダイクストラ法である。
距離の定義を時刻にすれば、そのままダイクストラ法で解ける。BFSで最短距離を求めるときに優先度キューに追加情報を足すように、目的地と移動時間(いわゆる距離)の他に発車間隔をキューに載せればよい。
コードはこちら
条件付き確率の定義そのものである。ある数字が表に書かれたカードで出尽くした場合を考慮する。
コードはこちら
確率の加法性から明らか。
コードはこちら
Binary Indexed Tree (Fenwick Tree) を使ってみた。あるいは縦の物を横にする。
公式解説によれば
コードはこちら
実際価値が大きい荷物を、その荷物が入る最小の箱に収めればよい。そうでなくて、同じ価値のものをもっと大きな箱に入れるのも、同じ大きさに価値が低い荷物に入れるのも意味がない(交換すればいい)。クエリ間に依存関係はなく、 std::multiset
で箱を管理して荷物の入った箱を取り除けばいい。
コードはこちら
C++の力でごり押してしまった。
半畳の間に境界線を引く/引かない、つまり半畳をまとめる/まとめないをビット全探索すると、C++実装なら2秒を切るのでACできる。境界線の内側の領域(半畳がつながったものを)をunion-find木で管理する。以下の通り全探索する。
- 最初は各半畳が孤立した領域とする
- 境界線を引くなら領域をmergeする
- 領域の大きさが2(一畳)を超えたらその場で打ち切る
- 打ち切らずに一通り境界線を引く/引かないを終えたら、一畳がA枚、半畳がB枚あるかどうか数える
解説通りに畳の配置でDFSすると9ミリ秒で解ける。私のコードはこちら
コードはこちら
コードはこちら
ベクトルを回転させれば求まる。
コードはこちら
えいやっとDP
同じ色のボールを回収するなら、右端のボールまで行って左端のボールまで回収するか、左端のボールまで行って右端のボールまで回収するか、どちらかにすればいい。左端か右端以外の場所で止まる意味は無い(次の色になってから動けば済むことなので)。
ここで今の色の左端と右端、次の色の左端と右端を組み合わせてDFSするとTLEする。そこでDPできないかと考える。つまり
コードはこちら
問題文をよく読む。
文字が同じなら入る数字も同じ、というのは分かる。しかし数字が同じなら文字も同じ、ということは問題文をよく読むと解る。後者の制約のおかげで随分簡単な問題になる。
まず文字が11種類以上あるときは解けない。文字が10種類以下なら総当たりでよい。文字が
std::next_permutation
すれば
コードはこちら
なので木を頂点1からDFSして、途中にある色を数えればいい。木なので葉に向かうときに経由した頂点の色を加え、根に向かうときに頂点の色を除く。
コードはこちら
解説を読んでも解けないのが青diff
連結成分に分解してそれぞれ解く、DFSでたどるのは分かったが、入力例1を解くことができずに諦めた。単に頂点の色を決め打ちして進むと、入力例1の右回りと左回りで同じ塗分けを重複して数えてしまう。DFSで頂点をたどる方法を固定すれば、重複せずに数えられる。
DFSで頂点をたどる順序を求めて頂点を追加すると、根が最初、深い場所が後になる。このとき追加順つまり根を最初に、深い場所を最後たどる必要がある。そうしないとTLEする。うっかり逆にしたためにTLEがなかなか取れなくて苦労したが、この難易度が青diffである。
コードはこちら
鳩の巣原理。
鳩の巣原理であっさり解ける。こういう発想も重要である。定石通りDFSと再帰で求めてもいいが、パスを保存しながらエッジケースを実装するのが難しい。コードはこちら。