TODO
概要 †
- プロコンで考えるべきこと
- 解いた問題数を増やすのは良い事なんだけど、解いた問題数を増やすためだけに、簡単な問題を解きまくるのってすげえ時間の無駄
記録 †
下位ページ †
目次 †
参考 †
勉強すべきこと †
まとめと例題 †
方針 | 説明 | 例題 | DPテーブルを書いてみて、混み合っている場合は無駄な計算がないか考える | | 2-3 漸化式を工夫する, 個数制限なしナップザック問題 | boolのDPは一般に非効率 | | 2-3 漸化式を工夫する, 個数制限付き部分和問題 | DPの双対性 | 「状態に対して評価値がどうなるか」⇔「固定された評価値(=ここでの状態)に対して、最も良い状態(=ここでの評価値)は何か」 | (2-3 漸化式を工夫する, 01ナップサック問題その2, 最長部分増加部分列問題) | 「fの最大化」⇔「命題C(x)=『fがx以上である』が判定可能ならば、xの探索的最大化」 | | 3-2 平均最大化 | n回の掛け算はO(log n)で計算可能 | | 3-1 べき乗を高速に計算する | 漸化式は行列演算に変換可能 | | 3-5 行列累乗 | modはどこでしても同じ | | 3-5 行列累乗, SRM 502 Div2 Hard | 集合を、整数型のビット演算を用いてDPの状態に組み込む | | 3-5 ビットDP | DPは適切なデータ構造を用いると高速化 | | 3-5 データ構造を用いて高速化 | 問題を同値な数学に置き換える | 牛が云々とかいう状態で考えたくない | SRM 502 Div2 Hard | DPのデータ型のパターンはいっぱいある | 6次元配列だろうが何だろうがビビらない | SRM 551 Div2 Hard, 桁DP | 一つ前にしか依存しないDPテーブルは2テーブルを使いまわす | int prev=i%2,next=prev^1; memset(dp[next], 0, sizeof(dp[next])); dpdp[next][(x+i)%N][y-1] += dpdp[prev][x][y];.絶対,dp[next]を初期化すること!!最後のテーブルの添字は毎回きちんと考えなければならない. | SRM 502 Div2 Hard | 最小手数といえば幅優先探索 | | SRM 509 Div2 Hard | A以上B以下のもの | f(n):=n以下の条件を満たす非負整数の総数とすればf(B)-f(A-1) | 桁DP | n進数の不定長データ列の生成はqueueを使う(A, B, C, AA, AB, ...など) | サンプルコード | SRM 682 Div2 Medium | マジックナンバーは消す | バグの温床になる | | 負のDP | const int MAX = 1000; int ary[2*MAX + 1]; int *dp = ary + MAX; // [-MAX, MAX]にアクセスできる! | | 二段DP | | SRM 523 Div2 Hard | DPは問題の制約を気にせず解いて,後でまとめるときに制約を考える | 円卓テーブルで隣り合って同じ色はダメ→同じ色も許容するDPを組んで,後で違う色の場合の数だけ集める | SRM 551 Div2 Hard | 単調増加DPは変化分だけを追うDPに変換する | | Yukicoder 269 | 包除原理 | 全体からある条件f1, f2, f3...を除いた場合の数=S(全\(f1orf2orf3orf4)) = S(全) - S(f1) - S(f2) - S(f3) - S(f4) S(f1&&f2) + S(f1&&f3) + ... - S(f1&&f2&&f3) - S(f1&&f2&&f4) ... + S(f1&&f2&&f3&&f4)。2^条件数で求まる | ABC 3D | 二次元の累積和は、前処理O(n)でO(1)になる | 「0からiのx方向累積和の参照」をの累積和y方向に取ればよい。 | ABC 5D | 「n個ちょうど使ったら」という発想大事 | | ABC 5D | 配るDP&空間を削減するDPの場合、iごとに初期化を忘れない!! | すごく忘れやすい。空間を使いまわしてるので初期化必須。 | ABC 7D | バイナリ状態を表すDPは、「j: ○○が確定している」という表現をする | なぜなら、j bitor (○○)と表現して遷移が書けるから | ABC 7D | 桁のDPは上から見ていく | | ABC 7D | 漸化式の計算は行列累乗と相性がいい | | ABC 9D | 行列演算は半環なら可能 | 要するに掛け算と足し算(*+, AND-OR, AND-XOR)が定義可能 | ABC 9D | 容量1の最大流問題は、同じ道を通らない最大経路数に一致 | | ABC 10D | 最大流最小カット定理 | 容量1の最大流は、最小カットに一致。 | ABC 10D | 容量1の最大流問題は、Ford Fulkersonが速い | | ABC 10D | modと-とif文の相性は最悪 | 何気なく、if (n % 2 == x % 2) continue; とか書くと死 (x負で死)。何気なく、if ((n - m) % 2 == 1) continue; とか書くと死 (n < mで死) | ABC 11D | 100万要素を超えるテーブル・10万頂点を超えるグラフはグローバルに置くかstaticをつける | スタック領域には入らない可能性 | ABC 14D | DPを回す添字の上下には注意、+1になったりする。 | 注意するしかない。あるいは、repを使っているのがいけないのか。 | ABC 15D | dpが0であることと、配れないことは同値ではない | 始め0でも、あとで+bして配ったりするので。 | ABC 15D | DPのデバッグは、iの下で別のループを使って0以外を出力。 | 遷移のループの中ではなく、下に新しいループを作る!また、自明なdpの項は出さない。rep(j, k + 1) rep(h, w + 1) { if (dp[next][j][h]) cout << i << " : " << j << " " << h << " : " << dp[next][j][h] << endl; } | ABC 15C | 一周回るときの漸化式はa[i] - a[(i+1)%n]とmodを使うときれい | | ABC 16D | mod系は、-が含んでいると、答えが-になりえる | | ABC 17D | 半分全列挙強い | 片方が固定されていると、もう片方が楽に取れることがある。マージ則が軽ければいける。今回のように男女に分かれている時もあれば、ナップザックの時みたいに自分で分けることも | ABC 18D | 木の直径はO(V)で計算可能で、二回走査すればよい。 | | ABC 19D | 二分探索の初期値は、upperは必ず条件を満たすもの、lowerは必ず条件を満たさないものを選ぶ。 | 更新はwhile (r - l != 1) { ll mid = (l + r) / 2; if (check(mid)) r = mid; else l = mid; } | ABC 23D | 整数a, b、素数p、a/bが整数の時、a/b (mod p) = a * b ^ (p-2) (mod p) | | ABC 24D | 「配るDP」と「集めるDP」がある。 | 漸化式の可換性があれば「配れる」。(+), (*), min, maxなどは配れる | ABC 25D | 配るDPは、 iで配ったものが必ずその後にしか影響しないならば、順方向に見ていけばOK | トポロジカルソートでも同じことが言える。 | ABC 25D | 配るDPの場合には、0を配ろうとして無駄なので、さっさとcontinueする。 | 高速化のため。集めるDPはそもそもいらないものは見ないので良いが、配るのはそうはいかない。 | ABC 25D | 順序がついているとn!の探索空間になるが、順序をなくすと2^nに落ちる。 | | ABC 25D | bit集合では常に、集合S<集合Sに何か埋めた集合 | | ABC 25D | bit集合の参照は楽なので、ラッパ関数とか作らないほうがわかりやすい | | ABC 25D | DPのループの順番は、依存関係の強いものを左に、弱いものを右に持ってくる。 | | ABC 25D | setは、insert, erase, countをよく使う。 | | ABC 25D | 左右のどちらかだけ埋まっている、という条件は、^演算子を使うとやりやすい。 | | ABC 25D | 誤差系は、型long double, 二分探索40000回、表示40桁あれば絶対安全。 | long long doubleのサイズによる | ABC 26D | long doubleの表示は%Lf, doubleは%lf, floatは%f(物議はあるが) | | ABC 26D | 制約なし(今回は戻ってきて原点)を前提として、問題が解けるかを考えるのは大事 | そもそもDPもそういう発想だし | ABC 27D | A->Bの効果が累積する系は、逆にB->Aの効果の累積となる | 数学的構造はどうなってる?? | ABC 27D。ARC 41C | 左しか影響しない、などの依存性の考察大事 | | ABC 27D | 紙できちんとできることを確認してからやること!!漸化式いじりすぎ。 | | ABC 29D | 間違ってないなら、枝刈り条件を削らない | 残して間違えたとするなら、枝刈り能力が足りないのだ | ABC 29D | 1indexは絶対0indexに!頭がこんがらがってしょうがない。 | | ABC 30D | 3進数の全探索 | rep(i, pow(3, K)) { vector<int> len(K); int tmp = i; rep(j, K) len[j] = tmp % 3, tmp /= 3;} | ABC 31D | ごちゃごちゃしてる時は大きい紙で整理するのと、遠慮なく変数を大量に作る。iとかjとかだとよくわからなくなる。 | | ABC 31D | デバッグ用の添字は_をつけるとかしたほうがいい。もしくは、そもそもデバッグ用のマクロを作るという手もある。 | | ABC 31D | stack, queueでは、探索の辺前後の状態変化可逆性を利用した空間計算量削減ができない | | ABC 31D | 単射な構造は、入出力を逆にした双対問題みたいのものを考えられる。 | 重さ→価値は単射なので、以下の二つの問題はだいたい同じ。「重量jをぴったり使用する時、最大達成価値f」「価値j*をぴったり達成する時、最小使用重量f」 | ABC 32D | INFはconst long long INF = 1e18;などとするのがいい。 | | ABC 32D | lower_boundはval以上の最小の値を指すイテレータ、upper_boundはより大きい最小の値を指すイテレータ。 | | ABC 32D | 以下、より小さいの条件で二分探索する場合は、そもそもsortを降順で行っておく | | ABC 32D | push_backするfor文の条件は、静的なサイズを指定。 | | ABC 33D | EPSは1e-14 | | ABC 33D | atan2は(y, x) | | ABC 33D | complexはデフォルト提供されてる唯一のベクトル | | ABC 33D | 演算子はキャスト * + =の順番で、順番をきちんと意識しないと型間違えて死ぬ。 | double a = 1.0 * a + 2 * a * f; は死 | ABC 33D | 予めどれくらいpush_backするかわかってる場合はreserveすべき。 | | ABC 33D | pairは辞書順ソート | | ABC 33D | Complexはreal, imagのどちらかだけを再設定する、ということはできない | | ABC 33D | 実験大事 | | ARC 51C | 逆元はmodが素数じゃなかったら即刻諦めること! | 頑張っても無駄 | ARC 51C | if (np & 1 == 0)はダメ。bitは()でくくること。 | | ARC 49C | XORの逆演算はXOR! | | ARC 45C | 最小云々はなんと言うかギザギザしてる構造を持っている気がするので、愚直にできなければDPか二分探索を考えるべき | | ARC 44C | x, yとある場合、独立性を判断する。別々に解いていいことがある。 | | ARC 44C | ノードに付帯情報をつけるといいことがある | 例えば、(i, j)の隣のマスは(i, j-h)であるみたいな | ARC 39C | 素数はだいたい1000万まで、だいたい1/10個くらい。(1000万で6.6%) | | ARC 34C | 最短閉路問題は簡単 | (O(V^3): ワーシャルフロイドO(V^3+全点走査O(V)*2点選びO(V^2))閉路に含まれる1点Pを削除することを考え、点Pと辺でつながっていた頂点の中から2点QRを選び、QRの最短経路コスト+辺PQコスト+辺PRコスト、で済む。 | ABC ??? | Mod構造体の^も()つけまくる | 優先順位 | | 嘘解法:大きいllの掛け算が面倒になったら、ldを介してしまう。 | ceill(((ld)t * (ld)x - (ld)r) / ((ld)x - (ld)1) - (ld)1e-14) | | 整数の割り算には注意 | 符号によってガウス記号の上下が変わるから。ちゃんとlgauss, ugaussを使いましょう | ARC 8B | Mod構造体は最小限に | 勝手にmodされては困るケースがある | | 〜してはならない、みたいな条件は面倒なので、だめなのはわかってながらも全部足してから、後で引けないか考える。 | 包除原理もそういうふうになっているはず | ABC17C | 紙に書いて考察せよ | | ABC23C | a+b=nなる非負整数を求めるとき、ループの範囲に注意 | | ABC23C | cout << 1 << endl; return 0;を徹底。 | atcoder, codeforcesでは | | 漸化式の依存方向と、ループの方向は「自然」に一致させる | 自然とは、「後にのみ依存するならi=0->n」「前にのみ依存するならi=n->0」。そうすると、メモリが使いまわせる。一方向にのみ依存するDPなどが使えるし、異常条件も減る | | 逆演算の存在は主に0-indexをi-indexにできる | | | 走査系ははじめに安心安全なprevで初期化しておくと、境界条件が減ってバグが減る | | ARC 16B |
講評 †
問題 | どんな問題か | 理解度 | SRM 502 Div2 Hard | 簡単な動的計画法。メモリ節約法。 | o(解答) | SRM 551 Div2 Hard | 基本の動的計画法。 | o.欲しい情報は定義してしまえ(解答) | SRM 509 Div2 Hard | 簡単め動的計画法、幅探索。 | x. 分かってない。ダイクストラとDPと幅探索と幅探索でいけるっぽい | SRM 682 Div2 Medium | n進数データ列のqueueによる生成 | o. 通った | SRM 682 Div2 Hard | 動的計画法.O(n^4), n=300が愚直だが計算追いつかないので工夫. | x. 計算量が追いつかないところまでは分かったが,どう単純化すればいいか | SRM 547 Div2 Hard | 動的計画法+素数+ちょっと工夫 | x(なんか実装ミスってるっぽい). bit操作、set, 解説読むのをやる | SRM 523 Div2 Hard | 動的計画法+bit集合+2段DP | x, 状態設定ミスった。分からない。解説 |
問題 | どんな問題か | 理解度 | SRM 502 Div2 Medium | 確率と文字列操作 | o(bits/stdc++.hとforallを導入) |
プロコンサイト †
Topcoder †
- div2 Hard は難易度的には div1 medium より簡単なイメージ
- CodeProcessor
- Challenge [#e9ce53e8]
- vectorのチャレンジの仕方
hoge,foo,test
- とすると、vector[3]={hoge, foo, test};となる。
- 変なスペースはつけてはならない。
- 最後にカンマをつけてはならない。
Codeforces †
Atcoder †
問題の見方 †
流れ †
- google翻訳に問題文を突っ込む
- 実験
- 明らかな条件を列挙し、覚えておく。自明な条件をコーナーケースとして把握する。
- 求められる情報量の確認
- yes-noか、max-minか、numかなど。少ない情報ならまとめられる。
- 計算機特有の発想
- 特性関数の作りやすさ。この答え(以上・以下)は問いの答えたりえるか?という質問に簡単に答えられるかを確認する。
- 半分全列挙
- 計算量
- アルゴリズムに要求される計算量のキツさを確認する。
- 頑張って実装
- テスト
- 時間最大セットと、メモリ最大セットと、コーナーケースで通るかを確認する。
チェックリスト †
チェック項目 | 説明 | デバッグメッセージを残すな | | 自前テストしたか | 極端な例(入力の小さいもの大きいもの)、最小の普通なもの、普通なもの、疎密(密粗密なども) | 1行に複数の値を返す時、きちんと最後のスペースを除け | | コーナーケースできちんとcout << 1 << endl; return 0;せよ | Atcoder, Codeforcesなどだと、間違えてreturn ret;とかすると死ぬ | 何もかもlong longにせよ | 掛け算でキャストミスしないために | はじめに書いたコーナーケースを最後にも確認せよ | | 整数同士の掛け算がオーバーフローしないか確認せよ | c[i]*c[i+1]でオーバーフローしえるので注意 | 整数同士の割り算がlgaussかugaussか確認 | intがlgaussになるのは同符号の時だけ | valgrindをかけよ | | 盤面問題で、添字はあっているか確認せよ | 添字のn, mを逆にしたりしてないか? |
コーディング中の注意 †
注意 | 説明 | long longでの演算 | 10^18とかの場合、一回も掛け算してはならない! | 負の%とif文の相性は最悪 | 両方正にして比較すること | Mod構造体のリテラルは必ずキャストすること | ^がxorと判断されたりなど超絶面倒なバグが起きる |
入出力 †
- cinは遅い。
- 30万変数読み込みで、scanfだと50ms, cinだと150ms
- 同期を切ると早くなる
- 切ったらもうcinとscanfを混ぜて使ってはいけない
計算量 †
計算量 | 安全 | 無理 | O(n) | 3000万 | 1億=10^8 | O(n log n) | 100万 | 400万 | O(n^2) | 5000 | 10000 | O(n^3) | 300 | 450 | O(n^4) | 75 | 100 | O(2^n) | 25 | 27 | O(3^n) | 15 | 17 |
- 制約2秒程度だったら、逆にどんな計算量が求められている?
制約 | アルゴリズム | 10^6 | O(n)以下、軽いO(n log n) | 10^5 | O(n log n)以下 | 3000 | O(n^2) | 500 | 軽いO(n^3) | 100 | O(n^3) | 30 | O(2^n)の半分全列挙 | 20 | O(2^n)、O(n 2^n) |
グラフ理論 †
頂点n辺n-1かつ連結なら必ず木構造
木構造はどこから見ても木構造、おぼえましたし
このスライドめっちゃいい
本「離散凸解析の考え方」
http://www.slideshare.net/tmaehara/ss-17402143
なんか関数定義をして、それが凸かどうかを判断して、云々している。とてもよい。
頂点は難しい、枝は簡単
葉を指定した最小全域木→Kruskalの変形
分散最小全域木、比最小全域木などは、パラメータ化して何かを止めると凸にする
k-th最小全域木
辺を共有しない最小全域木を求めよ
名前 | 計算量 | アルゴリズム | 特徴 | ベルマンフォード法 | O(VE) | 前提:負の閉路がない連結グラフ。全辺を見て、現状より辺を通じたほうが良ければ更新。更新するものがなくなるまで行う | 負のコストを許容。負の閉路がなければOK。また、すべての負の閉路の検出も可能(更新が規定回数で止まらなかったら) | ダイクストラ法 | O(ElogV) | 前提:負のコストがない連結グラフ。コスト最小の未確定頂点を選び、そこから行ける頂点を更新して未確定頂点とする。これを未確定頂点がなくなるまで続ける。priority queueを使って実装する場合は「確定」の概念が明示的には入らず、コストが同じなら一番初めに処理した頂点が強いことで弾く | 負のコストを許容しない。 | 経路復元 | O(E)かO(V) | O(V)は以前の道を記憶する。O(E)は以前の道をあとから探す | E: d[j]=d[k]+cost[k][j]なるkを探す。V: 更新時prev[j]を記録する。 | プリム法 | O(ElogV) | 統合頂点群から未統合頂点への最小距離をメモする。未統合頂点のうち統合頂点群に最も近い頂点を探す。統合頂点群に追加する。頂点追加によって、最小距離が変わるので更新する。 | 最も近い頂点を貪欲的に追加する方法 | クラスカル法 | O(ElogV) | 辺をコストの小さい順に並べる。コストの低い順に見て、閉路ができないならば連結していく。閉路ができることは連結先と連結元がUnion-Findで同一かによって検知可能 | 最もコストの小さい辺を貪欲的に選び続ける方法 |
ライブラリ †
アルゴリズムとリンク | 概要 | 計算量 | 二分探索 | 単調関数の境界面をlog(N)で計算。Nは探索空間。 | | Union Find | 「xとyの集合をまとめる」「xとyの集合が同じか判定」分割はできないのが重要な制約 | 構築O(n), union, findは両方O(A(n)) | BFS on tree | 幅優先探索で頂点rを始点とする最短距離を求める。 | O(V) | Bellman Ford | 1点から全点への最小コスト。負の閉路がない連結グラフを入力。あった場合は検出できる。 | O(V E) | Johnson | | 前処理 O(V E). 中間処理 O(V E log V). 後処理 O(V^2). | Warshall Floyd | 全点から全点への最小コスト。 | O(V^3)、だが漸化式が単純なので速い。 | Dijkstra | 1点から全点への最小コスト。コストは正でなければならない。 | O(E log V) | K Shortest | | O(k E log V) | Prim | 最小重み無向全域木=重みつき無向グラフの全域木の中で、重みが最小のものを求める。根は指定する。 | O(E log V) | Kruskal | 最小重み無向全域森=重みつき無向グラフの全域木の中で、重みが最小のものを全て求める。 | O(E log V + A(r)) | Cuninghame-Green | 最小直径無向全域木=重みつき無向グラフの全域木の中で,直径が最小のものを求める | 前処理O(V^3), 端点計算O(VE) | Chu-Liu/Edmond | 最小重み有向全域木=重み付き有向グラフの全域木の中で、重みが最小のものを求める。根は指定する。 | O(VE) | Dreyfus-Wagner | 最小重みシュタイナー木=頂点群Tを通る木の中で、重みが最小のものを求める。 | t=11が限界。時間O(n 3^t + n^2 2^t + n^3).空間計算量 O(n 2^t). | Ford Fulkerson | 有向最大流 | 辺容量が1ならこれ。F(E MAXFLOW) | Edmonds-Karp | 有向最大流 | O(E^2 V) | Dinic | 有向最大流 | O(E V^2) | Goldberg-Tarjan | 有向最大流 | O(V^2 sqrt(E)) | Nagamochi-Ibaraki | 無向グラフ最小カット | O(VE + V log V) | Primal Dual | 最小費用流=費用と容量が定義された有向単純グラフの2頂点と、そこに品物を流す量が与えられた際,流すために掛かる総費用を最小にするためにはどのように輸送するとよいのかを決定する問題 | O(V^2 U C) Uは費用の総和、Cは容量の総和 | Gomory-Hu | 最大流 | O(V MAXFLOW) | 木の高さの最大値 | 木の直径が最小になるような根を計算 | O(E) | Double Sweep=木の直径 | 葉と葉の最大距離を計算 | O(E) | 有向オイラー路存在判定 | すべての辺をちょうど一度通る閉路があるか判定 | O(E) | 無向オイラー路存在判定 | すべての辺をちょうど一度通る閉路があるか判定 | O(E) | 無向中国人郵便配達問題 | | O(o m log n + o^2 2^o), o=18が限度 | 最短ハミルトン路 | すべての頂点をちょうど一度通る閉路を求める。判定もNP困難。 | O(V^2 2^V), V=18が限度。全探索はO(V!)なので少し落ちてる | Fenwick Tree | 配列に、ある要素にvを足す・ある範囲の和を求める、をlog nで計算。 | 構築O(n)、位置和更新O(log n)、範囲和参照O(log n) | Segment Tree(和) | 配列に、ある範囲にvを足す・ある範囲の和を求める、をlog nで計算。Fenwickの上位互換 | 構築O(n)、範囲和更新O(log n)、範囲和参照O(log n)。範囲和のためには遅延評価を実装する必要あり。 | Segment Tree(最小値・最大値。Starry Sky木) | | 範囲和更新O(log n)、範囲最大最小参照O(log n)。範囲和のためには遅延評価を実装する必要あり。 | Segment Tree(加群) | | 範囲一般和更新O(log n)、範囲一般和参照O(log n)。範囲和のためには遅延評価を実装する必要あり。 | Interval Tree | 与えられた点 x を含む半開区間を全列挙, 与えられた半開区間 [a,b) と重なる半開区間を全列挙 | 構築O(n log n).クエリO(log n + k), kはヒット数 | レンジ木 | | | AVL木 | 平衡二分木 | | Splay木 | 平衡二分木 | | 赤黒木 | 平衡二分木 | | Treap木 | 平衡二分木 | | Next Conmination | nCrのループを回す | 前処理O(n log n), 点クエリO(log n + k), 区間クエリO(log n + k)、kはヒット数 | Prime(エラトステネス・区間篩の構築) | エラトステネスを用いて、素因数分解する | 構築O(n log n)、参照O(log n)、素因数分解・約数計算O(log n) | Prime(構築なし) | 2^64までのオンライン素数判定 | O(1)*200 | RMQ | 配列を与えて構築する→範囲クエリを与えると最小値を返すようになる。構築後は変更不能 | 構築O(n log n)、クエリO(log log n) | Linear Programming | | | Tarjan's OLCA | グラフを構築すると、(u, v)の最小共通祖先をO(1)で計算できるようになる | 構築O(E A(V))、参照O(1) | Big Num | 多倍長演算 | | Rational | 分数 | | Mod | 割り算、combinationはmodが素数を前提。 | 法での累乗O(log n)、等差数列和O(log n)、階乗O(n)、掛け算、足し算、割り算を安全に計算 | Geometry | 幾何ライブラリ | 直線・線分・点間の距離、多角形と線分の包含判定、点群の凸包O(n log n)、凸多角形の直線切断O(n) | Interval Tree | | | imos法 | 範囲和更新から累積和構築 | 範囲和O(1), 累積和構築O(n)、構築後累積和参照O(1) | 範囲和 | [i, j)の累積和 | 累積和構築O(n), 範囲和参照O(1) | 二次元範囲和 | 二次元で[i, j)から[h, k)までの累積和 | 累積和構築O(n), 範囲和参照O(1) |
アルゴリズム各論 †
バケット法 †
- バケット法=平方分割=sqrt(n)分木
- 範囲を扱う時に、平方に分割して記憶することで前処理O(n)、各クエリO(sqrt(n))を実現
- 応用力が高く、自分で考える部分が大きい。各バケットが2分探索木やBITを持ってたりする。
- 応用範囲は、バケット法>セグメント木。
- 速度はバケットO(sqrt(n))<セグメント木O(log n)
BIT・セグメント木 †
- 「データ型T, 二項結合演算op、単位元T0」が定義できると、構築O(n), 更新O(log n), 範囲参照O(log n)のデータ型が定義可能
- Fenwickは、「点更新」「範囲参照」
- Segment Treeは、「範囲更新」「範囲参照」(範囲更新はlazyによって実現)
op | T | T0 | max | int | 0 | min | int | inf | (+) | int | 0 | (*) | int | 0 | sort.(++) | [int] | [] | ^(累乗) | int | 1 | and | bool | 1 | or | bool | 0 | xor | bool | 0 | (++) | string | "" | 集合和 | [int] | [] | 集合積 | [int] | 全体集合 | gcd | int | 0 | lcd | int | 1 |
- Fenwick Tree=BIT=Binary Indexed Tree
- セグメント木
- 応用力が高く、自分で考える部分が大きい。
- 平衡処理が必要ないので、計算量・実装量的に有利
- 使う時
- クエリ
- (平面)走査系
- DP加速=漸化式に区間minが入ってるなど
- 具体的には?
op | 使い道 | max, min | RMQ、DP加速 | (+) | DP加速、オイラーツアー |
- 木いろいろ
- オイラーツアー [#e2df2bd8]
- bfsの経路を添え時に直したもの
- 木を列で扱える!
- しかも、各頂点番号が最初に登場するインデックスと最後に登場するインデックスの間が部分木に。
- 上から下への辺の重みも、上から下への経路で無向グラフ辺を足したものとして表現できる。
- セグメント木などのデータ構造を扱えるようになる!
- 部分木のコストを変える・部分木のコストのsum, min, maxを求める。(無向辺のコストを両方向正にして実現)
- 辺(上から下へに限定)のコストを変える・u から v へのパスのコストのsum, min, maxを求める(無向辺のコストを片方1片方-1にして実現)
DAGからフロンティア †
- フロンティア法の長い解説
- DAG
- Directed acyclic graphとは、閉路のない有向グラフ。
- トポロジカルソート
- BDD
- ROBDD
- Create Reduced Ordered Binary Decision Diagram (ROBDD)
- 変数に全順序関係が定義されている&限界まで簡約化されている
- BDDというとROBDDを指すことも。
- 構築は二つのルールを適用し続ければROBDDになる。冗長な接点を全て削除。等価な接点を全て共有。
- BDD 同士の演算 (Family Algebra) and, or, xor, not, ... みたいな色々な BDD 同士の演算ができる,便利
- 組合せ集合(集合族)を表現する ZDD(Zero-Suppressed BDD)
- 「計算爆発お姉さん」の問題が、O(nB)(n: fのビット数、B: BDDの頂点数)
- お姉さん本
- 集合族を表現するZDD
- BDDと違うのは、頂点削除のルールのみ
- BDDは「ここが0でも1でも結果が変わらない→頂点削除」
- ZDDは「ここが1だったらどうやっても結果が0になる→頂点削除」
- 疎な部分集合族を表現するのに適している
- BDD同様、ZDD 同士の演算がそのままできる(Family Algebra)
- フロンティア法
- ZDDはめちゃめちゃメモリ食う(1頂点当たりO(V))
- 実は、フロンティアの連結と次数だけ覚えれば十分
- その他
- 文字列集合を表現する SeqBDD (Sequence BDD)
- 置換の集合を表現する πDD (PiDD, Permutation DD)
メモ化再帰 †
- 返り値をメモするタイプの再帰(異常条件→メモ条件→遷移→メモ→return)
int f(int i, int j) {
if (i < 0 || j < 0 || i >= n || j >= n) return 0; // 入力異常条件
if (memo.count(P(i, j))) return memo[P(i, j)]; // もう答え知ってる
int ret = 0;
rep(d, 4) ret+=f(i+di[d], j+dj[d]);
memo[P(i, j)] = ret;
return ret;
}
- 一度来たところをメモするタイプの再帰(異常条件→メモ条件→メモ→自分→遷移→return)
int f(int i, int j) {
if (i < 0 || j < 0 || i >= n || j >= n) return 0; // 入力異常条件
if (field[i][j] != 'o') return 0; // 探し物じゃない
if (memo.count(P(i, j))) return 0; // もう来てた
memo[P(i, j)] = true; // 次からは来ない
int ret = 0;
ret += point[i][j]; // 自分を取って
rep(d, 4) ret+=f(i+di[d], j+dj[d]); // 近くのを取りに行く
return ret;
}
DP †
- 難しさ
- 分かったつもりで実際に問題をといても、アクセプトされない。
- デバッグしにくいので、しかも何でアクセプトされないかわからない。
DPの漸化式 †
- コツ
- 「i未満の添字を使って」という表現をする
- 「〜が確定している時」という表現をする
DPの種類 †
名前 | 特徴 | できること | 配るDP | 漸化式の関数が可換、かつ漸化式の関数に単位元が存在し、かつ更新前の添字から更新後の添字を計算可能 | 実装が楽。配れないならcontinueできる | 集めるDP | 更新後の添字から更新前の添字を計算可能 | 汎用的 |
前回のみに依存するDP | 空間計算量が抑えられる。DP配列を2だけ用意して偶数奇数で分ければいい。配る前回のみに依存するDPは初期化に注意 | 一方向に依存するDP | 前回のみに依存し、かつ更新式が後ろにのみ依存する場合、添字を前から後ろに回して、空間計算量を更に抑えられる。DP配列を使いまわせるので。 |
DPのデバッグ †
- コツ
- 変更後の状態を
- 別のループで
- 自明でないものだけ出す!
DPはメモ化再帰のループ版 †
- メモ化からの漸化式構築=動的計画法
- 「これまでに得られる〜」をメモ化する
- 幅・深さ優先探索のメモ化のときに保存した状態と、ここで計算するテーブルの状態変数は一致する
- 「ありえない組み合わせ」というものが存在する
- DPは指数計算量探索の合流である
- 探索の「状態」と「合流法則」を抽出したものがDPとなる
- まず指数計算量探索を図示してみる
- 丸の中に状態(=添字や重さや価値)を書いたグラフを書いてみる
- すると、合流を見つけることができる。合流のルール(=その上流すべての情報の集め方)を抽出する
- 状態と合流から自動的にDPが計算できる
- 初期条件は初めが決まっていることも、深いところの拘束条件があることもある
- DPでは深いところしか決まっていないとやりづらい
- 場合の数も、初めに自明に決まる場合の数から始めるとDPが楽になる
何を考えるべきか †
- 1セット
- 状態を適当に定めてみる
- 今の状態から次の状態がどう決まるかを考えてみる
- 次の状態が今の状態から統合されるかを考える
- 何の情報が足りないかを考え、情報を状態に追加する
全探索との対応 †
| グラフ分析 | メモ化探索 | DP | 状態 | ノード | 引数 | DPテーブル添字 | 値 | ノードに付属する値 | 返り値 | DPテーブル | 遷移 | 上から下への操作 | 再帰関数(更新) | 漸化式(代入演算) | 結合法則 | 二つ以上のノードから一つへのリダクション | 再帰関数(結合) | 漸化式(左辺値) | 初期条件 | 初めのノード | 関数呼び出し | 境界条件 |
例:A未満の非負整数を数える †
- 桁DPが入門に非常によいと思う。
- A: N桁の数字
- dp[i][j]: 以下の状態の時の場合の数
- i: 上からi桁目の自然数集合において (i=0, 1, ..., N, inclusive. つまりi=0では空集合を見ている)
- j: A未満であることが確定している (j = 0, 1)
#include <iostream>
#include <string>
#define rep(i, a) for (int i = 0; i < (a); i++)
using namespace std;
const int mod = 1e9 + 7;
int dp[101010][2]; // pos, less
int main() {
string A;
cin >> A;
int n = A.length();
dp[0][0] = 1;
rep (i, n) rep (j, 2) { // DPテーブルの結合則の数 「DPテーブルから決まっていく様子を想像する」というのはこれを思い浮かべること。
int lim = j ? 9 : A[i] - '0';
rep (d, lim + 1) { // DPテーブルの結合則の具体的な計算数。「DPテーブルから決まっていく様子を想像する」というのはこれを思い浮かべること。
(dp[i + 1][j || d < lim] += dp[i][j]) %= mod;
}
// dp[i + 1][0] = dp[i][0]; d[i + 1][1] = (A[i] - '0') * dp[i][0] + 10 * dp[i][1];
// でもいいはず。でも与えられたdによってDPテーブル添字が一意に決まるので、上のようにdで全探索している
}
int ans = 0;
rep (j, 2) (ans += dp[n][j]) %= mod;
cout << ans << endl;
return 0;
}
実装の構造 †
dp[初期状態] = 初期値;
for (状態) {
for (遷移) {
次の状態 = 遷移(状態);
dp[次の状態] = dp[状態];
}
}
// ↓のようにコメントを書いておくとよい
dp[i + 1][k][j] = (dp[i][k][j] //i番目は選ばない
+ dp[i][k - 1][(j - i + N) % N]) % MOD //i番目を選ぶ
初期値 †
- 探索がベースの DP はどこから始めれば全列挙ができるか考えると自然に初期値が定まる
- 初期値を定めることは探索ベースの時の関数呼び出しに相当する!初期値
- 初期値はDPの定義外.(そうじゃないと,空集合に対する場合の数みたいなのって扱われるべき?i=0, つまりAが空集合の時の場合の数が自明になる?A未満であることが確定している(j = 1)、「空集合」の元である自然数の場合の数は?→0A未満であることが確定していない(j = 0)、「空集合」の元である自然数の場合の数は?→1???本当に???みたいな悩み方をすることになる)
- ナップザックのような数え上げの問題では,初期値は0となる.
一方向に依存するDP †
- DPで漸化式が前にのみ依存している=ループを逆に回せば、後にのみ影響するDPを実現可能
- 一方向にのみ依存するDPは、用意する配列を1つだけに抑えることができる=漸化式が後にのみ依存している場合、i+1->iとして同じ配列を使いまわしていい(なぜかというと、DPの材料が必ず新鮮だから)
- d[i+1][j] = d[i][j]+d[i][j+1];
- i: 0 1 2 3 4 5
- 0: 0 2 7 5 3 2
- 1: 2 9 12 17 20 22
累積和 †
- コツ
- n要素配列の累積和なら、n+1個用意して0番目を単位元にする。
- 二次元でも累積和できる。
- imos法
Priority Queue †
- クエリ突っ込みながらソートしたい!という時のため。
- これだけだと、二分探索木(STLのmap)もできるが、これはもっと静的でクエリ解消はできない
- priority_queue
- 何も指定しないと最大がtopになる。普通のソートとは直感が逆なので注意。
priority_queue<ll, vector<int>, greater<int>> q; // topが最小
priority_queue<ll> q; // topが最大
- デバッグ
- 出力は、イテレータが使えないので、コピーして全部取り出さないといけない。auto q_tmp = q; while (!q_tmp.empty()) {cout << q_tmp.top() << " "; q_tmp.pop(); } cout << "#" << endl;
二分探索 †
while (1) {
ll t; cin >> t;
cout << check(t) << endl;
}
Link Cut Tree †
Segment Tree †
- コツ
- 非遅延セグメントツリーは、結合二項演算と単位元だけあればできる。
- Do Use Segment Tree
- 遅延評価
- 永続ってなに?
- セグメントツリー中級に良さそう
しゃくとり法 †
- 「ある区間で特性関数を満たすならば、区間を右に伸ばしても必ず満たす」時、条件を満たす区間をO(n)で全列挙するアルゴリズム
- 例題
Union Find †
FFT †
三角数乗 †
- 2^(n*n)は、三角数乗を順に求めるとO(n), 二分累乗だとO(n log n)?
ビームサーチ †
よく使うデータ構造と関数と注意点早見表 †
名前 | メソッド | 注意 | unordered_map | erase, count | ソートされない。 | map | erase, count | 50000要素でunordered_mapの3倍遅い。比較1, 2 | set | insert, count, erase | | queue | empty, size, front, pop, push | | priority_queue | empty, size, top, pop, push | デフォルトで降順 | stack | empty, size, top, pop, push | | deque | push_back, push_front | 前に挿入できるvector。前に挿入できるので添字が移動する | vector | push_back | vector<bool>は使用禁止 |
命名規則 †
- 添字の逆引きはinvをつける。
- 例えばvector<int> a;に対しては、unordered_map<int, int> ainv;
- int dx={0,0,1,-1}; dy={1,-1,0,0};は、使う添字の名前を借りてdi, djに。
イディオム †
- int a = -1;を異常値とすると、~a==1で正常値となる
- 2で割れるだけ割る: n/(n&-n)
- 一番下に立ってるbitだけを残して0にする: (n&-n)
- 最後に続いている0の数。NLZ(x) = count_bits*1-1)
- 最初に続いている0の数。NTZ(x) = 32 - NLZ*2
|