-
Notifications
You must be signed in to change notification settings - Fork 0
ABC lessons learned3
ABC 6,8問体制(126..最新)のD問題をだいたい解いたので、そこから得た教訓をまとめていきます。
コードはこちら
ゲーム理論とミニマックス法である。
2人の得点差だけに意味がある。得点差が正なら先手の勝ち、得点差が負なら後手の勝ち、得点差が0なら引き分けと定義する。マスに移動したときの得点を、
- 先手なら素直にマスの得点(+1なら+1, -1なら-1)
- 後手なら先手が如何に損するか、と読み替えて正負を入れ替える(+1なら-1, -1なら+1)
として、先手は得点差を最大化、後手は得点差を最小化するように移動すればよい。
盤面の外にはみ出さないようにするのだが、マスから一手移動する先をキューで管理するとTLEするので、ゴールまで(またはスタートから)距離が
コードはこちら
上の桁から順番に決めていく。
- 未決定の桁のうち、最上位桁をaをして残りの組み合わせがk以下なら、最上位桁をaをして残りの桁を決める
- そうでなければ最上位桁をbにして、kから残りの桁が取りうる組み合わせを引いてあら、残りの桁を決める
- aとbのどちらか一方が残っていなければ、残りはbだけまたはaだけに決まる
aとbの組は
コードはこちら
答えが整数
中央値を求めるのではなく、中央値
- すべての池で中央値を超える要素数が
$rank$ 以上なら中央値を上げられる - いずれかの池で中央値を超える要素数が
$rank - 1$ 以下なら中央値を下げられる
と
コードはこちら
値域から計算量が少ないことが読み取れる。
コードはこちら。もっと簡潔に実装できる。
コードはこちら
Union-find木は推移関係を表現できる。
AをBに置き換え、BをCに置き換えれば、AはCになるという推移則が成り立つ。なのでAからB、BからCに置き換えると分かっているなら、AからCまたはCからAへの置き換えは要らない。これをunion-find木で管理する。答えはunion-find木に追加した回数である。
コードはこちら
この種の数学問題は正解率が低いので、獲れると差がつく。
重心で回転すれば、二つの図形が一致するならすべての点が重なる。一方を固定すれば、回転角は点の数だけしかない。原点からの距離が一致する点を見つけて(見つからないなら二つの図形が一致しない)角度をそろえ、すべての点の座標が一致するかをどうかを一通りの回転角で試してみる。
すべての点の座標が一致するかどうかを総当たりで計算してよいのである。我ながら惜しかった。
コードはこちら
頂点数が少ないグラフには、ワーシャルフロイド法が使える。
ワーシャルフロイド法のループは外から順に、経由地、出発地、到着地にする。この順番を間違えると解けない。
ワーシャルフロイド法は、経由地の番号が浅い順に最短距離が更新される。この途中結果を集計すれば、問題の答えが求まる。
コードはこちら
二点間の距離は、余分に往復しても計算結果が変わらないことがある。
例えばオイラーツアーは、行きを枝の重み
この問題では、一往復すると距離は偶数増えるので、最短距離か寄り道したかで、二点間の距離の偶奇は変わらない。なので二点間の移動は必ず根を経由することにして、根からの距離の和にしても、二点間の距離の偶奇は変わらない。距離が偶数なら街で、奇数なら道路上で出会う。
コードはこちら
盤面を回転すれば絶対値を外せる。
ということで、盤面を4回回転して、左上の駅の方が高いという仮定の下で求める。DPを上手く使う。なんとなく解るというだけでは実装できないので、よく理解する必要がある。
コードはこちら
辺の距離が等しいのでBFSで解く。
キューには距離を載せ、それとは別に頂点1から他の頂点への距離と、他の頂点に行く組み合わせを管理する。
- 辺をたどると最短経路にならない頂点(迂回路の先に頂点)はキューに載せない
- 辺をたどると最短経路以下になる頂点については、その頂点に行く方法の数を足す
- つまり辺の先の頂点が最短経路ちょうどのとき、辺の先の頂点をキューに載せないが、その頂点に行く方法の数を足す。これがこの問題の肝である。
鉄則本の力試し問題 C14の要領で解いたら、ダイクストラ法なので
コードはこちら
下駄を履かせる。
変数に一斉に同じ数を足す操作は、足した値をオフセットとして持てば、それぞれの変数を更新しなくて済む。オフセットの初期値は0である。
コードはこちら
制約をよく見る。
行列演算のコストが高すぎてダブリングは使えない。よって別の方法を探す。
道の遷移行列を直に掛けると
コードはこちら
辺の数が頂点の数-1で、すべて頂点が連結なら木である。
辺を std::vector<std::vector<Num>>
で管理すると、頂点から次にたどる頂点を並べ替えできる。あとはDFSで訪ねればいい。この方法はオイラーツアーなので、スニペットにしておく。
コードはこちら
小さな距離と大きな距離
壁の無いところに一歩踏み出す時の小さな距離を
壁がなければ行ける場所の、現在地からの相対座標。
std::vector<std::pair<Num, Num>> diffs_short {
{-1, 0}, {1, 0}, {0, -1}, {0, 1}
};
壁を壊して行ける場所の、現在地からの相対座標。
std::vector<std::pair<Num, Num>> diffs_long {
{-2, -1}, {-2, 0}, {-2, 1},
{-1, -2}, {-1, -1}, {-1, 0}, {-1, 1}, {-1, 2},
{0, -2}, {0, -1}, {0, 1}, {0, 2},
{1, -2}, {1, -1}, {1, 0}, {1, 1}, {1, 2},
{2, -1}, {2, 0}, {2, 1}
};
これらをすべてのマスから相対座標で行った先について、双方向の枝を張る。後は左上から右下までダイクストラ法で最短距離を求め、大きな距離で割ると答えが求まる。小さな距離はいくら長くても答えは変わらない。
壁を壊して進むならそれはゴールに向かう最短経路なので、壊した2x2の壁の中には一度入ったらそこから出るしかなく、壊した2x2の壁の中を大きな距離で動くことはありえない。
公式解説によれば01BFSで解けて、その方が実装が簡単で速い。
コードはこちら
灰diffだが、AtCoder Daily Training MEDIUM 2023/11/23 18:30 start で解いて手こずった。
時刻で高速シミュレーションする。初期値はすぬけ君 std::set
型の集合
すぬけ君
公式解説通り二周させると
コードはこちら
辺の重みの最大値に関心があるなら、その辺より重い辺を除けばいい。
ということで、軽い辺から重い辺に向かって順番に辺を追加して、最終的に木になるようにする。辺
連結成分はunion-find木で管理する。辺が無い頂点の連結成分は1個で、 atcoder::dsu::size
もそうなっている。
コードはこちら
エラトステネスの篩っぽいことをする。
これは公式解説通りである。
コードはこちら
N=10ならBitDP
なのだが、加算方法が4通りあってよく場合分けしないと解けない。
コードはこちら
シミュレーションしてもいいが、しなくても済むことがある。
色が
- 同じ筒に
$I_1$ ,$I_2$ がある -
$I_1$ の上に$J_2$ かつ$I_2$ の上に$J_1$ がある (1と2を入れ替えても対称なので同じ)
これだけ検出すればACできた。解を示せと言われたらトポロジカルソートでできるらしい。
コードはこちら
上から整地する。
楽しさが最大のアトラクションに、楽しさが二番目になるまで乗りまくる。次に楽しさが最大だったおよび最初は二番目のアトラクションに、楽しさが三番目になるまで乗りまくる。以下同様に楽しさを整地して、K回乗るかすべてのアトラクションの楽しさが0になるまで乗ればよい。このとき満足度が最大になるので貪欲法で解ける。Kが中途半端に余ったときの処理に気を付ける。
別解として二分探索がある。 こう実装した。
コードはこちら
C配列はできれば使いたくない。
この問題の本質ではないのだが、C配列のサイズを一桁間違えたためにバッファオーバーランが発生して結果がおかしくなり、ジャッジサーバがREするまで気が付かなかった。できれば範囲チェック付きの配列を使いたい。そもそもこの問題で二次元配列は必要ない。
本題に戻ると
コードはこちら
STLの使いこなしが問われる。
切れ目 std::set<Num> cuts
を順序付けて並べ、 cuts.upper_bound(x)
で見つかる。「クエリを処理する時点で切られていないことが保証されます」ので lower_bound
でよかった。
コードはこちら
コレクションを二つに分ける。
この種の問題で何度も見た通り、ソート済集合と、挿入順序がついた集合の二つを分ける。ソート済集合は std::multiset
でも優先度キューでもよい。挿入順序は std::queue
だが、クエリ3で空にするので std::vector
でもよいかもしれない。
コードはこちら
実際にやってみる。
余白の削除と回転はそこそこ長いコードになるので、あらかじめコードスニペットにしておく。
コードはこちら
制約をよく見る。
コードはこちら
逆から考える。
辺を除くのではなく、辺を付け足すことを考えると、union-find木を活用できる。
報酬が負の辺は除く価値がないので最初から存在することにする。このとき辺を増やせばいつかは全頂点が連結になる。これは最小全域木を構成するアルゴリズムと同じである。
- 辺
$(A_i, B_i)$ の$A_i, B_i$ が連結なら、連結成分は増えないので辺を足す意味は無い。これは辺を除くことの逆操作なので、報酬$C_i$ をもらえる。 - 辺
$(A_i, B_i)$ の$A_i, B_i$ が連結でなければ、連結成分が増える。報酬はもらえない。
コードはこちら
繰り返しになるが、制約をよく見る。
たこ焼きとたい焼きはそれぞれ300個しか食べないのだから、300個以上は一律300個とみなして構わない。そうすれば
コードはこちら
悩むより手を動かすこともときには大事。
問題文の通り実際に実装すると答えが求まる。
意の要素を取ったときの和として取りうる値は、301通りしか考えなくて済む。
コードはこちら
重複を上手く数える。
題意を満たす頂点の組を数え上げるには、木の高さ(根から葉までの辺の数)を
- 根しかないときは、題意を満たす頂点の組は存在しない
- 高さが1のときは、葉から距離1で到達できるのは根だけ、葉から距離2で到達できるのは根の先にある葉だけである。
- 高さが2のときは、葉から距離1で到達できるのは葉の親だけ、葉から距離2で到達できるのは葉の兄弟と根、距離3以上だと根の向こう側にある頂点に到達する
- 高さが3ときは、葉から距離1で到達できるのは葉の親だけ、葉から距離3で到達できるのは根と葉の間にLCA(最小共通祖先)がある頂点、距離3だと根だけで、距離4以上だと根の向こう側にある頂点に到達する。
高さが2のとき葉から距離2で到達できるのは根と葉の兄弟というのは、高さ1の木と同じである。つまり高さが
-
$(v,u)$ の組み合わせは、$i \ne 2D$ なら$2 |v| \times |u|$ 個である。$v$ と$u$ は根からの距離が異なるので区別でき、入れ替えたものも数えるために倍にする。 -
$i = 2D$ なら、$v$ と$u$ は対称なので$|v| \times |u|$ 個 である。$v$ と$u$ は根からの距離が同じで区別できず、入れ替えたものを数えないため倍にしない。ここを間違えやすい。
高さ
コードはこちら
intだとオーバーフローする。
何か適当な頂点を根にして木を作る。このとき各頂点について子頂点の数(自分自身は含めない)を数え、すべての頂点の深さの和(根は0とする)を集計しておく。DFSで求まる。
根についてはすべての頂点の深さの和が答えである。根の子頂点
LCAの実装をそのまま使うと
コードはこちら
いもす法である。
いつも通り出入りイベントを定義して、同一時刻を圧縮してから計算する。
コードはこちら
区間和を
セグメント木に載せると上手くいく。まず入力を座標圧縮する。
一般的に
コードはこちら
制約をよく見ると、値域が狭い。
基本は数列の
コードはこちら
久しぶりに連立方程式。
ある頂点から他の頂点に行く経路は、BFSで経路を記録しながら求まる。よってある辺を何回通るかも分かる。
1回以上通る辺が
Rが0通りの場合があるのに注意する。
コードはこちら
A happens before B という条件は有効グラフで表現できる。
すべての条件をを満たす解があるなら、トポロジカルソートで解を得られる。トポロジカルソートに一意性はないが、優先度キューを使うとできるだけ頂点番号が最小のものから並べた解が得られる。
トポロジカルソート可能と循環(サイクル)が無いことは同値である。トポロジカルソート後の頂点数が入力より少なければ、トポロジカルソートできなかった、つまりサイクルがあったと分かる。
コードはこちら
考察が大変。
長方形が2個なら、指定された面積以上なのだから横幅一杯広げて、その上に他の長方形を乗っければよい。縦横は交換して試す。
長方形が3個なら、まず下に一個置いて残り2個をを上に置けばいい。縦に積むか横に積むかはそれぞれ試す。縦横は交換して試す。
コードはこちら
遅延セグメント木。
(
と )
を +1
と -1
に読み替えて、括弧の収支が合えばいい。つまり区間
区間和はセグメント木で管理し、クエリ1でセグメント木のノードを入れ替える。これとは区間和の最小値を遅延セグメント木で管理する。
- ノードは整数とする
- 区間に対するクエリは、区間の最小値を返す。単位元は負の無限大にする。
- ノードに対する作用素は整数の可算にする
ノード
クエリ2に対しては、
コードはこちら
状態遷移はグラフで表現できる。
この種のパズルは、コマではなく空白を動かすと考える。すべてのコマの状態を遷移前と遷移後を辺で結び、状態間の最小距離が答えだ。よってBFSで求まる。C++で1秒掛かる。
空白を 9
する。 123456789
をゴールとして、 123456789
から 987654321
までの順列を網羅することで、遷移元の状態をすべて挙げることができる。状態を数値として持っても文字列として持ってもいいが、BFSの枝と距離を std::vector
ではなく std::map
で持たないとメモリが足りない。
コードはこちら
最上行の条件は二つある。二つ目を忘れると after_contest が通らない。
- 右隣の列は、7で割った余りが1ずつ増える
- 右隣の列は、(7で割っていない元の)値が1ずつ増える
最上行以外は、直上の行に7を足した値である。
コードはこちら
電車の前と後を別の頂点と考える。
公式はfrontとbackという二つの頂点配列を用意し、車両をindexとしている。頂点を一つの配列で管理し、奇数を車両の先頭、偶数と車両の末尾としてもいいが、公式のように上手く考えないとif文だらけでコードが長くなる。
コードはこちら
最少公約数を使えばその倍数は表現できる。
std::unique
なり std::set
なりで重複を排除する。
コードはこちら
考察が大変。
頂点から辺が一本から出ないのだから、頂点の数と辺の数が等しいことが必要条件であることが分かる。というより解説を読むまで気が付かなかった。大変なのは十分条件でもあることで、公式解説に長い文章がある。分かってしまえば実装は7分でできるが、何時間考えても答えが見えなかった(サイクル検出は関係なかった)。
再実装 したものを自分の言葉でまとめると以下のようになる。条件に重複が多く、実は最後の条件だけ調べればよい。
-
$N \ne M$ なら各頂点から辺が一本ずつにはできないので、0通り。下記に含まれるので、明示的に計算しなくてよい。 - グラフを
$G$ 個の成分に連結成分分解して、すべての連結成分が以下の条件を満たすなら、答えは$2^G$ 通り。- 連結成分は2個以上の頂点を含む(1頂点しかなければ辺を出せない)。これも下記に含まれるので、明示的に計算しなくてよい。
- 無向グラフとみなしたときの、頂点の次数の和は、頂点数x2に等しい。これがないと1 WAする。
上記の条件を満たす場合、なもりグラフなので必ずサイクルちょうど1つ存在する。サイクルの向きは右回りと左回りの二通りあり、サイクル外からサイクルに向かう枝はサイクル側に向けるしかないので一通り、よって連結成分において向きの付け方は必ず二通りである。
コードはこちら
二分探索と、縦の物を横にする。両方思いつくのは結構大変である。
条件を満たすのにちょうど
部署からプロジェクトに出す人数は、プロジェクトの数か部署の人数かどちらか少ない方である。よって人数が多い部署から
プロジェクト1,2,... に人を出し、足りなければその次に人数が多い部署からプロジェクト3,4,... に人を出す、というのを繰り返せば
例によって整数の二分探索は、幅が2のときが大変なので、
コードはこちら
指定された値以上に滑っていくことを想像する。
std::set::lower_bound
がうってつけである。
- 値が -1 の要素を全部入りとして集合
std::set
で持っておく - 添え字が
$x_i$ 以上(Nは0にループする)で -1 の要素は集合にあるはずだから探す -
$A_h$ が$x_i$ から来たことを書き留めて、クエリ2で返す。まだ書き留めてないなら、クエリ2の返り値は-1である。
公式解説通り区間管理を実装すると こうなる 。かなりコードが長くなるのでこの問題では要らないが、他の問題でこの種の実装を正確にできる必要はあるだろう。
Union-find木を用いて区間を束ねると こうなる がそこそこ実装量がある。
コードはこちら。Pythonである。
こちらの記事 が正解である。だが
64-bit整数を受け付けるように、 __int128
を使うなり (提出したコード) 、 boost::multiprecision::cpp_int
を使うなり (提出したコード) 、PythonやRubyの任意精度整数なりを使えばよい。いずれにせよ累乗を求めるコードはしっかりデバッグしておく。
コードはこちら
二分探索には向かないが尺取り法ならいけそう。
先頭からみて、連続するXが最長何個か求める。これも公式解説は累積和を用いたすっきりした解法で、私の元の解法はif文だらけでごちゃごちゃして見づらい。尺取り法も二分探索と同様に、型に嵌めて覚える。
なので累積和を使って解き直したのが こちらである。 X
ではなく .
を数えるのがミソである。
-
$S$ の先頭からの.
の個数の累積和cumsum
を取る。 -
$S$ の$i$ 文字目およびそれ以後で、.
が$k+1$ 回現れる場所を求める。これは$cumsum(i-1)+k$ をstd::lower_bound
で求めれば分かる。 - 返ってくるイテレータ
it
は.
が$k+1$ 回に現れる位置で、該当するものがなければ$S$ の末尾である。これは言い換えれば、.
が$k$ 回現れ、その後にX
が0回以上現れ、その直後の位置である。よってit - (it.begin()+i)
が、$i$ を始点としてX
を最大で連続できる個数である。 - これを
$i=1..N$ について求める。
尺取り法で実装すると こうなる
コードはこちら
Union-find木は辺を削除できない。
なので辺が無い状態から辺を付け加える。連結成分の数はライブラリからは直接求められないが、以下から求まる。初期状態は頂点も辺もないので連結成分は0個である。
- 頂点1個を加えたら1増える
- 辺を1本加えて、非連結成分な頂点を結んだら1減る
コードはこちら
Greedyでいけそうと思ったらgreedyな件である。
並べ替えをどうするかで実装量が変わる。公式解説は非常にすっきりして、そうでないとif文だらけになる。229-D と同様に、尺取り法のような単純なループで実装する方法を覚える。尺取り法はleftをfor eachしてカウンタをインクリメントする方が、条件に合うときだけイテレータをインクリメントするより書きやすい。 こう 実装する。Leftとrightの両方を自由に動かすとややこしい。
コードはこちら
除算の商を固定して、その商になるような割る数を考える。
コードはこちら
ループ検出はトポロジカルソートでできるが、union-find木の方が簡単である。
木構造つまり連結している頂点同士はたどっていけるがループが無い、という状況で、連結成分にある頂点同士を短絡するとループになる。というのをunion-find木だと簡単に確認できる。それと、次数が3以上の頂点があると題意を満たさないことも忘れずに検出する。
コードはこちら
座標圧縮してセグメント木
問題文を解釈するのがややこしいが、要するに高橋君が
221-Eより大変なのは
コードはこちら
再帰がそれほど深くなければDFSで書ける。
公式回答は右下から左上に向かってループで実装している。左上から右下に向かっても同じことができるし、ループではなくDFSでも解ける。ループは右下から左上に向かって このよう もしくは このよう に行い、左上から右下に向かうと上手くいかない。それと
BFSで実装するときは、優先度キューで距離が短い順に処理し、優先度キューに同じマスを二度投入しないように seen[y][x]
でガードする。ガードしないとTLEする。実装は こちら か こちら
コードはこちら
最初に重要なことだが
DPとしては4種類用意する。つまり
-
$x=x2 \land y=y2$ は、$x=x2 \land y \ne y2$ ,$x \ne x2 \land y=y2$ の各点からそれぞれ一通りの方法で行ける -
$x=x2 \land y \ne y2$ の各点へは、$x=x2 \land y=y2$ から$h-1$ 通り、$x=x2 \land y \ne y2$ の各点から$h-2$ 通り、$x \ne x2 \land y \ne y2$ の各点から一通りの方法で行ける -
$x \ne x2 \land y=y2$ の各点へは、$x=x2 \land y=y2$ から$w-1$ 通り、$x \ne x2 \land y=y2$ の各点から$w-2$ 通り、$x \ne x2 \land y \ne y2$ の各点から一通りの方法で行ける -
$x \ne x2 \land y \ne y2$ の各点へは、$x=x2 \land y \ne y2$ から$w-1$ 通り、$x \ne x2 \land y=y2$ の各点から$h-1$ 通り、$x \ne x2 \land y \ne y2$ の各点から$h+w-4$ 通りの方法で行ける
コードはこちら
下駄を履かせる、つまりオフセットで考える。
コードはこちら
自力で筆算を実装する。縦の物を横にする。
なので任意精度整数の足し算と、10で割る操作を実装すればよい。しかし10で割る操作をそのまま実装すると計算回数が桁数の2乗になってTLEする。なので上位
任意精度整数を用いた別解をC++と boost::multiprecision::cpp_int
で実装したらTLEした。
コードはこちら
一度圏外に落ちたら復活できない。
のでこれまでの上位K番目までの値を std::priority_queue
で持ち、数がくるごとに圏内か圏外か調べて集合を更新する。 std::multiset
で持つ場合は こう する。
コードはこちら
候補を全列挙してもよいことがある。
等差数は少ないので全列挙してよかった。これは思いつかなかった。
- すべての桁について、数字が0以上9以下に収まる
-
$N \geq X$ である。つまり辞書式順序で以下が成り立つ
- 上から
$i$ 桁目が Xより大きい$X_i < c+d*(i-1)$ - 上から
$i$ 桁目が Xと同じ$X_i = c+d*(i-1)$ で$i+1$ 桁目についてこれらの条件を満たす
となる
全列挙すると こちら 。
コードはこちら
後で並べ替えるのだから、 aaa..zzz
と同じ文字は常に連続して出現すると考えて差し支えない。そのような
std::map
だと
数えるのは順不同の文字列なので、同じ文字が何回出るかを求める。例えばaが3回、bが2回, cが5回とする。
-
a
が1回、a
が2回、a
が3回出て、a
以外の文字がない組み合わせは1通りずつ(a
,aa
,aaa
)。 -
b
が1回出るということは、a
の1..3回の並びにb
を差し込むということである。差し込む場所はa
の前後併せて4か所である。 - 一般的に
b
を含まない長さ$L$ の文字列があって、$i$ 個のb
を差し込む組み合わせは、$L$ の各文字もそれぞれのb
も区別しないで並べる順列組み合わせなので${L+i} \choose i$ 通りである。 - よって
$L$ が$combi(L)$ 通りあるなら、$L$ の順序を保ったままb
を差し込む組み合わせは、$combi(L) \times {{L+i} \choose i}$ 通りである。 - これを空文字列から最大長の
$L$ 、ここでは空文字列、a
,aa
,aaa
について求める。
これを a,b,c について逐次的に求める。
- 初期値として、空文字列は1通りある
- 空文字列に
a
を加えて$i$ 文字にする組み合わせは、$combi(empty) \times {i \choose i} = 1$ 通り -
a
しか含まない長さ$L$ の文字列にb
を加えて$i$ 文字にする組み合わせは、$combi(|L|) \times {L+i \choose i}$ 通りある。これを$L$ の長さ$0..max(|L|)$ と、b
の個数$0..|b|$ の組み合わせについて求める。 -
a,b
しか含まない長さ$L$ の文字列にc
を加えて$i$ 文字にする組み合わせは、$combi(|L|) \times {L+i \choose i}$ 通りある。これを$L$ の長さ$0..max(|L|)$ と、c
の個数$0..|b|$ の組み合わせについて求める。以下同様に、まだ加えていない文字種に対して同じことを行う。 - 最後に空文字列以外のすべての組み合わせの数を足す
コードはこちら
探索するかどうか悩むより、BFSに任せよう。
DFSでもスタックを使い切らず答えが求まるが、BFSすべきだろう。遷移先の数字は6桁しかないので、Nから1を作るより1からNを作る方が簡単である。
Nから1を作るときに
-
$i$ を$a$ で割り切れる -
$i$ が10以上である -
$i$ の上から二桁目が0ではない。$i$ の最下位桁は0ではない、というのは条件ではないので注意する。
コードはこちら
クエリ先読み
本来の辺と、クエリで加える辺の両方を区別せずにMSTに加えようとすればよい。クラスカル法で重みの小さい辺から加えていき、クエリで加えることに成功した(非連結成分同士を結ぶ)ならそのクエリは Yes
を返し、加えられないときは No
と返す。クエリの辺はunion-find木に加えるつもりというだけで、実際に加えてはならないので注意する。
コードはこちら
重複する組み合わせをできるでだけ数えない。
XORは動的計画法できないので総当たりしかないが、
- 組1には、人1が必ずいて、人
$2..2N$ の誰か組む - 組2は、組1が
$(1,2)$ なら人3が必ずいて人$4..2N$ と組む。そうでなれば組1は$(1,i), i\ne 2$ なので、組2には人2が必ずいて、人$3..2N$ のうち人$i$ 以外と組む。 - 一般的に組
$k$ には、まだ組んでいない人のうち最も小さい番号の人を固定し、それ以外の残りの人と組む
これをDFSかループ実装すると、すべての組み合わせを再帰的に求められる。
コードはこちら
DPだけでは解けない。
std::vector<Num>
をコピーして末尾に
公式解説にある通り、平均値と中央値を決め打ちして二分探索するのだが、何要素あったかを数えないというのが巧妙である。これは思いつかなかった。
コードはこちら
返り値のイテレータが何かSTLの仕様を把握しておく。
イテレータによる挿入は、挿入した要素を指すイテレータを返す。よって挿入した場所の前後に挿入する処理は、返り値のイテレータを連鎖させればよい。挿入コストが定数の std::list
を使う。
コードはこちら
ポテンシャル
ダイクストラ法のminをmaxに取り換えたら、 after_contest_01,05.txt
だけTLEした。公式解説2を自分で実装したら違うテストケースがTLEした。ということで自力では解けなかったので、結局公式解説1をそのまま実装した。
コードはこちら。解説のように解析的に解かず、かなりややこしい解き方をしたがこれでもACできる。
式変形が問われることがある。
詳しは公式解説を参照。直観的な説明は、加算器を思い浮かべて桁の繰上りが起きない条件とは何かを考えれば分かる。
コードはこちら
累積和の使い方
累積和の要素に依存関係を持たせてグラフを作り、連結かどうか調べる、というのが答えだが全く思いつかなかった。なので公式解説を読んで実装した。水diff以上は解法を全く思いつかないことがある。
コードはこちら
Min-max戦略である。
先手が何をしても後手が最善を尽くせば後手必勝、そうでなければ先手必勝である。これを総当たりで求める。
コードはこちら
入力範囲に注目する。
コードはこちら
重実装かもしれない青diffを解いてみた。
高速道路を連結するなら、異なる連結成分をつなぐのが得である。そこで街を連結成分に分けて、あと何回連結するかをマージテクで管理する。
- 街
$i$ をあと何回連結できるかを、入力$D_i$ で管理する。連結するたびに1減らす。 - 連結している街の集合
$G_j$ に含む、$D_i > 0$ な街を$S_j$ で管理する。集合の代表元に関連付ける。 - 連結している街の集合
$G_j$ を何回連結できるかを、$Cnt_j$ で管理する。集合の代表元に関連付ける。
もっとも連結しにくい街の集合(
街を連結すると
これを繰り返すと、連結できる街が無くなるか、街の集合が一つになるか、どちらかである。すべての街が連結でなければ(街1を含む連結成分の大きさが
残りの高速道路の新設枠を、連結可能な街同士を連結成分かどうか問わず連結して、高速道路を
方針は公式解説2と同じだが、もう少し軽い実装がありそうだ。
コードはこちら
たまにはスタックを使う。
コードはこちら
題意を諦めずに読み解く。
問題文がとてもややこしいので読解に時間が掛かるが、要するに部分木を葉ノードで分類する、ということである。つまりある二つの部分木について、葉ノードが互いに素なのか包含関係かということを、通し番号で表現する。これは葉を左から右(あるいは右から左)に番号をつければよいので、DFSの訪問順に葉ノードに番号をつければよい。短く実装すると このように なる。
コードはこちら
冷静にピーク値を計算する。
区間の終わりが答えではないない場合が二通りある。以下も求めて、最大値を取ると答えになる。
- 答えが先頭要素だけつまり
$x_1$ のときがある。空集合で0は答えではないので注意。 - 区間の途中にピークが来る場合がある。具体的には、
$x_i < 0, base=B_{1+y_{i-1}} > 0, B_{y_{1}} < 0$ のときである。このときは区間の先頭を1番として位置$p=base/-x$ にピーク$C_{y_{i-1}} + \sum_{i=1}^{p}(base+x \times i)$ が来る。この条件判定で区間を1間違えて$base=B_{y_{i-1}} > 0$ にするとWAする。
注意点として、負の数の除算を避けて、正の数の除算にする。
コードはこちら
解説を読むと様々な方法があって面白い。
クエリ先読みをするかしないかどちらでも行ける。クエリ先読みをしないかつSTLの範囲で解くなら、 std::multiset
と std::multiset::upper_bound
を使えばよい。座標圧縮してセグメント木やBITでも行けるらしい。
コードはこちら
鳩の巣定理
状態遷移における状態は knot
を2度通ることが鳩の巣定理から言える。つまり初期状態 0
から knot
までいき、 knot
から knot
へのサイクルを回り続けることが分かる。高々 knot
を求められる。その後は、
- 初期状態
0
からサイクルの手前まで - サイクルを回り続ける
- サイクルの途中で
$K$ 回が尽きる
までのそれぞれについて、追加するアメを数える。初期状態 0
からサイクルの直線までの累積和と、サイクルの始点から始点に戻る直前までの累積和を求めることで、大きな
ダブリングでも解けると思ったが上手く定式化できなかった。実際公式解説にある通り、ダブリングでも解ける。
コードはこちら
ゴールからスタートに再帰する。 ABC
巡回を何回進めるかは
再帰の方向を間違えるとどうしようもないことがある。公式解説のようにゴールからスタートに再帰するとあっさり解ける。しかしスタートからゴールに再帰すると、ノード数が
一応スタートからゴールにたどることもできる。
-
$k > 2^{60} > 10^{18}$ なら、$S$ の最初の一文字から派生した系列で埋め尽くされる。よって$S(0)$ を$t - 60$ 回ABC
と巡回した文字を起点に60回枝分かれした$k$ 文字目である。 -
$k < 10^{18}$ なら、$S$ の一文字当たり$n=2^t$ 個の葉を持つので、$S( \lfloor k/n \rfloor)$ 文字目を起点に$S(k \quad mod \quad n)$ 文字目である。 - 根から葉までは、一段降りると
ABC
の巡回を一文字進み、$k$ の立っているビット1が1個あるごとにさらに巡回を一文字進む。
ゴールからスタートにたどるコードは、 このように はるかに簡潔である。
コードはこちら
辞書順の意味をよく考える。
下準備に、長さ
- 1文字目が
C
なら、A
またはB
で始まる長さ$N-1$ の任意の回文と、C
で始まる回文がある。残りの文字を見ていく。 - 一般的に、
$1..(center-1)$ 文字目の文字が$c$ なら、$ord(c)-ord(A)$ 種類の長さ$N-1$ の任意の回文と、$c$ で始まる回文がある。ここで$ord(c)$ は$c$ のアスキーコードである。 - 中心の文字が
$c$ なら、少なくとも$ord(c)-ord(A)$ 種類の回文は作れる。
なので中心の文字を
- 反転した位置より反転前の位置の文字が辞書順で後に来れば勝ちが決まる
- 反転した位置より反転前の位置の文字が辞書順で前に来れば負けが決まる
- 反転した位置と反転前の位置の文字が同じなら次の文字を比べる。すべての文字の組が同じなら勝ち。
勝ちなら中心の文字を
上記をすっきりまとめた実装が こちら である。
コードはこちら
完全二分木なら、
なので ノード
コードはこちら
青diffを解き損ねた。
ワーシャルフロイド法で最短距離を求め、迂回路が直交路より短ければ直交路は要らない。ここまですぐ分かったがWAが取れなかった。公式解説にある通り、始終点を固定して経由地を総当たりすればよいのだが、迂回路と直交路の距離が同じなら直交路は不要という境界条件を間違えた。
ワーシャルフロイド法は経由地つまりパスを同時に求めることもできる。当初はその方針で解いており、実際このように 解ける 。
コードはこちら
転倒数が答えになる。
一般解は転倒数だがこの問題は奇置換か偶置換かだけ区別できればいい。置換は3通り、状態は6通りしかないので全部数えあげればいい。
コードはこちら
真面目に数え上げると辛いときはDPを使う。
std::vector<ModInt> table(n+1, 0)
を奇数、偶数、前と今の4個である。前と今のテーブルを入れ替えるのは std::swap
が便利である。 std::vector<std::array<ModInt, 2>> dp(n);
と同じ型の next
を持つとswapが楽である。
これをDPで更新する。
- 移動先が
$X$ なら、前に$X$ を通った回数が奇数なら今は偶数、前の回数が偶数なら今の回数は奇数である。このように移動元の回数を移動先に加える。 - 移動先が
$X$ なら、前に$X$ を通った回数が奇数なら今も奇数、前の回数が偶数なら今の回数も偶数である。
コードはこちら
小学生の筆算である。
多項式の除算を筆算するには、単に10進数ではない割り算と考えればよい。
コードはこちら
入れ物と入れるものを混ぜる
と言ってしまえばそれだけだが、解説を読むまで何をしていいか分からなかった。235-Eと考え方は同じだった。
チョコレートと箱をまとめて降順に並べればよい。そうすれば縦の長さの降順、縦の長さが等しければ横の長さの降順、縦横が等しければ箱が先でチョコレートが後になる。この順に先頭から見ていけば、チョコレートの縦が収まる箱はもしあるなら見つかっているので、横が収まるぎりぎりの箱に入れればいい。
コードはこちら
難しく考えすぎない。
グラフに一つ以上のループがあるなら、ループに到達可能な頂点の集合が答えである。ただし孤立している、つまり辺を持たない頂点はあらかじめ union-find木で除いておく。すべての頂点から出発し、ループまたはループに到達可能な頂点にたどり着いたらそこで探索を打ち切る。これで解けるが有向グラフのループ検出を実装するのが難しい。
公式解説の方法はもっと簡単である。それ以上遷移先がない、つまり出次数が0の頂点を再帰的に刈っていけばよい。実装するとこうなる 。
もう一つの方法は、グラフを強連結成分分解して、頂点数が2以上の強連結成分から到達可能な頂点をBFSで求める方法である。強連結成分から到達できるかどうかは、有効グラフの向きを逆にしておくとBFSで調べられる(そうすればサイクルから枝に向かうし、サイクルは逆にしてもサイクルのままである)。実装は こちら 。
コードはこちら
値域に注意する。
整数の累乗が式で与えられときに定義域や値域を求めよ、という問題は探索範囲を上手く狭めないとTLEする。
公式解説は尺取り法を用いたもっと簡単な方法で、実装すると このよう になる。二分探索をラムダ式で実装すると このよう になる。
コードはこちら
BFS。
始点から1手、2手、3手... で到達可能な範囲をBFSで探索すればよい。探索範囲が尽きた時、終点までの最短手番が分かっているか、到達不能(距離が無限大)か分かる。
というのをキューで実装したらキューが長くなりすぎて、TLE, MLE, REが起きた。なので探索範囲を効率的に実装する必要がある。
- 探索範囲、つまり始点からの手番は1手ずつしか増えない
- 一度訪れた場所はキューに乗っているか乗っていたはずだから、キューに載せない
と思って全方位の行先を一気にキューに載せたらTLEぎりぎりのほぼ6秒掛かった。
正解は01BFSである。実装 が大変である。以下の通り正確に実装しないとACできない。
- 距離を方向別に管理する
- 優先度キューの辞書順比較は、距離、座標、向きの順する
- 頂点を訪問したかどうか印をつける
- 終点を見つけたらすぐ出力して終了する
- 距離を更新するのはキューから出した時ではなく、キューに入れた時にする
- 始点には向かう方向がないので特別扱いする
コードはこちら
ランレングス符号化である。以上。
コードはこちら
区間分割である。
難しく考えすぎない。
-
$p$ 以降で$X$ が出る位置を$p \leq X_{pos} \leq R$ とする -
$p$ 以降で$Y$ が出る位置を$p \leq Y_{pos} \leq R$ とする - そのような
$X_{pos}, Y_{pos}$ が存在しなければ、$p$ に対する組み合わせは無い - そのような
$X_{pos}, Y_{pos}$ が存在すれば、$p$ に対して$R + 1 - max(X_{pos}, Y_{pos})$ 個の組み合わせがある
これらを全区間および
尺取り法で解くと std::lower_bound
で
もう少し素直な実装を考える。
- 区間
$[1,N]$ を一つ以上の区間$[L,R,lows,highs]$ に分割する。$L$ は区間の左端、$R$ は区間の右端、$lows$ は$a_i=Y$ となる0個以上の位置(昇順)、$highs$ は$a_i=X$ となる0個以上の位置(昇順)である。これは$A$ を先頭から走査し、$Y \leq a_i \leq X$ が連続する区間をまとめればよい。$R=-1$ を初期値にすると、空の区間にできる。 - 区間は
$L$ の昇順に並んでいる。まず空の区間$R=-1 \lor lows.empty() \lor highs.empty()$ を無視する。そうでなければ$i \in [L,R]$ を先頭から走査し$min(lows), min(highs) \geq i$ の両方が見つかるなら$n - max(min(lows), min(highs))$ が$i$ に対応する右端の個数なのでそれらを合計する。 - 区間内の走査は
std::lower_bound
で 書ける が、 尺取り法 も覚える。尺取り法なら$[lows,highs]$ を保持する必要はない。
コードはこちら
値域に注意する。
std::lower_bound
で検索できる。
コードはこちら
いい実装がありそう。
- X軸に平行な直線は、Y座標で管理する
- Y軸に平行な直線は、X座標で管理する
- X軸にもY軸にも平行でない直線は、その直線を通る点の番号が最も小さい頂点番号で管理する。XまたはY軸との交点は浮動小数なので正確に表現できない。
直線の傾きは以下の通りにする。
- X軸に平行な直線は
$(1,0)$ - Y軸に平行な直線は
$(0,1)$ - X軸にもY軸にも平行でない直線は、傾きの最大公約数で割ったあと、X方向が正になるようにYの符号を適宜反転させる
これで直線の位置と傾きが決まるので、二つの直線が同一か判定できる。直線に
公式解説は外積を使っている。三重ループを回せばよいが、同じ直線を二度数えないように記録しておく。コードは こちら
ADT HARDで改めて解いたが、直線の同一判定を 以下のよう にした。4つの整数を使って、Y軸との交点(切片)を有理数表現する。ここで向きを正規化するとは、X軸方向の向きを非負にし(そうでなければ符号を反転する)、X方向とY方向の向きを絶対値の最大公約数で割って互いに素にしたものである。
- X軸に平行なら、1, 0, 0, Y軸切片。Y軸切片という意味では、1, 0, Y軸切片, 1 とするのが適切な実装だったが、ここではX軸に平行な直線を互いに区別できれば十分である。
- Y軸に平行なら、0, 1, X軸切片、0。Y軸切片は無いので、他の直線と区別できる一意な値ならなんでもいい。
- そうでなければ、正規化したX方向の向き、正規化したY方向の向き、Y軸切片の分母、Y軸切片の分子を使って、Y軸切片を有理数表現する。
公式解説にあるように、ちょうど
コードはこちら
計算量解析が重要である。
素朴な方法は、
公式解説にある通り、二重ループの計算量は実は小さい。
コードはこちら
後ろから考える。
先頭には
よって
$y_i$ -
$i$ 以降の$t=2, y \geq 0$ な$y$ の総和 -
$i$ 以降の$t=2, y < 0$ な$y$ のうち、$y$ の下位$K$ 個を除いたものの総和
一般的に最後から
$y_i$ -
$i$ 以降の$t=2, y \geq 0$ な$y$ の総和 -
$i$ 以降の$t=2, y < 0$ な$y$ のうち、$y$ の下位$K-p \geq 0$ 個を除いたものの総和
最後から
上記の1はその場でわかる、2は累積する、3は std::multiset
に
コードはこちら
やはり値域に注意する。特に変数間の制約は大事。
素数表を二分探索してもいいし、尺取り法でも求まるらしいが std::upper_bound
の方がコードは簡潔である。素数表を求める方法は頻出なのでスニペットにしておく。
コードはこちら
確率的アルゴリズム
集合を比較するとTLEするのでダイジェストを作る。重複の無い数字の集合
こうすると
Zobrist hashing をきちんと実装したものが こちら。
公式解説は、 集合
-
$i=1..n$ についての集合の大きさ$|a_1,...,a_i|$ を求める。$|b_1,...,b_j|$ も同様。 - 集合の大きさが
$k=1..N$ となるような$|a_1,...,a_i|=k$ と$|b_1,...,b_j|=k$ を逐次的に求める - 集合の大きさが
$k$ のとき、余ったつまり$a$ ,$b$ のどちらか一方にしかない要素があれば不一致、そうでなければ一致である。 -
$x,y$ について、$|a_1,...,a_x|$ と$|b_1,...,b_y|$ は前計算してあるので、不一致ならNo
である。そうでなければ集合の大きさ$k$ について集合が一致するかどうか前計算してあるので返す。