-
Notifications
You must be signed in to change notification settings - Fork 0
ABC lessons learned1
ABC 6,8問体制(126..最新)のD問題をだいたい解いたので、そこから得た教訓をまとめていきます。
コードはこちら
intを使わない。
問題自体は、連結するマスを連結成分に分解するだけである。色の異なるマス同士を連結してできるグラフでは、任意の黒マスから任意の白マスに到達可能である。よって連結成分の黒マスの数 x 白マスの数が求める個数で、これを連結成分ごとに数えればいい。連結成分といえばunion-find木である。
解の大きさが明示されていないが、どうも 32-bit 整数に収まらないようである。よって 64-bit 整数で計算する。ACLのunion-find木は成分を int
で扱っており、したがって long long int
から int
への narrowing cast が発生するが気にしない。答えがオーバフローしてWAするのは非常に分かりにくいバグなので、初めから回避する方が簡単である。
コードはこちら
EDPC G問題の復習である。
コードはこちら
コードはこちら
精選100-67で青diff。頂点同士ではなく、頂点とX,Yに平行な直線を結ぶ。
ある頂点
辺をコストの小さい順に並べ替える。あとはunion-find木でクラスカル法を実行し、コストが低い順に連結していない辺を連結する。
コードはこちら
精選100-63。
入力にワーシャルフロイド法を実行して結果が変わったら -1
である。
ある辺
コードはこちら
XORとは桁繰上りのない和である。
と知っていれば題意を満たす
2で割る回数は決め打ちでよい(入力から20回だし、32回としても問題ない)。
コードはこちら
ある数を
コードはこちら
累積和の累積和を取る。
- 最初にL始まりR終わる列車の二次元配列を作る。このときRで終わるのではなく、RおよびRより西で終わる列車を数える。これは累積和で求まる。
- 次にLで始めるのではなく、LおよびLより東で始まる列車を数える。これは上記に累積和を、Lついて累積和と取ることで求まる。
- あとは各クエリに答える。
コードはこちら
問題文を素直に実装する。
のだが、焦ると何をすべきか分からなくなる。順を追って考えよう。
- 入力を入力順に保存する(これを忘れがち)。県 x
$10^9$ + 年 x$10^6$ でよいが、出力に引きずられて県 x$10^6$ にするとWAする。 - 市を県ごとに分類する
- それぞれの県の市を、年の昇順で並び替えて1から順位をつける。値を県 x
$10^6$ + 順位 x$10^6$ とする。 - キーと値とを結び付け、入力されたキーの順に値を出力する
整数の0埋めはイディオムなので覚える。
os << std::setfill('0') << std::setw(12) << ids[key] << "\n";
コードはこちら
いもす法のココロだが、いもす法ではない。
手を動かすと解るが、水やりすると左端に上りエッジが一段、右端に下りエッジが一段できることが分かる。よって
図にすれば少し分かりやすい。以下の4パターン(左右対称を省略)で、上記の条件を満たすことが分かる。
+++++ +++++ +++++ +++++
-- --- --- -----
-+++++ +++++ +++++ +++++
コードはこちら
落ち着いて問題文を最後まで読む。焦らない。
コードはこちら
悩んでないで手を動かす。
- コマが1個なら、左端において、右端まで一通りなめる
- コマが2個なら、左右の端において、それぞれ中央に向かう。このとき二つのコマが出会う必要はないと解る。では最終状態でどれだけ離せるかというと
$X_i, X{i+1}$ の最長距離である。 - コマが3個なら、左右の端と、先の
$X_i$ または$X{i+1}$ におく、両端から中央へ、中央から端に向かう。三つのコマが出会う必要はなく、最終状態でどれだけ離せるかというと、先の$X_i, X{i+1}$ と二番目の最長距離$X_j, X{j+1}$ である。 - 以下同様に
$N$ コマあるなら$N-1$ 番目に大きい$X_k, X{k+1}$ を離せる。特に$N \geq M$ なら、すべてのコマをすべての$X$ に配置するので初期配置から移動しなくてよい
これを実装する。
コードはこちら
効率が悪いが組み合わせを網羅。
合成コストは、
竹の組み合わせ (0...0123
を始点として std::next_permutation
を回し、 0
を 0123
に置き換えればよい。このとき重複ありの組み合わせは
コードはこちら
クエリを先読みして逆から考える。
Union-find木は要素を増やすことはできるが除くのが難しい。なので、不便さが最大という最終状態から、橋を掛けたら不便さが減っていく、と逆操作を行う。A-Bに橋を架けることで不便さはAの連結成分 x Bの連結成分だけ減る。辺のない頂点の連結成分は1個なので特別扱いは要らない。
コードはこちら
左右から累積最大公約数を取ると、ある数を除いた最大公約数を求められる。セグメント木にも載るが、
コードはこちら
- 入力が木なので、幅優先探索(BFS)を使えば頂点数の線形時間で解ける。根である頂点からBFSすれば、各頂点を根に近い順に訪問できる。ダイクストラ法は
$log(N)$ 倍だけ時間が掛かるので使わない。 - 木の頂点をキューに載せるとき、訪問元の色を追加情報に載せる。 (辺の重さ+訪問元の色) の偶奇(and 1)を、訪問先の色にする。
- 根はどの頂点でもいい。わざわざ葉=次数が1の頂点を探す必要はない。
コードはこちら
難しく考えすぎないことも重要である。
コードはこちら
2時間8分掛かった。
それ以外の場合は工夫がいる。
公式解説を読むと、上記の
コードはこちら
- すべての操作終了後の結果だけ解答すればよいので、操作ごとに状態を更新をする必要はない。いわゆるクエリ先読みと同じ。
- カードを削除したり置き換えたりするコストが高そうなので、これらの操作を無くしたい
-
$A_i < C_j$ なら、$C_j$ が選ばれて$A_i$ は選ばれない。ここから「N枚のカードに書かれた整数の合計の最大値」を「降順に上位N枚選んだカードに書かれた整数の合計の最大値」と読み替える。そうすれば$C_j$ に隠された$A_i$ は無視できる。これでカードを削除せずに済む。 - ただしBが大きいので、CをB枚Aに追加する操作をM回やると、TLEかMLEするかもしれない。よって
$C_j$ の降順にN枚以上までAに追加する。Cを余分にAに追加してもどのみち小さいカードは選ばれないので、ざっくり実装して構わない。 - CをB枚というのは、きちんとしたコードなら構造体とメンバと比較演算子を定義すべきである。しかし比較演算子を定義する時間がもったいない早解きなら、
std::tuple
を使うと辞書順の比較演算子がついてくる。
コードはこちら
全く手掛かりがつかめず諦めたのだが、解答があっさりしていてびっくりした。
駒を一つ固定してDPしたら解けなかった。駒を二つ固定するのが正解である。駒を二つ固定したとき、以下の操作が答えである。
- 駒を二つ固定したときの、行の差の絶対値
$0 \leq dy < N$ 、列の差の絶対値$0 \leq dx < N$ を固定する。 - このような駒二つの距離は
$dy + dx$ である。 - 矩形
$(0,0)-(dy-1,dx-1)$ をの左上と右下に駒を置く方法は、行方向に$N-dy$ 、列方向に$M-dx$ 通りで、それぞれ独立なのでこれらの積である。 -
$dy > 0 \land dx > 0$ なら上下反転があるので二倍する - 上記をまとめると、
$(dy+dx)(N-dy)(M-dx)(1+(dy > 0 \land dx > 0))$ である
駒を二つ固定すると、残りの駒の置き方は
コードはこちら
コードはこちら
std::back_inserter
の使いどころ。
- 何回操作するか
$n_{iter} \leq K$ - 何個取り出すか
$n_{pop} \leq min(n, n_{iter})$ - 左から何個取り出すか
$left \leq n_{pop}$ - 右から何個取り出すか
$n_{pop} - left$
について、筒から宝石を実際に取り出せばいい。 std::copy
と std::back_inserter
を使うと簡潔に書ける。価値が負の宝石を最大
コードはこちら
当時は遅延セグメント木で青diffだったのだろう。
道路の工事時間は原点に読み替えて、位置
座標圧縮した時刻を遅延セグメント木に載せる。キーは座標圧縮した時刻である。値はある時刻において、最も左にある工事の位置
工事をすべて遅延セグメント木に載せたら、 -1
と答える。
コードはこちら
- 4,000,000マスなので、一マス
$O(log(max(H,W)))$ の探索コストならTLEしない - あるマスの光線が障害物に当たるかどうかは、
std::upper_bound
で分かる - 障害物に当たらずグリッドを突き抜けてしまうと、イテレータを
begin()
,end()
と比較するのがめんどくさい。なので番兵としてグリッドの外周を障害物で囲っておく。番兵を使うことを覚えておきたい。
コードはこちら
301-Dに似ているけどややこしい。
上の桁から順に組み合わせの数を累積すればいい。話がややこしいのは、ある桁が0ならその下位の桁については総当たりした組み合わせを増やし、ある桁が1ならその値に決め打ちして隣の桁をみることである。Lが2進数で
- 最上位桁を0と置けば、残りの桁は0または1である。(0,0), (0,1), (1,1)の組み合わせが
$3^{n-1}$ 通りある - 最上位桁は必ず1である。
$L=1x...$ について$x=1$ なら- 最上位桁は (0,1), (1,0)の組み合わせが
$2$ 通りある -
$x=0$ とおいたとき、$...$ については$3^{n-2}$ 通りある -
$11$ については隣の桁を見る
- 最上位桁は (0,1), (1,0)の組み合わせが
-
$L$ の最上位の隣の桁が0つまり$10...$ なら、0については (0,0)の1通りに限るので組み合わせは増えず、隣の桁を見る
一般的に
コードはこちら
- まず累積和 cumsum を取る、と勘づく
-
$cumsum(a_2 ... a_i)$ =$cumsum(a_1 ... a_i) - a_1$ なので、部分列の開始点をずらしていけば部分列の cumsum を更新できる。なので部分列の総当たりは避けられる。 - 実際には cumsum を更新する代わりに、部分和を取る区間より左も含めた cumsum + k 以上になる a の位置を探す。累積和は部分列を長くすれば増えるので二分探索か尺取り法が使える。まず二分探索で書く。
Num answer {0};
for(decltype(n) i{0}; i<n; ++i) {
const Num d = k + cumsum.at(i);
auto it = std::lower_bound(cumsum.begin() + i, cumsum.end(), d);
const auto count = cumsum.end() - it;
answer += count;
}
尺取り法を使えば、kの探索回数を
Num answer {0};
Num right {1};
for(Num left{1}; left<=n; ++left) {
while((right <= n) && ((cumsum.at(left-1) + k) > cumsum.at(right))) {
++right;
}
answer += n + 1 - right;
}
コードはこちら
LISっぽいのだが、なかなかLISが身につかない。
便宜上、空整数列
これを
コードはこちら
鉄則本の「6.4 一手先を考える」問題 A39とほぼ同じである。ここまでに仕事を始めないと間に合わない締め切り (b-a) を現在時刻が超過しないかどうか調べて、超過したら即 "No" と出力する。
仕事を表現する構造体は、締め切り、終点、掛かる時間から成る。ソートするのに必要なのは終点だけなので、構造体ではなく std::tuple
で作れば比較演算子は実装しなくてもついてくる。
コードはこちら
最大値を考えるらしい。
中心に頂点1を置き、残り
コードはこちら
連結成分とは気づいたが、それ以上どうしていいか分からなかった。
同一X座標上にある頂点の集合を
コードはこちら
けんちょん氏の解説にある通り、重複組み合わせに関する理解が問われる。
階乗を求めるコードは頻出なのであらかじめ用意してテストしておく。GMP (GNU Multi-Precision Library)を使えば、任意精度整数を使って組み合わせを求められるし mod 1000000007 も求まるので検算に使える。Rなら gmp
パッケージから利用できる。
コードはこちら
単にBFSして、各頂点までの最短距離を求める。ただし移動回数が3の倍数以外なら最短距離を更新しない。移動回数が3の倍数で最短距離を更新できない移動についてはその後に移動しないので、いつか探索する頂点は無くなる。
コードはこちら
変数の和が二つあるとき、それらから特定の変数を除くには、差を取ればキャンセルできることを思いつく。統計学では difference in differences (DID) という手法がある。
- 山
$i$ に降った雨の量を$a_i*2$ とする。雨量は偶数なのでこう表現できる。山$i$ に降った雨の量は$a_i+a_{i+1}$ である。 -
$B_2 = A_2 - A_1$ ,$B_3 = A_3 - B_2$ , ... と置く。下記より$B_n = a_1 + a_1$ である。ここから$a_1=B_n / 2$ が求まる。 - あとは
$A_{2..n}$ から芋づる式に$a_2..a_n$ が求まる。この値を2倍して出力する。
A1=a1+a2, A2=a2+a3, A3=a3+a4, A4=a4+a5, A5=a5+a1
B2=A2-A1=a3-a1,
B3=A3-B2=a4+a1,
B4=A4-B3=a5-a1,
B5=A5-B4=a1+a1
コードはこちら
素直に頂点を塗る。
131-Eの苦戦とは打って変わって、こちらは素直に彩色すればいい。木の頂点間の距離が2以下というのは、頂点
- 木の根は
$K$ 色で塗る。頂点番号をインデックスとして、何色で塗れるか保存する。 - 木の根に子が
$i$ 頂点個あれば、子を$K-i..K-1$ 色で塗る - 木の根に子の子が
$j$ 個あれば、子の子を$K-j-1..K-2$ 色で塗る。以下DFSで葉まで繰り返す。 - 各頂点を何色で塗れるかをmodintで乗算する
途中で塗る絵の具が無くなったら頂点を0通りの色で塗るので、結果的に答えは0通りになる。だから塗る色がなくなったときの処理は明示的に実装しなくてよい。
コードはこちら
エラトステネスの篩は、素数
ここではその逆で、素数とは限らない
コードはこちら
なんとgreedyだった。
解説を読んだところ、増加列を複数管理しておいて、増加列につなげられる(増加列の最大値より大きい)ならその増加列に積んで、そうでないなら増加列を1個増やす。積むべき増加列は最大値が最も小さいものを選ぶ、というgreedyで解ける。増加列そのものを管理する必要はなく、最大値の値だけ std::multiset<Num>
で管理すればよい。
LISとかstackとか色々考えたけど駄目だった。
コードはこちら
Mod 13 と mod 1000000007 を区別する。
何通りあるかは atcoder::modint1000000007
で数える。初期値は、余り0が1通り、他は0通りである。
コードはこちら。if文だらけで長いので、もう少しすっきり書けそうである。
結論から言えば、連続するマスを分割すればよい。
十分長い移動回数になれば、 LR
または RL
を往復してそこから外に出ないことが分かる。
-
LR
またはRL
になる出発点は、前者は左に連続するL
と右に連続するR
、後者は左に連続するR
と右に連続するL
である。 - 上記の分水嶺は
RL
である。よってRL
を見つけたらこのRとLの間でマスをグループ分けする。 - 後はグループごとに、
LR
またはRL
に到達するまでの回数の偶奇を求める。
別解としてはダブリングがあるらしい。
コードはこちら
操作後の
ある
コードはこちら
締め切りから考える。
動的計画法ではなく貪欲法だということは何となくわかるのだが、初日から
-
$M..0$ 日についてループする - ある
$i$ 日について、報酬を得られるまでちょうど$M-i$ 日掛かるアルバイトを優先度キューに追加し、 - その時点で最も報酬の高いアルバイトを請けたら、同じアルバイトは受けられないので優先度キューから削除する
コードはこちら
Bellman-Ford法だった。
頂点1と頂点Nの両方から到達可能な頂点だけでグラフを構成し、辺のスコアを
基本的にはBellman-Ford法で、頂点の値を
コードはこちら
126-D とほぼ同じである。違うのは辺の重みを伝搬するのではなく、親の頂点のカウンターを子にBFSで伝搬することである。
コードはこちら
こちらもgreedyだった。
134-Eの苦戦に比べると、こちらはあっさりしている。まず t
にあって s
にない文字があったら題意を満たさないので-1を返す。
あとは t
を先頭から順番に見ていけばいい。 s
の何文字目を指すか(カーソル)、 s
を何個つなげたか(周回数)を管理すればよい。
- 初期化時点では、カーソルは-1, 周回数は0とする
- まず
t
の先頭文字c
が、s
の何文字目か(先頭を0番とする)探す。s
を毎回スキャンするとTLEするので、c
の出る位置をあらかじめ調べて、std::upper_bound
で調べる。c
は必ず見つかるのでカーソルをそこに置く。 - 同様に
t
の二文字目d
を探す。d
の出現位置がカーソル以降なら.end()
が返るので、周回数を1足してカーソルを-1に戻してスキャンする。というより、d
の出る位置は調べてあるのだからその最小値である。
以後同様に t
の最後の文字までみていけば周回数とカーソルが分かるので答えが求まる。
コードはこちら
整数を整数Nで割ったときの余りは最大N-1である。ならば数列を回転させれば、一つを除いて割った余りを最大にできる。
コードはこちら
トポロジカルソート
試合
トポロジカルソートに成功した場合は、ソース済グラフの直径を求めればいい。これはトポロジカルソート済の頂点順にDFSして、 子の頂点の深さ = max(子の頂点の深さ, 親の頂点の深さ + 1) で更新する。すべての頂点の深さの最大値が答えである(深さ0から始めた時は最大値に1足す)。
コードはこちら。
解説を読むまで、何をどうしてよいか分からなかった問題である。入力例4を解けなかった。
RRRLRLRLL
RRRRRLRLL
RRRRRRRLL
解き方は連続するLかRを反転させれば、反転させた部分の左右の端の人が幸福になる、というものである。もう少し詳しく書くと、 L..LR..RL
の連続するRを反転させると、最右のRと最右のLの人が反転して幸福になる。ただしここで最右のLがない、つまり連続するLと連続するRは合わせて2つしかないときは、最右のRだけが幸福になる。LとRを入れ替えても同様である。一回反転させれば2人幸福になり、連続するLと連続するRの組が2つ減る(ただし最低1)。
得られた教訓と言えば、どこを反転させると最適解なのか分からないが特徴量は分かるということである。入力例を手作業で解けなくても、盤面を記述する特徴量(偶奇とかもその一種)を見つけると、解ける問題がある。
- 一回反転させると連続するLと連続するRの組という特徴量が2減る(最低1)
- 幸福な人の数 = 人数 - 特徴量
コードはこちら
ものすごく久しぶりに、解説ACできない問題に出会った。
直感的な理解は解説を読まずに分かったのだが、考察が進まず実装方法が全く分からなかった。セグメント木でACできず、 他の方のコード を読み解いてようやく理解した。
実装が大変である。値
コードはこちら
優先度キュー std::priority_queue
の使いどころである。優先度キュー std::priority_queue
はデフォルトで値が最も大きい値を取り出すので、最も小さい値を取り出すときは比較演算を反転する。
std::priority_queue<Num, std::vector<Num>, std::greater<Num>> q;
割引券を0枚以上使った後で、最も値段の高い品物に割引券を使うと、最も割引き額が多い。つまり貪欲法で解けばよく、割引後の値段を優先度キューで表現すればよい。
コードはこちら
蟻本を知っていれば、もっとエレガントに解けるらしい。
のだが蟻本を読んでないのでローリングハッシュで解く。L=1..N文字からなる部分文字列について、1..(N+1-L)文字目から始めたものにローリングハッシュが一致するものがあるなら、答えはLである。ローリングハッシュを素数1個で解くとハッシュ衝突が多発してWAするので素数2個にしておくのがよいが、そうするとTLEするのでstd::mapをstd::unordered_mapに変えるとぎりぎりTLEしなくなる。
コードはこちら
素因数分解も頻出である。コードスニペットとして用意してテストしておく。
解法としては、AとBを素因数分解して、AとBに共通の素因数の数である。理由は以下の通りだが、ここまで思いつくに時間が掛かってしまった。
- AとBに共通ではないAまたはBの素因数は公約数にはなれない
- AとBに共通の素因数から同一の素数を含めて複数選んで掛け合わせると、他の約数と素でなくなる
コードはこちら
Bit DP
動的計画法の縦軸に鍵、横軸に宝箱が開いたかどうかのビットパターンを
初期値は鍵を全く使わず、箱が全く開いてないときのコスト
鍵
- 鍵
$i$ をコスト$a_i$ で使い$DP[i-1][p] + a_i$ - 鍵
$i$ は使わず$DP[i][q]$
のうち小さい方である。これをすべての
コードはこちら
這うようにしてAC。
解
まず頂点2の解はすべての辺を調べれば分かる。逆方向の枝があればそれが解である。
それ以外はDFSで探索する。DFSで頂点を一つずつ追加し、解の候補に追加した頂点をたどるならループである。最短のループを切り出して解の条件を満たすかどうか調べ(これが無いと1 WA)、解なら出力する。
最短のループを切り出すのがまさに答えであったと、公式解説を読んで分かった。上記の方法以外に思いつかなかったが、実はBFSで 求まる 。
コードはこちら
変数の順序を固定すると、重複なく数え上げできる。
一般性を失わずに
-
$a \leq b < b + c$ から$a < b + c$ -
$b \leq c < a + c$ から$b < a + c$
std::vector
の要素を指すイテレータが先頭から何番目にあるか(距離 std::vector::begin()
との差として定数時間で求まる。 std::distance()
を使ってもよい。
const auto d = std::max(0LL, static_cast<Num>(
std::lower_bound(nums.begin(), nums.end(), s) - nums.begin())
- second - 1);
コードはこちら
ワーシャルフロイド法だと思って時間を溶かした。制約を深読みし過ぎである。
正解はダイクストラ法である。
出発する町
$D_S = 0$ -
$D_V = D_U + dist(U, V) if \lceil D_U / L \rceil = \lceil (D_U + dist(U, V)) / L \rceil$ 。これは町$U$ で給油せずに$V$ に到達できる場合である。 -
$D_V = L\lceil D_U / L \rceil + dist(U, V) if \lceil D_U / L \rceil < \lceil (D_U + dist(U, V)) / L \rceil$ 。これは町$U$ で給油しないと$V$ に到達できない場合である。
と思ったら解説を読んで、無給油でたどり着ける町同士をコスト1のグラフにして、ワーシャルフロイド法で 解ける と分かった。これは思いつかない。
コードはこちら
実数解を求める問題は、解析解を求めるより、二分探索による絞り込みが有効な場合が多い。
角度を上げる=より多く傾ける程、容器の中身がこぼれるのは明らかなので、単調関数として二分探索できる。水面が底辺と側面のどちらにあるかを場合分けすればよい。
円周率の定義 M_PI
は、もしかしたら下記のマクロとインクルード文が要るかもしれない。
#define _USE_MATH_DEFINES
#include <cmath>
コードはこちら
二分探索である。
何を探索するかが重要なのだが、ここではチーム全体の成績とする。まず自明に言えることとして、最もコストの高い人を最も食べやすいものに割り当てるのが最適である。よってコストの降順、食べにくさの昇順にソートする。ソート後のコストと食べにくさのマッチングを
次にチーム全体の成績
修行する回数が
コードはこちら
二次元DPするには
- ナイトが移動した前も後も、x座標+y座標は3の倍数である。
- なので
$X+Y$ が3の倍数でないマスには到達不能である。 - Xにできるだけ少なく移動するなら毎回+1移動する。このときYは毎回+2移動する。なので
$Y > X * 2$ なら$X$ に達しても$Y$ に達しない。同じく$X > Y * 2$ ならYに達してもXに達しない。どちらも ($X$ ,$Y$ ) に到達不能である。 これに注意しないとWAする。 - 上記を除いた (
$X$ ,$Y$ ) には到達可能である。
- 移動する回数は
$n = (X+Y) / 3$ - 毎回1移動するので、2移動する回数は追加分
$k = min(X,Y) - (X+Y) / 3$ - よって移動する方法は
${}_n \mathrm{C}_k$
コードはこちら
ループの内外が重要である。
最適な答えが求まっていて食べる皿の集合が固定なら、限られた時間では食べるのに掛かる時間が短い皿から食べるのが最適である。よって食べるのに掛かる時間が短い順に、時刻
このときループの内外が重要である。外ループを皿
コードはこちら
126-D, 138-D と同様に、木の頂点をBFSでたどるときに何を載せるかである。今回は辺の色を載せる。
- 無向グラフの辺を
std::vector<std::vector<Num>>
に載せるのはいつも通り - 辺を塗るために、辺の始終点から辺の番号への連想配列を作る。キーは
from + to * (n+1)
にする。 - 頂点の次数(つながっている辺の数)を記録する。最大の次数が必要なの色数である。
- 次数が最大の頂点からBFSする。親頂点とつながっている辺(根を除く)以外の色を、子頂点との辺に塗る。
コードはこちら
落ち着いて境界値を探る。
要素
最初に
-
$U = \sum_{1}^{i} A_i$ とする。累積和なのであらかじめ求めておく。 -
$(\sum_{L}^{L+W} A_i) mod K = 0$ の組み合わせの数は$T[U]$ である。これを答えに足す。 -
$T[U]$ を1減らして、以後$[A_1,...A_i]$ を数えないようにする -
$R \leq N$ なら$V = \sum_{1}^{R+1} A_i$ として、$T[V mod K]$ を1足す。その後$R$ を1増やす。
これらの和が答えである。
コードはこちら
DPを諦めることも重要。
DPで
出目の列が辞書順で最小である、という条件を満たすにはゴールからできるだけ大きな目で飛んで、スタート近くを小さな目で飛べばいい。後ろから考える。
コードはこちら
証言に矛盾が無いときの、正直者の最大値を答える。
コードはこちら
Nim数の発想は、2進数で桁が異なるものを独立に扱うことであった。
-
$A_i$ のビット$pos$ が立っているかどうかを、二次元配列$board[i][pos]$ に入れる -
$A_{1..N}$ のビット$pos$ が立っている個数の合計を、一次元配列${nbits}[pos]$ に入れる
こうすれば
-
$A_i$ のビット$pos$ が0なら、$A_{i+1..N}$ のビット$pos$ が、1である個数 -
$A_i$ のビット$pos$ が1なら、$A_{i+1..N}$ のビット$pos$ が、0である個数 - 2進数で桁に対応する係数
$2^{pos}$
の個数と係数を掛けたものを足すと答えが求まる。
二次元配列としては様々な実装があり、添え字検査が必要なら boost::multi_array
がよい。要素を一括設定できる。
コードはこちら
後は左から右、上から下に向かって動的計画法で、ある差を取りうるかどうか調べる。上と左にセンチネル座標0があると仮定すると場合分けが楽になる。
- 初期値は
$DP[][0..W][0..S-1] = false$ 、ただし差0について$DP[0][1][C] = true$ とする。 - 外ループ
$y=1..H$ 、内ループ$x=1..W$ について、$DP[y][x][i \pm D_{y,x}] = DP[y][x-1][i] \lor DP[y-1][x][i]$ である。ただし$i \pm D_{y,x}$ は範囲外なら無視し、$DP[y]$ の部分は今更新している行と前の行だけ持って、それ以前の行は忘れてよい。
コードはこちら
先頭から見て最初に見つけた1を残し、次に見つけた2を残し... を繰り返す貪欲法で解ける。なぜなら最左の1を残した方が、それより後に出る1を残すより、次の2を残せる余地が高まるからだ。
コードはこちら
10の倍数=2の倍数かつ5の倍数
奇数系列は2の倍数を含まないので答えは0である。
偶数系列は5の倍数かどうかだけ調べる。10未満なら答えは0、10以上なら素因数5の数より素因数2の数が多いので素因数5の数を求める。これは N % 2
と N & 1
はどちらかに統一して混ぜない(どのみちこの除算のコストを気にしても仕方ない)。
コードはこちら
証明が長い。詳しくこちらの記事を参照。
LCA (Lowest Common Ancestor) を使う必要はないが、294-Gで使うので用意しておくとよい。
コードはこちら
一次元DP(動的計画法)を、複数のスロットを用意して更新する。鉄則本の力試し問題 C09: Taro's Vacation と同様である。
ここでは直前にグー、チョキ、パーを出した時の得点を用意する。グー、チョキ、パーそれぞれについて、K回前とは異なる手は2通りあるので、どちらか得点が高い方を選んで、今回の手で得た得点を足す。前述のC09で、前日に勉強したら今日は勉強しない、と同じことをしている。
実装上は以下の点に注意する。
- i回目のジャンケンで、i<Kなら前の手は無いので何を出してもいい
- i mod K回目が同じK通りについて動的計画法を解いて、K通りの得点を足す。なぜならi mod K回目が異なるジャンケンは、出す手が互いに影響しないからである。
コードはこちら
整数を因数分解するときは、因数分解が可能である(整数の積で表現できる)ことだけでなく、因数分解の式が条件を満たすことを確認する。
最小公倍数(LCM)は推移則 std::lcm
と std::gcd
が用意されているので自作する必要はない。
ここまでくれば