cafelier のSRM参加記録です。コンテスト中に考えてたことを執拗に全部書き残すとどうなるだろうかという試み本番中にこういうコードが書きたかったなあ、という後で書いた反省コードを書き残す試み
スパムが来たのでしばらくコメント欄をはてなユーザ限定にしています、すみません、
みなさま二分探索ってどういう風に考えながらコードを書くのでしょうか、ということがちょっと気になりました。特に実数ではなくて整数のもの。問題:
private int X = 1 以上 9_9999_9999 以下の秘密の整数; bool query(int x) { return X <= x; }
X を直接読まずに query() 関数を何回か呼び出すことで X を求めなさい。
自分の場合、こう考えながら書いています。
int solve() { int L = 0; // 条件を満たさないとわかっている数を適当に int R = 10_0000_0000; // 条件を満たすとわかっている数を適当に // 不変条件: 「常に答えは半開区間 (L, R] の範囲にある」
以下、意地でもこの条件を崩さないことだけを念頭においてコーディング。
以下、意地でもこの条件を崩さないことだけを念頭においてコーディング。(重要なので二回)
while( R-L > 1 ) // 答えの範囲が整数一つになるまで繰り返し { int C = (L+R)/2; // 二分探索なので真ん中をとる
(query(C) ? R : L) = C;
// (query(C) ? L : R) = C; ではない。(L,R] が答えの範囲なので R が条件満たす方、L が満たさない方
}
return R; // 答えは (L,R] にあるのだから L じゃなくて R が答え }
Topcoderにサブミットした二分探索のコード全部に半開区間の不変条件がコメントで書いてあります。これを念じていないと「ループ終了条件」「どっちの場合にLとRのどっちを更新するんだっけ?」「最後に返す値はどれ?」の3カ所で、USB端子の差し込み方向並に毎回迷ってしまうので。
チャレンジフェーズで他の人のコードを見ていると、違う考え方でコード書いている人が結構多いようなので興味が湧いています。if(query(C)) R=C; else L=C+1; みたいな形の更新式になるコードとか。気になります。
300に泥はまりしすぎて時間かけられなかったせいで焦りすぎて変なtypoを直せず…終了直前にサンプル通せたコードがそのままpractice通ったので無念です。以下ちょっと整理したもの
言語というかライブラリによって意外と違うのが標準入出力の速度で、入力サイズ N の線形オーダ O(N) で解かないといけない類の問題では、注意しないとデータを読み込むだけでタイムアウトするのも日常茶飯事です。その辺の心配はないのか調べてみました(今までDでそういう問題解いたことがなかったのでした…)。以下、
をとった結果です(単位は秒)。最適化ありというのは g++ -O3 と dmd -O -release -inline で、なしというのは引数なし。
プログラム | 最適化あり | 最適化なし |
---|---|---|
(1) C++ iostream (手抜き) | 5.224 | 5.428 |
(2) C++ iostream (真面目) | 1.119 | 1.155 |
(3) C++ scanf | 0.499 | 0.507 |
(4) D readln+split+to!int | 1.078 | 1.529 |
(5) D readln+split+map!(to!int) | 1.072 | 1.545 |
(6) D readf | 1.958 | 2.335 |
(7) D scanf | 1.393 | 1.410 |
なにかの優劣比較をしたいわけではないので、まったくひとかけらも厳密なベンチマークではありませんが、結論としては、まあ readln + split で問題ないのではないでしょうか、と思いました。というか、だいたいこんなオーダなのでOnline JudgeにD言語を入れる人は参考にしていただければという感じです。あと iostream が遅くてコンテストで困るという人はまずは tie(0) と sync_with_stdio(false) を (定期発言)。
// (1) C++ iostream (手抜き) #include <iostream> using namespace std; int main() { long long total = 0; for(int a,b,c; cin>>a>>b>>c; ) total += a+b+c; cout << total << endl; }
// (2) C++ iostream (真面目) #include <iostream> using namespace std; int main() { cin.tie(0); ios_base::sync_with_stdio(false); long long total = 0; for(int a,b,c; cin>>a>>b>>c; ) total += a+b+c; cout << total << endl; }
// (3) C++ scanf #include <cstdio> using namespace std; int main() { long long total = 0; for(int a,b,c; scanf("%d%d%d",&a,&b,&c)==3; ) total += a+b+c; printf("%lld\n", total); }
// (4) D readln + split + to!int import std.array; import std.conv; import std.stdio; void main() { long total = 0; for(string s; (s=readln()).length; ) { auto ss = s.split(); int a = ss[0].to!int(); int b = ss[1].to!int(); int c = ss[2].to!int(); total += a+b+c; } writeln(total); }
// (5) D readln + split + map!(to!int) import std.algorithm; import std.array; import std.conv; import std.stdio; void main() { long total = 0; for(string s; (s=readln()).length; ) { auto ss = s.split().map!(to!int); total += ss[0]+ss[1]+ss[2]; } writeln(total); }
// (6) D readf import std.array; import std.conv; import std.stdio; void main() { long total = 0; for(int a,b,c; readf("%d %d %d\n",&a,&b,&c); ) total += a+b+c; writeln(total); }
// (7) D scanf import std.c.stdio; void main() { long total = 0; for(int a,b,c; scanf("%d%d%d\n",&a,&b,&c)==3; ) total += a+b+c; printf("%lld\n", total); }
presented by cafelier/k.inaba under
ほげ2012/07/04 00:51自分は while(L<R) で回して if(query(C)) R=C; else L=C+1; の更新式でした。
閉区間[L,R]で考えて、query(C) == false の時は C を閉区間から追い出して良いので次の範囲は[C+1,R]になる感じです。
ループ終了条件と返す値は分かり易いのですが、条件を満たす側が逆な場合等に丸め方向と更新式を変える必要があるのがネックでした。
半開区間の方が扱い易そうですね。
cafelier2012/07/04 02:21コメントありがとうございます。
> query(C) == false の時は C を閉区間から追い出し
こういう考え方を知りたかったのです!なるほど、閉区間で書くイメージもできてきました。
どのやり方も一長一短あるとは思うのですが、特にTopcoderで他人のコードを読むときに、自分のスタイルと違うのを見て詰まってしまう自分が勿体ないなあと思いまして。
cafelier2012/07/22 18:09「閉区間 & trueでもfalseでもmidを常に追い出す」という方針を教えていただいた。なるほど。
https://twitter.com/nabesan_tofu/status/226959241632690178
https://twitter.com/nabesan_tofu/status/226964459439136768