-
Notifications
You must be signed in to change notification settings - Fork 0
ABC lessons learned2
ABC 6,8問体制(126..最新)のD問題をだいたい解いたので、そこから得た教訓をまとめていきます。
コードはこちら
マスを頂点をみなして、あるマスから他のマスへの最短距離を求める。126-Dと同様にBFSで求まる。木ではないので、既知の最短距離ではないマスは移動しない(移動すると無限ループになる)。
コードはこちら
Nが200000と読んで、いろいろ探索するより数え上げた方が早く実装できる、と勘づく。
先頭と末尾の組は9x9=81通りしかないので、
コードはこちら
上手く状態遷移をまとめれば、状態数が指数になるのを防げる。
モンスターをどういう順番で攻撃しても、体力が0になるまでの体力の減り方は同じ、ということが重要である。よって体力の減り方の経路は一通りだけ(2で繰り返し割る)で、ある体力のモンスターが何体あるかだけ数えれば攻撃回数になる。
コードはこちら
Greedyだと思ったらDPだった。
ということでDPで解く。このとき、
コードはこちら
期待値の加法性を使う。
隣接するK個のサイコロとあるので、サイコロK個の期待値は、サイコロを1個加えてK+1個になったら、最後に加えたサイコロの期待値を減らす。期待値の加法性からこれが成り立つ。移動平均の求め方と一緒である。
コードはこちら
困ったら二分探索。
二数を書けた結果が、正、ゼロ、負の三通りに分けて、
コードはこちら
二項定理に対する基本的な知識が問われる。それとダブリングの使いどころである。
Nから0..N通り選ぶ組み合わせの総和は
nが
ここまできたら
コードはこちら
Union-find木を知っていると解ける、知らないと解けないか実装が大変な問題である。
友達の友達は友達という推移関係は、友達関係をグラフで表現した時の連結成分である。なのでそれぞれの連結成分の頂点数から、既に友達である関係の数と、 同じ連結成分で ブロックしている関係の数を引く。異なる連結成分でブロックしている関係は、友達の友達は友達という推移関係は成り立たないので引かない。
コードはこちら
コレクションを反転するのはコストが高いが、反転したとみなすのフラグ1個書き換えるだけである。手元の列に対して、
- 手元の列が反転していないときに先頭に追加するのは、列の先頭に追加する
- 手元の列が反転していないときに末尾に追加するのは、列の末尾に追加する
- 手元の列が反転しているときに先頭に追加するのは、列の末尾に追加する
- 手元の列が反転しているときに末尾に追加するのは、列の先頭に追加する
反転しているか否か、先頭と末尾のどちらに追加するか if文で4通りに分けてもいいし、xorを取って2通りにまとめてもよい。
コードはこちら
全部の組み合わせの個数を挙げてから、ある種の組み合わせの個数を引く。
整数
後はボールが一個 (
コードはこちら
入力サイズから計算量を見切ることが大事である。
頂点の位置を、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、それ以外の
コードはこちら
これも 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で求める。方法は既出の通り。
コードはこちら
素因数分解して、素数pで1回、2回、3回... 割ってみる。素因数とその数をコードスニペットを書いておく。
コードはこちら
任意の組み合わせを数え上げるのに、ソートしても一般性を失わないだろうか。
小さな数でそれより大きな数を割ることはできない。よって昇順にソートして、自分より大きな数で割り切れるかどうか確かめる。そのまま実装すると
なお同じ値の数が複数ある場合は互いに割り切れるので、答えの回数には含めない。
コードはこちら
要素が何個あるかは気にしない。
なので
コードはこちら
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しか減らないので、石の色を変える意味はない。
コードはこちら。
グループ分けといえばunion-find木である。
友達の友達は友達という推移関係は連結成分に分解できる。これをバラバラにするには、連結成分の数だけグループを作って、疎な連結成分の人をそれぞれのグループに集めればいい。よって最小のグループ数は最大の連結成分の要素数である。これは気が付かなかった。
当時は自前で実装したUnion-find木を使っていた。今はACLを使っている。
コードはこちら
用語の定義を落ち着いて理解すれば分かる。
Pairwise coprime なら必ず setwise coprime だが逆は成り立たないので、 pairwise coprime ではない例を一つでもみつけたら setwise coprime と出力し、そうでなければ pairwise coprime である。これは
コードはこちら
DFS(幅優先探索)はスタックの深さに注意すればきれいに再帰で書ける。
数列は順序が異なれば違うものと数えるので、3以上の数を
コードはこちら
45度回転。
しかし式変形をしなくても直観的に理解する方法がある。xが左、yが上に増える座標系で、x+y等高線は左下-右上関係をとらえ、x-y等高線は右下-左上関係を捉えると思えば、二点のマンハッタン距離は、二点における等高線の差の大きい方と一致する。
コードはこちら
厳密解を求めるより、鳩の巣原理で解く。
余りは
- 周期に入る前
- 周期を繰り返し
- 周期の途中で
$n$ に達する
について和を求める。ただし周期に入る前に
コードはこちら
ジムに通うと強さがどう変わるかは今の強さと今通うジムだけで決まり、これまでどう鍛えたかには関係ない。これをマルコフ性という。
なので両方のジムに通ったとして強さが低くなる方を選べばよい。弱いうちは強さをA倍にし、その後はB増やすとよさそうだが、そこまで考察しなくても解ける。
コードはこちら
算数的な方法で解ける。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の更新式は条件付き確率そのもの(A,B,C枚あるときにA,B,Cを一枚引く確率はA:B:C)である。私の解法は出発点(A,B,C枚)から配るDP、公式解説は終着点(いずれかの硬貨が100枚)からメモ化再帰で実装している。
コードはこちら
問題文を制約に落とし込む。
赤いハンコが青いハンコに重なってはいけないが、赤いハンコを押す場所同士が重なってもいい。ということは以下の制約になる。
- 連続する白のマスの最小値が、赤いハンコの長さの最大値
$l$ になる - 連続する白の
$n$ マスに押す回数は、$\lfloor l/n \rfloor$ である
コードはこちら
セグメント木を使う。
加法 +
と同様に 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日あたりプライム料金を最大として支払えばよい。
コードはこちら
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から順に調べて組み合わせを加算していく。
コードはこちら
等差数列の公式を因数分解する。
等差数列の公式 std::set
で集計する。
コードはこちら
辺ではなく頂点に注目する。
辺に注目すると、凹の部分で上手く値が求まらなくなる。よって頂点が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
で箱を管理して荷物の入った箱を取り除けばいい。
コードはこちら
コードはこちら
ベクトルを回転させれば求まる。
コードはこちら
なので木を頂点1からDFSして、途中にある色を数えればいい。木なので葉に向かうときに経由した頂点の色を加え、根に向かうときに頂点の色を除く。
コードはこちら
鳩の巣原理。
鳩の巣原理であっさり解ける。こういう発想も重要である。定石通りDFSと再帰で求めてもいいが、パスを保存しながらエッジケースを実装するのが難しい。コードはこちら。