競技プログラミングでC++を書くときに意識していること
Table of Contents
この記事は、Competitive Programming (1) Advent Calendar 2020 - Adventarの18日目の記事です。
はじめに
本格的に競技プログラミングをはじめてから3年近く経ったので、実装の際に意識していることについてまとめることにしました。 自分はどちらかというと競技プログラミングに特化した方法というよりはある程度汎用的に使えるような手法を模索してきたつもりですが、その中でも競技プログラミングでC++を書く際に意識していることに限定して書いていきます。
以下参考資料です。 方針が違う部分もあると思うので、複数の記事などを参考にそれぞれのコーディングスタイルを確立して貰えればと思います。
自分が最も重視しているのは、バグの沼にハマるのを防ぐために、多少時間をかけてでもバグを埋め込みにくい&バグってもデバッグしやすいコードを書くことです。 以下、そのために自分が取り入れている手法を紹介していきます。
★★★:かなりおすすめ ★★:やった方が多少良さそう ★:個人の好みの域かも
コードはできるだけ簡潔に!(★★★)
- 問題自体の考察が終わっても実装に移る前に実装の方針をまとめる
- コピペしようとしていたら要注意!同じ操作はできるだけ
for
文で書けないか考える- 逆から同じ操作するときは2回ループ回して
std::reverse
するなど - 入力がa,b,cなどであっても
std::vector
を検討
- 逆から同じ操作するときは2回ループ回して
- 番兵などを適切に用いて境界の場合分けを減らす
- できるだけC++の機能を使う
std::accumulate
、std::find
など- この記事に一度目を通しておくとよい 競技プログラミングとC++のアレコレ - koturnの日記
- 簡単な処理はできるだけ短く書くと、アルゴリズム的に本質的な部分がわかりやすくなる
- コンテストなどで複雑な実装になってしまった場合は他人のコードなどを読んでみると勉強になる。個人的にはrisujirohさんやarmeriaさんのコードがおすすめです
フォーマッタを使う(★★★)
- 見やすくなるのももちろんだが、コーディング中にスタイルに関して脳を全く使わなくて良くなるのが大きい
- 別の記事参照
未初期化の変数を作らない(★★)
- 気づきにくいバグの原因になるので避ける
- 初期化する値が特にない場合は
-1
などありえない値に初期化しておいてassert
する - 意見が分かれるかもしれないが、
int x; if (flag) { x = -1; } else { x = 1; }
よりは
c++ int x = flag ? -1 : 1;
- C++の入力は独特なので、
std::cin
のwrapperの中に未初期化の変数宣言を押し付ける#include <iostream> #include <string> struct in { template <class T> operator T() { T t; std::cin >> t; return t; } }; int main() { int x = in(); std::string s = in(); std::cout << x << " " << s << std::endl; }
input関数、関数返り値でtemplateが推論されないのが面倒で使ってなかったけどキャスト演算子でいけることに気づいてしまった
— けんしん (@knshnb) November 24, 2019
struct input {
input(){};
template <class T> operator T() {
T t;
cin >> t;
return t;
}
};
int main() {
Int n = input();
}
- どちらも単純な処理がより短い行数で書けるという副次効果もある
変数のスコープは小さく(★★)
- グローバル変数はできるだけ使わない
- マルチテストケースなどでバグらせやすい
- 定数倍高速化でグローバル静的配列などを使うのは仕方ないが、一旦普通にコーディングしてテストした後に逐次的に変更していくのが望ましい
- 再帰は
auto dfs = [&g](auto&& self, int v, int prv) -> void { for (int s : g[v]) { if (s == prv) continue; self(self, s, v); } };
- ラムダ式の参照キャプチャはすべての変数が見れてしまうので、丁寧にコーディングするときは変数を指定してキャプチャする
ローカルの実行環境(★★★)
- g++の
-include
オプションでローカルで常に読み込むファイルを指定できる- 例えば、提出ファイルには
#ifndef dump #define dump(...) #endif
dump
マクロで手元のみデバッグ出力するといったことができる - 例えば、提出ファイルには
- 他には、
-O0 -std=c++17 -Wshadow -Wall -Wno-sign-compare -D_GLIBCXX_DEBUG
といったオプションを付けている。これは環境や個人の好みにも依存すると思うが、配列外参照などを検知してくれる-D_GLIBCXX_DEBUG
の優先度は高そう - online-judge-toolsによるサンプル実行の自動化などは別記事参照
巨大な生配列ではなく、std::vectorで使いたい丁度のサイズを確保する(★★)
- 思考停止でサイズが決定できることよりも、想定外の挙動の検知しやすさを優先
- 0がたくさんついた生の数字よりも
n
などの方が間違えにくい
便利マクロ・関数など(★★)
-
単純なループのときにfor文を手打ちするのはややリスクがあると思う。2引数と3引数を両方扱えるREPマクロが便利
#include <iostream> #define REP_(i, a_, b_, a, b, ...) for (int i = (a), lim##i = (b); i < lim##i; i++) #define REP(i, ...) REP_(i, __VA_ARGS__, __VA_ARGS__, 0, __VA_ARGS__) int main() { REP(i, 5) std::cout << i << std::endl; REP(i, 10, 13) std::cout << i << std::endl; }
- 慣れると
for
文に比べて可読性も上がる。人によって命名が違うので定義見に行かないといけないのがやや面倒だが…
- 慣れると
-
入出力高速化。標準入力と標準出力の同期やCの入出力とC++の入出力の同期を切ったりする。AtCoderでは少ないが、Codeforcesではこれをせずに
std::cin
,std::cout
を使うとTLEすることがたまにあるstd::cin.tie(nullptr), std::ios::sync_with_stdio(false);
-
以下は便利なので使用している
template <class T> bool chmin(T& a, const T& b) { return a > b ? a = b, true : false; } template <class T> bool chmax(T& a, const T& b) { return a < b ? a = b, true : false; } template <class T> using V = std::vector<T>;
その他宗教(★)
- 可読性のため、テンプレートはできるだけ短くしてsnippetなどエディタの機能を使う
#define int long long
しない- どうやらgccでは未定義動作ではないようだが、他人にコードを見せる際などにあまりに紛らわしいわりにテンプレートとして入れておくメリットは少ない
- Topcoderではコンパイルが通らなかった気がする
- 代わりに、
using Int = long long
としてInt
の型を簡単に変えられるようにしている
using namespace std;
しない- 実装が数秒遅くなるというデメリットがあるが、現実世界とのバランスをとって(競プロにおいては)妥協している
終わりです。 そういえば、この記事は競技プログラマのためのC++作法がそこそこアクセスされていることに対する反省で書きました。