📖 プログラミングコンテストチャレンジブック [第2版] ~問題解決のアルゴリズム活用力とコーディングテクニックを鍛える~ Kindle Edition
「 蟻本読まないと ABC の D問題に出てくる DP が分かんないのよ」
蟻本買った( ゚ー゚) この本読んでそこら辺の着色コーダーと同じ道具が手に入るなら あまりに楽な話だが( ゚ー゚) pic.twitter.com/v5uMwri5DW
— むずでょ@きふわらべ第31回世界コンピュータ将棋選手権一次予選31位 (@muzudho1) June 14, 2021
📖 「プログラミングコンテストチャレンジブック第2版」サポートサイト
「 アルゴリズムに著作権は無いから バンバン ブログに書いてもいいと思うんだが、どんなパンチが飛んでくるか分からないんで 手元でこっそり動かすかだぜ」
📖 コードテスト(C++入門 AtCoder Programming Guide for beginners (APG4b))
「 👆 コードテストって、練習に勝手に使っていいのかだぜ?」
「 お父んみたいなやつがマジョリティーになって社会問題になったら AtCoder も腰を上げるだろ」
「 蟻本のコードは G++ だから APG4b のサンプルの GCC と違うのな。
ちょっと読み替えないといけないな」
「 理解しようとして手が止まってしまうよりは、コードを真似て動かして進もうぜ?」
「 C++の for文って 打鍵量多いから やっぱ rep(i, N)
だよな」
Test1:
// 実行時間とメモリ:
ms KB
Test2:
// 実行時間とメモリ:
ms KB
#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)
// 制約
const int MAX_N = 100;
// 入力
int n, a[MAX_N];
void solve() {
}
int main() {
// 標準入力より入力
cin >> n;
rep(i, n) {
cin >> a[i];
}
solve();
}
「 この蟻本、 POJ からの引用なのだったら、わたしが引用してもいいのでは?」
「 👆 大学なのか。
中国で著作権の取り扱いがどうなってんのか分かんね」
// 抜粋
minT = max(minT, min(x[i], L-x[i]));
「 👆 最小の時間を計算、と言ってるのに なんで min( )
ではなく max( )
なの?」
「 👆 全長が 5 で、蟻が 3 に居れば、
蟻の左側の長さは 3 で、右側は L - 3 だな」
「 👆 min を取ると 長さが短い方を取るのは わかるのよ」
「 時間がかかって足が引っ張ってしまう方を取るから 上図の上のバーね」
「 最小の時間を取るという話しをしているのに、なんで 時間がかかる方を取るんだぜ?」
「 部分としては max 関数を使って 時間がかかる方を取る場面もあるということだな」
xより小さければ、xはあるなら半分より後ろにある
「 日本語はワケ分かんないが、二分探索は知ってるんで あとで説明するぜ」
kの中央の数字
「 👆 例えば、数字の4を折り曲げれると思ってくれだぜ。
このとき、折り目は2だぜ。この2を 中央の数字
と呼んでいる」
「 でも 数列はバラバラよ。例えば 2, 3, 9, 11, 105
とか」
「 👆 そのような数列でもイケる。昇順か、降順に並んでさえいれば」
偶数個の場合
「 👆 数字が偶数個の場合、長さを2で割ると 0.5 余るから切り捨てて、
下の方が短めの、真ん中から 0.5 ずれたところが 折り目になるな」
「 👆 x
というのは探したいものだぜ。
棒の上にあるかもしれないし、ないかもしれない」
xより小さければ、xはあるなら半分より後ろにある
↓
xより 「中央の数字が」 小さければ、xはあるなら 「半分」 より後ろにある
「 👆 半分
とか 中央の数字
とか ワケ分かんないんで、
折り目と、折り目んとこにある数字 と考えろだぜ」
「 👆 例えば xが100だったら、上図の棒には x は無いぜ」
「 👆 log
の右下に小さく 倍々の速度を書くのが本来だぜ。この右下の数字を 底(てい)と呼ぶ。
競技プログラミングでは 底は 2 を使うんで 省略しているそうだぜ」
「 👆 緑色の数は 2倍、2倍で増えていっているが、 log2 緑色の数
と書くと、赤色の数 が出てくる表引きだと思ってくれだぜ」
「 👆 二分探索の動きを絵にしたぜ。同じことを3回してるよな」
「 👆 長さが8のとき、3回折ったら xがあるなら、xを見つけてるだろ」
「 👆 この茶番は確かに 切り上げ なのよ。 xが折り目にくるまで 3回かかるわ」
ソートにO(n log n)時間
「 👆 並び替える必要がないときはすぐ終わるし、最悪のケースでは たくさん 操作があるぜ」
「 ライブラリなどに実装されている、配列の要素の並び替えの操作にかかる時間のオーダーは 最悪のケースで O(n log n)
と言われているんだぜ」
「 👆 n って要素数だから、1つの要素ごとに log n の計算量をさばいているということだぜ。
最悪のケースで」
「 n log n
の左側の n の意味としては、要素数が増えれば それだけ単純な掛け算として 時間がかかりますよ、ということだぜ。
右側の n は 対数時間だな。 対数時間というのは 指数時間の逆のようなものだぜ。さっきやった」
「 もっと時間のかかるソートもあるが、今どきのプログラム言語でソートといえば O(n log n)
でしょ、ということだぜ」
ループにO(n^3 log n)時間
「 最悪のケースで、全件探索を3回しながら log n の計算量の処理をするということだぜ」
「 👆 n=3
が 3個あるのを、例えば上図のように描いてみようぜ?」
「 👆 例えば 住所に例えれば 0県0市 みたいに階層になっていると考えろだぜ」
「 👆 3階層あれば 0県0市0区だな……、あれ? なんか多くね?」
「 お父んが勝手に 0 から数を振ってるから、n は 3 ではなくて、実際には 4 だな」
「 このように、 1重ループなら n、 2重ループなら n×n、 3重ループなら n×n×n だぜ」
8ms --> 8ms
408KB --> 336KB
7ms --> 6ms
452KB --> 352KB
// rep(d, n) {
// if (k[a] + k[b] + k[c] + k[d] == m) {
↓
// if (binary_search(m - k[a] - k[b] - k[c])) {
「 👆 何でこれが 等しいのか 説明読んでも分かんないんだけど?」
「 👆 長さが1,3、5 の棒が いくつでもあるとき、 4本使って 長さ10 にしたいとしよう」
「 👆 例えば ピッタリ行くパターンはこれだぜ。 他にもあるな」
「 それがなんで m から Ka、Kb、Kc を引いていく 引き算になるの?」
「 👆 この Kd
のループを、 O(n)
から O(log n)
に縮めたいんだぜ」
「 長さが 5 の棒と、バイナリーサーチの 何が関係あるんだぜ?」
「 3本ぐらい バイナリーサーチしても おいしくないんじゃないかだぜ?」
制約
1 <= n <= 50
「 👆 3本が 50本に増えても まだ大したことないかもしれないが、 1000本に増えると ダメらしいぜ」
3
10
1 3 5
「 👆 どうやって n=1000 のケース作んの? 大変よ?」
#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)
int main() {
cout << "3" << endl;
cout << "4000" << endl;
rep(i, 1000) {
cout << i << " ";
}
cout << endl;
}
「 👆 コードテストが目の前にあるんで使おうぜ? テスト考えるの大変だけど」
#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)
int main() {
/*
cout << "3" << endl;
cout << "4000" << endl;
rep(i, 500) {
cout << i << " ";
}
// */
// /*
rep(i, 500) {
cout << i + 501 << " ";
}
// */
cout << endl;
}
「 n=3 を n=1000 に変えないとダメなんじゃないの?」
// 制約
const int MAX_N = 50;
// 入力
int n, m, k[MAX_N];
「 👆 あれっ? 配列のサイズをオーバーするエラーになるんじゃないのかだぜ?
これでエラーにならなかった理由は不明。 MAX_N を 1000 にしたろ」
「 ループが3つで O(n^3)
、 バイナリーサーチ1つで O(n log n)
として、 O(n^3 log n)
だな」
「 蟻本の想定しているCPUって何だぜ? Ryzen Threadripper 12 Core 使ってるから O(n^3 log n)
の計算 n=1000
で 早く終わってしまったのかだぜ?」
「 いや、お父んが コピー貼り付けで MAX_N = 50 に戻したからだぜ」
「 👆 O(n^3 log n)
の計算ではまだ遅いんで、 O(n^2 log n)
にしようぜ、という話しだぜ」
「 👆 Kc
と Kd
のパターンを1つにしてしまうんだぜ」
「 👆 main 関数で dfs(0, 0)
と書いているな。これを上図で示した」
「 👆 で、 sum が増えない出口と、 sum が増える出口 の計2つの出口があるんだぜ」
「 👆 13 は、 +2、 +4、 +7 で作れるみたいだな」
「 👆 戻るときは True か False を返してこいだぜ。 True が優先」
「 👆 DFS(Depth-First Search; 深さ優先探索)だから、上図のように潜ったり浮かんだりするんだろうけど」
「 コンピューター囲碁の連(れん)を見つけるアルゴリズムだ」
「 これって DFS(Depth-First Search; 深さ優先探索)だったの?」
「 そんな分類なんだな。 お部屋のお掃除 ぐらいにしか思ってなかったぜ」
「 その捉え方だと ロジックが弱いから 類型問題の連想に結びつかないんじゃないか?」
「 👆 これにも名前が付いているのかだぜ?
横が m サイズ、 楯が n サイズの盤を 全マス スキャンするんだぜ」
「 特に呼び名は内容ねぇ。 強いて言えば 2重ループかしら」
「 蟻本のサンプルは 縦方向の2重ループだけど、
漏れなく全マス見るようね。 オーダーは O(N × M)
」
「 👆 蟻本では 8方向と言っているが、9マス調べているぜ。
調べ終わったマスは .
(ドット)で塗りつぶしていくから、
それでも 同じマスを何度も調べるような不具合は出ないんだな」
状態を探索します。
「 👆 こいつ(状態;State)だぜ。
探索(Search) という言葉が難しければ、何かを探して状態を訪問していることだと思えだぜ」
「 👆 蟻本の日本語難しくて分からないんで、幅優先探索なら知ってるんで 絵にして分かりやすく説明したろ」
「 他の人は お父んが分かりやすいように説明されると 分からないんだけどな」
「 👆 ハムスターの4方向を 右側から時計回りに光が回転して、
床のパネルは浮き上がって 筒に入っていくんだぜ」
「 👆 キューに入っていたものは捨てて、新しい床を入れろだぜ」
「 👆 あとは 繰り返しだな。
ハムが 移動のターン数を床に書いていってるんで、階段に着いたときには 移動の最小ターン数が分かるな」
「 👆 読取り方向は さきに x で、 次に y だから……」
rep(j, N) {
rep(i, M) { // 内側がx軸
cin >> maze[i][j]; // (x,y)
}
}
「 i, j
じゃなくて x, y
を使っちゃいけないの?」
「 ループカウンタは i, j, k...
を使うのが習慣だが」
「 👆 すると、On/Off や 並び は、いろんなパターンがあるな。
このパターンが 解候補 だぜ」
「 👆 枝を増やせば、 On/Off だけじゃなくて、半分点灯 とかも いけるわね」
「 さっぱり分からん。 ソースコード実装して 動かしてみよ」
「 next_permutation()
を使わない場合と、使う場合の2種類のソースコードがあるんだけど」
0 1 2
0 2 1
1 0 2
1 2 0
2 0 1
2 1 0
「 👆 next_permutation()
を使わないソースコードで、 3 を入力した結果が 上記だぜ」
0 1 2
0 2 1
1 0 2
1 2 0
2 0 1
2 1 0
「 👆 next_permutation()
を使ったソースコードで、 3 を入力した結果が 上記だぜ」
int perm2[3];
int n = 3;
do {
// ここでperm2を利用
} while(next_permutation(perm2, perm2 + n))
「 👆 next_permutation( ) の引数に何を渡しているの?」
Program:
// [コードテスト](https://atcoder.jp/contests/APG4b/custom_test)
// C++(GCC 9.2.1)
#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)
#include <algorithm>
const int N = 10;
int main() {
int array[N] = {1, 2, 3, 4, 5, 6, 7, 8 ,9, 10};
int begin = 3;
int end = 6;
do {
// 表示
rep(i, N) {
cout << array[i] << " ";
}
cout << endl;
// arrayの指定区間をスワップする。辞書順で現在より後ろのうち、最小のものへ
} while (next_permutation(array + begin, array + end));
}
Output:
1 2 3 4 5 6 7 8 9 10
1 2 3 4 6 5 7 8 9 10
1 2 3 5 4 6 7 8 9 10
1 2 3 5 6 4 7 8 9 10
1 2 3 6 4 5 7 8 9 10
1 2 3 6 5 4 7 8 9 10
「 👆 next_permutation 関数は、この範囲の中の要素を スワップ(入替え)するんだぜ」
「 4と5と6を使った3桁の数で、456より大きい数のうち、一番小さな数だぜ」
「 next_permutation( )
(次の順列) ねえ」
「 👆 もう大きくならないなら、何もスワップせず false
を返すぜ。 whileループでも抜けろだぜ」
int t = min(A / V[i], C[i]); // コインiを使う枚数
「 👆 コインを持っている枚数は決まっているからな。右側は 出せるコインの枚数の最大値だぜ」
「 左側は 残額を コインの額で割って 小数点切り捨て。
残額が900円なら、500円玉を2枚もっていても、ぴったり合わせるには1枚しか使えないな」
「 右側のコインの枚数は、左側で3枚とか出ても、出せませんよ、という意味だぜ」
「 こんな min を使ったプログラム 制限時間の中で とっさに書けないぜ」
「 👆 そして 仕事のスタートとエンドは 被っていることもあるぜ。
この図で選べる仕事は1つだけだな。2つは選べない」
「 こりゃ むずかしいな……。 P43 の問題から証明しようぜ?」
「 水色のブロックを使ったら、緑色のブロックを使ったときより隙間が小さくなって、もう緑色のブロックが入らなくなったからだな」
「 欲張っていいときと、欲張ってはダメなときの違いを教えてくれだぜ」
「 👆 それは、ブロックが小さいもの順に並んでいるときの、隣り合うブロック2つに着目しろだぜ」
「 水色のブロックが緑色のブロックより大きいのは分かっているとして、
水色のブロック1つは、 緑色のブロック2つでカバーできる面積に 2 足りないわけだが……」
「 👆 緑の左隣の小さなブロック 2つないと カバーできないだろ」
「 この はみ出たところをカバーするのに、2つかかってしまった、というのが いけない」
「 1枚でカバーできたって、すぐには 良いとは言えないんじゃない?」
「 👆 ワラちゃん、黒い点線の枠を 一番少ない数のブロックで埋めなさい」
「 👆 じゃあ 赤色のブロックが品切れのときも やってくれだぜ」
「 欲張っていいときと、欲張ってはダメなときの違いを教えてくれだぜ」
「 👆 例えば 隣り合うブロックが 1つずつ違う大きさのとき……」
「 👆 どこでもいいんで、連続する3つのブロックを選んでくれだぜ」
「 👆 そして 真ん中のブロックを積み上げて、背の高い方と 比べてみてくれだぜ」
「 それだと もっと いろいろなセーフのパターンもいけそうねぇ」
「 👆 j
はどこか適当に選ぶとして、 j+1
って、 j の右隣だよな。
この回りくどい言い方は、どこでも 言いハマる、と言いたいんだぜ」
「 👆 δ
(デルタ)は、a[j]
より左の、いずれかの a だな」
「 いずれかのa と言われたら、あいまいで 落ち着かないぜ」
「 どこでもいいということは、両端の 最小のa とか、 最大のa とかにしろだぜ」
「 数式は どうとでも当てはまるから、極端なケースを入れてやれだぜ」
「 👆 赤いブロックは十分あるとして、緑色の点線の枠を埋めろだぜ」
「 aは合ってるだろ。jも合ってるだろ。j+1も合ってるだろ。pも合ってるだろ。H(x)も合ってるだろ。
怪しいのは δ だぜ」
0 ≦ δ < a[j]
「 👆 δ のとこにブロック1個埋めておわりなら 左の柱がブロック2個、右の柱がブロック2個なんで トントンだな」
「 a[j]
側の同じブロックの数から1個引いたものより、
δ側をカバーするブロックの数は 同じか、より少なくあって欲しいようだな」
「 ブロックは小さな順に並んでいるというが、同じサイズのブロックが連続していてもいいのかだぜ?」
「 👆 jより1つ前のブロックではδからはみ出るけど、2つ前のブロックなら埋めれるとき、なんで いけないの?」
「 いけなくはないぜ。 H(δ)
は、貪欲法としか指定がないんで」
「 図解で 面積は描けるが、 ブロックの個数であることを強調することは 視覚的な図形で 描けないしな……、
何かいい方法は無いものか」
📖 別の記事
Crieitは個人で開発中です。
興味がある方は是非記事の投稿をお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください!