競技プログラミングでの愚かな失敗
競技プログラミングの記事なので,別アカウントから移動してきました (2020/07/25)
競技プログラミングで,よく間違う.C++の言語仕様をうっかりして,ということもあるし,そもそもしょうもない間違いということもある.もちろん愚かだから間違うのだけれど,なるべくなら同じ間違いを何度もしないように,メモをしておく.
- 最小値を求めるループの初期値
- 入力を最後まで読まない
- 31以上のビットシフト
- accumulate (に限らないが...)
- int vs long long
- これも int vs long long
- long long でも足りない...
- long long を過信しない
- 0-indexed と 1-indexed
- ベクトルの値の設定: at と push_back
- 添字の範囲
- ループの回数
- 座標圧縮
- 無駄な入力値
- 領域確保にも時間はかかる
- 小数点以下の桁数
以下常に,次のように書いてあるものとする.
using namespace std; typedef long long int ll;
最小値を求めるループの初期値
vec の最小値 vMin を求めるループを次のように書いて失敗した.
vector<ll> vec; ... ll vMin = 1e9; for (ll v : vec) vMin = min(vMin, v);
vec の中の値がすべて $10^9$ より大きかったからである.もちろん注意力が足りないのではあるが,次のように書く癖をつけておけば良かった.
ll vMin = LLONG_MAX;
かと思えば,別の問題では,
x = LLONG_MIN;
...
x += y;
のようなコードを書いて,$y < 0$ だったためにオーバーフローさせてしまった.やはり制約を熟読するのが大事ということか....
入力を最後まで読まない
数の列が与えられる問題で,列の途中まで読むと答が決まってしまい,後が不要なことがある. つい,次のように書いてしまう.
for (int i = 0; i < N; i++) { int x; cin >> x; if (some_condition(x)) { cout << "-1\n"; return 0; } v.at(i) = x; ... }
テストケースが1つなら別に問題ないのだが,Codeforces のように複数のテストケースが与えられる問題で,全体を次のように書いているのに,solve() の中で上のコードを書いてしまった.
int main() { int t; cin >> t; for (s = 0; s < t; s++) solve(); }
教訓: なんであれ入力は全部読め.
31以上のビットシフト
次のように書いて失敗した.
for (int i = 0; i < 60; i++) { ll bit = 1 << i; ... }
もちろん,1 << i
でなく,1LL << i
でなければならない.
accumulate (に限らないが...)
和を計算してくれる std::accumulate というものがあるというのを見かけて,使ってみた.
vector<ll> vec;
...
ll vSum = accumulate(vec.begin(), vec.end(), 0);
これはだめで,accumulate(vec.begin(), vec.end(), 0LL)
でなければならない.生兵法は大けがのもと.とはいえ,前節「31以上のビットシフト」と同じ話だよな....学習しない奴.
int vs long long
大きな数は int でなく long long で持たなくてはならない.ということはさすがに間違えないのだが,$10^5$ くらいの数を int x;
に代入しておいて,ll y = x * x
とやって溢れさせた.何回やったかわからない.学習しない....
これも int vs long long
同じ話だが,関数の戻り値でも失敗した.main の中で
cout << solve() << endl;
と書いておいて,結果は32bitに納まらないことがあり得るとわかっているのに
int solve() { ll answer = ... ... return answer; }
と書いてしまう.なんなんだろうなあ.
long long でも足りない...
ARC021-C 「増築王高橋君」は,long long でも足りない計算が出てきた.... 厳密には足りないわけでは無いけれど.等差数列の和を足していくコードを書く必要があり,以下のように書いた.
ll tot = 0; ... tot += (2 * A + (p - 1) * D) * p / 2;
テストケースが1つだけ通らない.制約をよくよく見ると,$A \leq 10^3$, $D \leq 10^3$, $p \leq 10^8$ なので,2で割る直前の部分が $10^{19}$ を超えることがあり得る.LLONG_MAX は,$9.22\times10^{18}$ くらいなので,オーバーフローしてしまった.次のように書き直して通した.多倍長整数を使っても良かったかもしれない.
tot += p % 2 == 0 ? (2 * A + (p - 1) * D) * (p / 2) : ( A + ((p-1)/2) * D) * p ;
教訓はなんだろうか? 全部の式で制約をちゃんと見てオーバーフローに気を遣え,ということだろうか? 現実的でない気がする....
long long を過信しない
上と同様な話だが,$ n \leq 10^{18}, m \leq 10^{18} $ という制約で,$ 2^{ (n - 1) * (m - 1) } $ を $ \mod 10^{9} + 7 $ で求めることになった.何も考えずに
long long n, m; // ... modint x = power<modint>(modint(2), (n - 1) * (m - 1));
と書いて失敗した.(modint は mod P で計算するクラス.power は べき乗を計算する自作の関数)
modint x = power<modint>(power<modint>(modint(2), n - 1), m - 1);
としなくてはいけない.
0-indexed と 1-indexed
グラフ問題などで多いのだが,問題文は 1,...,N で与えられているものを 0,...N-1 に直してコーディングすることが多い.例えば,点が1, ..., Nという設定で,入力データは,M本の辺に引き続いて始点,終点が指定されている,と行った場合に,辺だけは1減らして読み込む.この場合,その後の始点,終点を読むときに1減らし忘れて失敗する,という失敗をたびたびする.こんな具合.
ll N, M; cin >> N >> M; vector<vector<ll>> neighbour(N); for (ll i = 0; i < M; i++) { ll u, v; cin >> u >> v; u--; v--; neighbour.at(u).push_back(v); } ll start, goal; cin >> start >> goal;
つい,start--; goal--
を忘れてしまう.
1,...,N のままでやれば良いのかもしれないが,この方が全体的には楽だと思っている.上の例では,neighbour の2次元めはどのみち 0-indexed になるのだから,1次元目もそうしたい,という気持ちである.方針が悪いかなあ.
ベクトルの値の設定: at と push_back
最初はこう書いていた.
vector<int> vx(N), vy(N); for (int i = 0; i < N; i++) cin >> vx.at(i) >> vy.at(i);
事情で pair にすることに変えた.
for (int i = 0; i < N; i++) { int x, y; cin >> x >> y; v.emplace_back(x, y); }
vが宣言されていないというエラーになったので,宣言を書き換えた.
vector<pair<int, int>> v(N);
訳がわからないことになった.
最初から pair のつもりで書いていれば,v(N)
とは書かなかったと思うが,こういうシークエンスだと,つい間違えてしまう.vector<int>
のときも at ではなく push_back を使うように習慣づけるべきだろうか?
添字の範囲
int N; cin >> N; vector x(N), y(N), p(100000);
のようになっていて
for (int i = 0; i < N; i++) { ... x[i] ... } for (int i = 0; i < N; i++) { ... y[i] ... } for (int i = 0; i < N; i++) { ... p[i] ... }
とやって失敗した.最後の N は 100000 と書くべきところ.
単純ミスと言ってしまえばそれまでだが,「ベクトルの要素を全部見る」と思っているので油断したか.
ループの回数
0, 1, 2 で 3 回ループさせるのは,
for (int i = 0; i < 3; i++) {
であって,
for (int i = 0; i < 2; i++) {
ではない.あっっっっっったり前なのだが,なぜか間違えてしまうことがある.この手のはまずデバッグで気づかない.どうしたらよいのやら.
座標圧縮
$\{ (x, y) \mid 0 \leq x \leq X, 0 \leq y \leq Y \} $ に点 $(x_1, y_1), \ldots , (x_k, y_k)$ があるという状況で, 座標圧縮するとなったら,X軸に関しては, $x_1, \ldots , x_k$ だけでなく,当然, $0$ と $X$ も登録しなくてはいけない.これを忘れてしまう.
無駄な入力値
入力に 「$a_i \quad b_i$」(i = 1,..,n) が並んでいて,この意味は「$f(a_i)$の値は $b_i$ 以下」だという. 何の疑問も持たずに
map<int, int> f_bound; for (int i = 0; i < n; i++) { ll x, y; cin >> x >> y; f_bound[x] = y; }
と書いて失敗した.たしかに,「$i \neq j$ ならば $a_i \neq a_j$」という但し書きがなかった
が,こういうのは詐欺ではなかろうか
ので,ちゃんと気をつけなくてはいけない.
領域確保にも時間はかかる
十分間に合っているつもりなのにTLEするコードを一生懸命デバッグして,どのループもそんなに時間はかかるはずがないのに... と思っていたら,下のようなコードがあった.
for (int i = 0; i < n; i++) { // n は 2e5 まで vector<int> count(k); // k は 2e5 まで ... }
そりゃ無理ですね.
この後で count[x] として現れるxの種類は十分少なかったので,vector<int>
でなく map<int, int>
を使うべきところだった.
小数点以下の桁数
答が小数になる問題で,何も考えずに次のように書いたら,小数点以下5桁くらいしか表示されなくてだめだった.
double ans = ....;
...
cout << ans << endl;
これより前のどこかで次のように書いておけば良い.
cout << setprecision(20);