【260303】今日も今日とて競プロ
競プロを伸ばすための「添削プロンプト」テンプレを本気で作った
はじめに
最近、競プロを練習していると 問題は解ける。でも、成長している実感が薄い。 ACはするけど、レートが伸びきらない。
そんな中で気づいたのが、「解いた後の振り返りが浅い」ということでした。
正解したあとこそ、実は一番伸びるタイミング!?。 そこで私は、「上級者コーチとして添削してもらうためのプロンプト」を設計しました。
この記事では、
- 競プロを伸ばすための汎用添削テンプレ
- 抽象化力を鍛える上級者向けテンプレ
- さらに成長を加速させる使い方
をまとめています。
本気で水色〜青色を目指している人の参考になれば嬉しいです。
⚡ 1分で読める要約
- 正解後の「振り返り」が競プロ成長の鍵
- 添削AIには役割を明確に与えるのが重要
- 上級者コーチとして振る舞わせるテンプレを作った
- 思考の抽象化・状態設計・拡張性まで評価させる
- さらに伸びるための使い方も紹介
なぜ「添削プロンプト」が必要?
競プロで伸びる人は、
- 問題を解く量が多い人
- ではなく
- 問題から学ぶ量が多い人
だと思っています。(もちろんコーディングのスキルもすんごく重要だと思います・・・)
解いて終わりにすると、 「実装力」しか伸びません。
でも、
- なぜその解法が本質なのか
- どんな典型パターンだったのか
- 状態設計は最適だったのか
- 難しくなったらどう拡張されるのか
そのためのテンプレを用意しました。
🧠 テンプレ①:汎用・成長特化型コーチングプロンプト
あなたは AtCoder水色〜青色レベル以上の競技プログラミング上級者で、 かつ指導経験のあるコーチです。 目的: 私は競プロで水色〜青色を目指しています。 単なる正誤判定ではなく、「上級者になるための思考力強化」を目的とした 厳しめのコーチングをしてください。 以下を必ず守ってください: 【1】まず問題の本質を1〜3行で要約してください 【2】この問題の典型パターン・分類を説明してください 【3】私の解法の良い点を具体的に指摘してください 【4】改善できる点を重要度順に挙げてください 【5】「上級者ならどう書くか」を示してください(コード例あり) 【6】計算量・状態設計・抽象化の観点から評価してください 【7】今後似た問題に出たときのチェックリストを作ってください 【8】難易度が上がった場合どう拡張されるかも説明してください 甘やかさず、成長のための指摘をしてください。 実装の細部よりも「思考の質」を重視してください。 --- 問題文 --- (ここに画像を貼る) --- 自分の解答 --- (ここにコードを貼る)
🚀 テンプレ②:抽象化力を鍛える上級者特化版
あなたはICPC経験のある競技プログラマーで、 問題の構造抽象化・アルゴリズム設計の専門家です。 私はAtCoder水色を目指しています。 「実装力」ではなく「問題構造を見抜く力」を鍛えたいです。 以下を必ず行ってください: 1. 問題をグラフ・DP・貪欲・構築・数学などの抽象カテゴリに分類 2. この問題の"制約の本質"を説明 3. 別解の可能性を列挙 4. 私のコードがスケールしないケースを指摘 5. 状態設計の改善余地を指摘 6. 1段階難しくなったらどうなるか説明 7. 競プロ上級者が考える思考プロセスを再現 厳しく、論理的にお願いします。 --- 問題 --- (画像) --- 解答 --- (コード)
📈 さらに伸びる使い方
最後に:
この問題を通して私が鍛えるべき抽象スキルを1つ選び、
それを強化するための練習方法を提案してください。
これを入れるだけで、
- 単発の反省 → 継続的なスキル強化
もできます!便利になりましたね。。
🔚 おわりに
この記事のテンプレが、 同じように伸び悩んでいる人の助けになれば嬉しいです。量も質も大事!
競プロでコピーと参照に躓いた話
参照とコピーの違いを山田君で理解する(+アドレスの話)
はじめに
最近、C++の参照(&)とコピーの違いでしばらく悩んでいました。
「なんとなく動くけど、本質がわからない」という状態が続いていて、競プロでも自信を持って使えていない感覚があったんですよね。
そこで、自分なりに腹落ちした「山田君のたとえ」を使って整理してみることにしました。
せっかくなので、未来の自分のためにもブログとして残しておこうと思います。
今日は、参照とコピーの違い、そしておまけでアドレスの意味までまとめます。
⚡ 1分で読める要約
- 参照は「同じ人の別名」
- コピーは「双子を作ること」
- 参照は同じメモリ(アドレス)を使う
- コピーは別のメモリを使う
- 競プロでは
const auto&がほぼ最強
🏁 そもそも参照とは
こんなコードを書いたことはありませんか?
void add(int x) { x += 1; } int a = 5; add(a); cout << a << endl;
「あれ?aが6にならない…?」
初心者の頃、私はこれで混乱しました。
原因は「コピー」です。
add(int x) の x は、a のコピー。
だから x をいじっても a は変わらない。
でも、参照を使うと世界が変わります。
void add(int& x) { x += 1; }
これだけで、a がちゃんと増える。
👉 参照を使うだけで、コピーを防ぎ、元の値を直接操作できる。
これが大きなメリットです。
🧠 解説
■ コピー
int a = 5; int b = a;
- a と b は別のメモリ
- b を変更しても a は変わらない
メモリイメージ:
1000 → [5] ← a 2000 → [5] ← b
■ 参照
int a = 5; int& r = a;
- r は a の別名
- 新しいメモリは作られない
メモリイメージ:
1000 → [5]
↑
a
↑
r
r を変更すると a も変わります。
👪 たとえ(山田君バージョン)
参照はこういう感じです。
- 本名:山田太郎
- ニックネーム:タロちゃん
どちらも同じ人。
タロちゃんにお使いを頼む
→ 山田太郎が動く。
山田太郎がケガをする
→ タロちゃんもケガしている。
これが参照。
コピーは?
コピーは「山田太郎のクローン」。
- 太郎にお使いを頼んでも
- クローン太郎は動かない
片方がケガしても、もう片方は無傷。
これがコピー。
✏ 使用例
① 関数でコピーを防ぎたいとき
void solve(const vector<int>& v)
- コピーしない
- 安全(変更不可)
競プロでは超重要。
② for文で高速化
for (const auto& x : v)
大きな vector のときは特に効果的。
❓ クイズ
Q1
int a = 3; int& r = a; r += 2; cout << a;
答え:5
(同じメモリを見ているから)
Q2
int a = 5; int b = 7; int& r = a; r = b; cout << a;
答え:7
(rはaの別名。bをaに代入している)
Q3(アドレス編)
int a = 5; int& r = a; cout << &a << endl; cout << &r << endl;
答え:同じアドレスが出力される。
🪜 躓いたポイント
私は「参照=ポインタみたいなもの」と思っていました。
でも実際は違う。
- ポインタは「住所を書いたメモ」
- 参照は「その住所そのもの」
ここが一番の気づきでした。
🎯 次回予告
次は「右辺値参照(&&)」と「move」について理解したい。
ここが理解できると、C++中級者に一歩近づけそう。
競プロでもより効率的なコードを書けるようになりたい。
🔍 関連キーワード
AtCoder / C++ / 参照 / ポインタ / 競技プログラミング / メモリ / const / STL
新年の抱負の書き方
はじめに
2025年もいよいよ終わりを迎えました。恐ろしく一年ははやいもので、充実していた時期もあれば、悩みの多かった日々もあったように思います。皆さんはいかがでしたでしょうか。
そして、年の終わりが見えてくると、自然と来年のことを考え始めます。そんな年末の終わりにふと思うのが、今年はなにを成し遂げたかなということです。「私は、仕事を頑張った」「毎日楽しい日々を過ごせた」「毎朝ランニングを頑張った」などでしょうか。
けど、ほとんどの人は、年の初めにどんな目標を立てたかを思い出せない人も多いのではないでしょうか。私もその一人です…。つまり、年始に計画だけは立てたものの、計画的に実行できなかった人です。
そんな自分が少し嫌になり、この12月中に、来年のことをもう一度きちんと考えてみようと思いました。なので、この記事では、2026年がより自分のためになるような、計画の立て方についてメモしておきたいと思います。
1. なぜ社会人の新年の抱負は続かないのか
新年の抱負が続かない理由として、よく「意志が弱いから」「忙しくなったから」と言われます。しかし、多くのビジネス書を読んでいると、原因はもっと単純なところにあると感じます。
それは、抱負の立て方そのものが曖昧だということです。
仕事では、目的・やること・期限が整理されていないプロジェクトは、ほぼ確実にうまくいきません。
この考え方は、ピーター・ドラッカー『マネジメント』でも繰り返し語られています。
成果は偶然ではなく、管理された行動の結果です。
新年の抱負も同じで、「頑張る」「成長する」といった抽象的な言葉だけでは、行動につながりません。
抱負が続かないのは、根性の問題ではなく、仕事と同じで設計の問題なのかもしれません。
2. ビジネス本が共通して語る「うまくいく抱負」の条件
結果ではなく「行動」を書く
よくある新年の抱負には、次のようなものがあります。
- 今年は評価を上げたい
- 仕事で成果を出したい
- 成長を実感できる一年にしたい
どれも間違ってはいませんが、今すぐ何をすればいいかが分かりません。
この点について、ジェームズ・クリアー『Atomic Habits』では、次のような考え方が紹介されています。
目標ではなく、システムに集中せよ
つまり、「達成したい状態」ではなく、日々の行動(システム)を書くべきだということです。
(図解案:結果と行動の違いを示すシンプルな図)
3. 社会人向け・新年の抱負の3つの型
① テーマ型(判断基準を決める)
具体的な行動を細かく決めるのが難しい場合、「今年のテーマ」を一言で決める方法があります。
- 整える
- 積み上げる
- 深める
この考え方は、スティーブン・R・コヴィー『7つの習慣』にも通じます。
忙しい中で正しい選択を続けるためには、判断軸が必要です。
② 行動習慣型(仕事の中に組み込む)
次は、具体的な行動をそのまま抱負にする方法です。
- 毎朝5分、今日の優先順位を書く
- 週に一度、仕事の振り返りをする
- 月に一回、学んだことをまとめる
この考え方は、デイヴィッド・アレン『Getting Things Done』で詳しく説明されています。
派手な目標よりも、仕組み化された行動の方が続きやすいです。


図解:週次レビューの仕方

③ やらないこと型(負担を減らす)
最後は、「やること」ではなく「やらないこと」を決める方法です。
- 目的のない残業をしない
- なんとなく資料を作り直さない
- 即レスできない通知は切る
この発想は、グレッグ・マキューン『エッセンシャル思考』と相性が良いです。
4. 抱負を書くときに気をつけたい3つの落とし穴
完璧な一年を描こうとする
元気な日の自分ではなく、「疲れている日の自分」が守れるかどうかを基準にすると、続きやすくなります。
一度決めたら変えてはいけないと思う
仕事の計画が修正されるように、抱負も途中で見直して構いません。
書いて満足してしまう
書く → 見返す → 少し直す、というサイクルを意識すると、抱負は生き続けます。

5. それでも新年の抱負は「未完成」でいい
新年の抱負は、誓約書ではなく、行動を助けるための仮説メモです。
途中で変わっても、曖昧でも問題ありません。
来年の今頃に振り返ったとき、
「去年の自分は、ちゃんと考えようとしていたな」
そう思えれば、それで十分なのだと思います。
【ABC207C】Many Segmentsで区間の重なり判定とビット演算を学んだ話
⚡ 1分で読める要約
- AtCoder ABC207 C「Many Segments」で、4種類の区間の重なり判定問題に取り組んだ記録です。
- はじめは
intで端点を扱ってしまい、0.5の補正が無意味になるバグで悩みました。 - その後、
doubleと 0.5 のずらし+「max(left) ≤ min(right)」というシンプルな条件に整理してAC。 - 公式解説で使われていた
t & 1,t & 2というビット演算テクも学びました。 - 緑を目指す中で、「まずは分かりやすく正しく書く」ことの大事さも再確認しました。
🏁 はじめに
最近、AtCoder でコンテストに参加しながら、少しずつレートを上げていこうとしています。
今の目標は「まずは緑(レート800〜)」に到達すること。そのために、過去問のC問題を中心にコツコツ解いているところです。
今回は、ABC207のC問題「Many Segments」に取り組みました。
区間の重なり判定という一見シンプルなテーマですが、「開区間と閉区間が混ざるだけでこんなにややこしくなるのか…」と良い意味でしっかり鍛えられたので、学びをまとめておきます。
💭 問題概要
扱うのは長さ (N) 本の区間です。
各区間は次の4種類のいずれかで与えられます:
- (t = 1): [l, r]
- (t = 2): [l, r)
- (t = 3): (l, r]
- (t = 4): (l, r)
やることはシンプルで、
というものです。
公式問題はこちら:
ABC207 C - Many Segments
ポイントは「端点を含むか含まないか」が4種類もあるので、単純に if で条件を書き分けようとすると、場合分けがカオスになってしまうところです。
🧩 実装と試行錯誤
最初の私は、だいたいこんな流れで実装しようとしていました。
- 入力の区間をそのまま
intのl[i],r[i]に保存 - 2重ループで全ペア
(i, j)を確認 t[i]とt[j]に応じて場合分けしながら、「重なっているかどうか」を頑張って条件で書く
ただ、この方針だとすぐに次のような問題にぶつかりました。
- 区間の種類が4種類あるので、条件分岐がどんどん増えて読みにくい
- 「端点がちょうど一致しているとき」に、どちらが開でどちらが閉かを考えるのがつらい
- 実装してみたら、サンプルは通るのに提出するとWA
さらに、途中から「0.5 ずらすテクニック」を取り入れたのですが、最初はこんなバグを踏みました。
l1,r1をint型にしたまま、r1 = r[i] - 0.5;のように書いてしまう- 結果、小数部分が切り捨てられて、0.5 の意味が完全に消える
- なのに、自分では「0.5 ずらしているつもり」なので、バグに気付きにくい
同時に、重なり判定も
if (right - left > 0) ++ans;
のように書いていたため、
- 端点がぴったり一致している場合(
right == left)で、- 本当は「重なる」ケースがあるのに
> 0なので数え漏らしてしまう
という問題も発生していました。
🔍 改善の流れ
1. すべての区間を「同じルール」にそろえる
公式解説やアドバイスを参考に、まずは発想を変えました。
という方針です。
具体的には、次のように「端点を 0.5 ずらす」ことで、開閉の違いを吸収します:
- [l, r] → [l, r]
- [l, r) → [l, r - 0.5]
- (l, r) → [l + 0.5, r]
- (l, r) → [l + 0.5, r - 0.5]
こうしておくと、すべての区間が「両端を含む閉区間」として扱えるので、
2つの区間が重なる条件は
max(l1, l2) <= min(r1, r2)
という、めちゃくちゃシンプルな形になります。
ここで重要なのが、
l/rはdouble型 にすることleft/rightもdouble型 で受け取ること
でした。
int にキャストしてしまうと、小数が消えてしまい、そもそも 0.5 のずらしが無意味になってしまいます。
2. ビット演算で「開いているかどうか」を管理する
もう一つ、公式解説で「おお〜」となったのがこの部分です。
int t; cin >> t >> l[i] >> r[i]; t--; if (t & 1) r[i] -= 0.5; if (t & 2) l[i] += 0.5;
t を 1〜4 から 0〜3 に t-- でシフトし、
2進数のビットを「左端が開いているか」「右端が開いているか」のフラグとして使っています。
t & 1… 右端が開いているなら1(真)t & 2… 左端が開いているなら2(≠0 → 真)
最初見たときは「レベル高くない…?」と思ったのですが、
意味がわかると if を4つ書くよりもスッキリ していて、かなり気持ち良い書き方でした。
もちろん、緑を目指している段階では、まずは素直に if (t == 1) { ... } と分岐して書いてもまったく問題ないと思います。
ただ、「こういう書き方もある」と知っておくと、将来自分でも自然に使えるようになりそうです。
🚀 最終コード
最終的にACしたコードは、ほぼ公式解説と同じ形になりました。
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int N; cin >> N; vector<double> l(N), r(N); for (int i = 0; i < N; i++) { int t; cin >> t >> l[i] >> r[i]; t--; // t: 1..4 -> 0..3 にシフト // tのビットで開閉を管理 // t & 1 が立っていたら「右端が開いている」 → rを少し左にずらす if (t & 1) r[i] -= 0.5; // t & 2 が立っていたら「左端が開いている」 → lを少し右にずらす if (t & 2) l[i] += 0.5; } int ans = 0; for (int i = 0; i < N; i++) { for (int j = i + 1; j < N; j++) { double left = max(l[i], l[j]); double right = min(r[i], r[j]); if (left <= right) { ans++; } } } cout << ans << ' '; return 0; }
やっていることはシンプルで、
- 入力段階で区間を「0.5ずらし」で正規化
- あとは全ペアを見て
max(l[i], l[j]) <= min(r[i], r[j])ならカウント
というだけです。
🧠 今回の学び
今回の一番の学びは、「ややこしい場合分けは、うまく前処理して揃えてしまうと楽になる」 ということでした。
という流れは、他の問題にも応用できそうだと感じました。
また、バグの原因になったポイントも大事な反省点です。
intに0.5を代入すると小数が切り捨てられるright - left > 0だと、「端点がぴったり一致している」ケースを拾えない<=なのか<なのか、境界条件を丁寧に確認する必要がある
そして、ビット演算についても
t & 1で「右端が開いているか」を判定t & 2で「左端が開いているか」を判定
というように、2進数の各ビットをフラグとして使うというアイデアが、少し身近になりました。
今の私の目標は緑なので、まずは「読みやすくて正しいコード」を安定して書けるようになることを優先しつつ、
こういったテクニックも少しずつストックしていきたいなと思います。
?クイズ
今回の内容から、ちょっとしたクイズを用意してみました。
Q1. 区間の変換
次の区間 (l, r) を、「0.5ずらし」を使って閉区間に変換するとどうなりますか?
答え:
([l + 0.5, r - 0.5])
両方とも開いているので、左端を右に +0.5、右端を左に -0.5 します。
Q2. 重なり判定の条件
すでに 0.5 ずらし済みの2区間 ([l_1', r_1']), ([l_2', r_2']) が重なっているかどうかは、どんな1行の条件式で判定できますか?
答え:
max(l1, l2) <= min(r1, r2)
左側の大きい方が右側の小さい方よりも左(または同じ位置)にあれば、区間は少なくとも1点は共有しています。
Q3. ビット演算の意味
t-- した後の t について、if (t & 1) が真になるのは、元の区間の種類(1〜4)のうちどれでしょう?
答え:
- 元の
t = 2([l, r)) - 元の
t = 4((l, r))
どちらも「右端が開いている」区間なので、t & 1 が1になります。
🪜 躓いたポイント
今回、自分が特に躓いたのはこのあたりです。
intで端点を持ったまま-0.5や+0.5をしていたこと
→ 「ちゃんと0.5動かしてるつもり」で、実はまったく変わっていなかった- 重なり条件を
right - left > 0としてしまい、端点一致のケースを取りこぼしていたこと t & 1,t & 2という書き方を初めて見て、「何をやっているのか」ピンと来なかったこと
ただ、一つひとつ丁寧に追いかけてみると、
- 型の問題(int vs double)
- 境界条件の問題(
<か<=か) - ビット演算の意味(2進数で考える)
など、どれも基礎的だけど重要なテーマだったので、
この問題で一気に復習&理解が深まった感があります。
🎯 次回の目標
今回のABC207Cで、
- 区間の重なり判定
- 0.5 ずらしテク
- ビット演算でフラグ管理
あたりの感触がつかめてきたので、次はこんなことを目標にしていきたいです。
- 他のABCのC問題で、今回と似たような「実装力系」の問題をもう少し数こなす
- 特に「境界条件がややこしい系」の問題を怖がらずに触ってみる
- 余裕が出てきたら、ビット演算を使ったテクニック(ビットDPまではいかなくても、フラグ管理など)にも少しずつ慣れていく
最終的には、水色も見据えつつ、まずは緑で安定して戦える実装力を身につけたいなと思っています。
🔍 関連キーワード
- AtCoder
- ABC207C
- 区間の重なり判定
- ビット演算
【ABC325C】グリッドBFSで学んだことメモ
⚡ 1分で読める要約
- AtCoder ABC325 C「Sensors」で、グリッド上の連結成分を数える問題に取り組んだログです。
- 2次元配列の扱い、
charと"文字列"の違い、pair<int,int>の使い方でしっかり躓きました。 - BFS での
visited管理や範囲チェックの順番など、典型テクニックの重要さを再確認。 - 最終的には
vector<string>を使ったすっきり実装に落ち着きました。
🏁 はじめに
最近、AtCoder で水色を目指してコツコツ問題を解いています。
その中で、グリッド探索 + BFS という、いかにも競プロっぽい問題に取り組んだので、備忘録がてらまとめておきます。
今回扱ったのは AtCoder Beginner Contest 325 C 問題「Sensors」。
やってみると、実はアルゴリズム自体よりも、
- 2次元配列の宣言と初期化
charと"."の違い- 座標の軸(行・列・x・y)がこんがらがる問題
といった 実装寄りのところ でだいぶ苦戦しました。
この記事は、未来の自分がまた同じところでつまづかないようにするための、自分向けの解説メモです。
💭 問題概要
問題の内容(ざっくり)
- 縦
H行、横W列のマス目がある。 - 各マスには
.または#が書かれている。#が「センサー」。 - 上下左右・斜め(8方向)のいずれかで隣接している
#同士は「つながっている」とみなす。 - この「つながっている」センサーの塊を 1 個のセンサーと数え、全部で何個あるか を求める問題。
要は、8 方向連結な # の連結成分数を数える グリッド探索問題です。
問題リンク
🧩 実装と試行錯誤
最初はこんな感じの方針で書き始めました。
- グリッドは
vector<vector<char>>で管理 usedもvector<vector<bool>>で訪問管理- 2重ループで全マスを見る
#で未訪問のマスから BFS を始め、つながっているマスを全部たどるたびにans++
…と、方針自体は王道だったのですが、実装でかなり迷走しました。
1. 2次元配列の宣言と初期化まわり
vector<vector<char>> a(h, vector<char>(w)); vector<vector<bool>> used(h, vector<bool>(w));
ここまではよかったのですが、読み込みループの中でなぜか
used[h][w] = false;
なんて書いてしまっていて、完全に範囲外アクセス。
そもそも vector<vector<bool>> used(h, vector<bool>(w)); の時点で、全部 false 初期化されるので、ここは何も書かなくてよかったところでした。
2. char と "." の比較でコンパイルエラー
つい Python のノリで、
if (a[i][j] == "." || used[i][j]) continue;
と書いてしまい、
comparison with string literal results in unspecified behavior
ISO C++ forbids comparison between pointer and integer
のようなエラーが出て撃沈。
C++ では a[i][j] は char なので、1文字は ' '(シングルクォート) で書く必要がある、という基本を忘れていました。
if (a[i][j] == '.' || used[i][j]) continue; // こっちが正解
3. queue<pair<int,int>> の宣言ミス
queue<pair<int,int>> の宣言ミス4. 範囲チェックで軸を取り違える
BFS で隣接マスを見に行くとき、最初はこう書いていました:
if (nx >= 0 && nx < w && ny >= 0 && ny < h && !used[nx][ny]) { ... }
本来、
nxは「行」なので0 <= nx < hnyは「列」なので0 <= ny < w
でチェックすべきところを、逆にしてしまっていたパターンです。
これもバグの温床でした。
5. 始点を visited にするのを忘れる
BFS 開始時に、
queue<pair<int,int>> que; que.push({i, j});
とだけ書いていて、used[i][j] = true; をしていない状態でした。
これだと、条件によっては同じマスが何度もキューに入ったり、成分が 1 マスだけのときに used が最後まで false のままだったり、地味に危ない挙動になります。
🔍 改善の流れ
何度かコンパイルエラーや WA と戦った結果、次の点を意識して書き直しました。
1. グリッドは vector<string> にしてシンプルに
もともと
vector<vector<char>> a(h, vector<char>(w));
と書いていましたが、行ごとに 1 本の文字列で扱うほうが楽だと気づいて、
vector<string> s(h); for (int i = 0; i < h; i++) cin >> s[i];
という形に変更しました。
これで、
- 入力が 1 重ループで済む
s[r][c]でそのままcharが取れる- デバッグ時に
cout << s[i]で行をそのまま出せる
など、扱いが一気に楽になりました。
2. 座標は r, c(row, column)で考える
x, y という書き方だと、「どっちが縦だっけ?」と毎回迷ってしまったので、思い切って r, c に統一しました。
r…row(行) → 0 〜 h-1(縦)c…column(列) → 0 〜 w-1(横)
として、
int dr[8] = {1, 0, -1, 0, 1, 1, -1, -1}; int dc[8] = {0, 1, 0, -1, 1, -1, 1, -1};
と置くことで、
int nr = r + dr[d]; int nc = c + dc[d]; if (0 <= nr && nr < h && 0 <= nc && nc < w) { ... }
と書いても違和感がなくなり、軸の取り違えがかなり減りました。
3. BFS の「定石形」を身体に入れる
BFS まわりは、以下の形で「テンプレ」として覚えることにしました。
- 開始地点で
usedを true にしてからキューに突っ込む - 隣接マスも、
usedを true にしてからキューに入れる - 範囲チェック → 中身のチェック (
'#'かどうか) →usedの確認、の順で条件を書く
queue<pair<int,int>> que; used[r][c] = true; que.push({r, c}); while (!que.empty()) { auto [x, y] = que.front(); que.pop(); for (int d = 0; d < 8; d++) { int nx = x + dr[d]; int ny = y + dc[d]; if (0 <= nx && nx < h && 0 <= ny && ny < w && s[nx][ny] == '#' && !used[nx][ny]) { used[nx][ny] = true; que.push({nx, ny}); } } }
この形にしてからは、バグの入り込みどころがかなり減りました。
🚀 最終コード
最終的に AC したコードはこんな感じになりました。
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int h, w; cin >> h >> w; // グリッドは行ごとに string で持つ vector<string> s(h); for (int i = 0; i < h; i++) { cin >> s[i]; } // 訪問管理用 vector<vector<bool>> used(h, vector<bool>(w, false)); // 8方向(上下左右+斜め) int dr[8] = {1, 0, -1, 0, 1, 1, -1, -1}; int dc[8] = {0, 1, 0, -1, 1, -1, 1, -1}; int ans = 0; for (int r = 0; r < h; r++) { for (int c = 0; c < w; c++) { // センサーがない or もう訪問済みならスキップ if (s[r][c] == '.' || used[r][c]) continue; // このマスを起点に BFS して、1つの連結成分を塗りつぶす queue<pair<int,int>> que; used[r][c] = true; // キューに入れる前に visited にする que.push({r, c}); while (!que.empty()) { auto [x, y] = que.front(); que.pop(); for (int d = 0; d < 8; d++) { int nx = x + dr[d]; int ny = y + dc[d]; // 範囲チェック → '#' か → 未訪問か の順 if (0 <= nx && nx < h && 0 <= ny && ny < w && s[nx][ny] == '#' && !used[nx][ny]) { used[nx][ny] = true; que.push({nx, ny}); } } } // 1つの連結成分をまるっと探索し終えたのでカウント ans++; } } cout << ans << ' '; return 0; }
🧠 今回の学び
今回の ABC325C を通して、特に大事だなと感じたポイントは以下の通りです。
1. グリッドは「行」「列」で考える
- 配列の添字は基本的に
[行][列]。 - 座標を
r, cと書くことで、縦横の混乱がだいぶ減りました。 - 範囲チェックも「
0 <= r < H」「0 <= c < W」と書くのが自然。
「x が縦? 横?」と毎回悩むくらいなら、最初から r/c で慣れてしまう方が精神的に楽だと思いました。
2. BFS の「型」を覚えると安心
毎回イチから考えるのではなく、以下の流れをテンプレにするとよさそうです。
- 始点のチェック
queueを用意- 始点を
visited = trueにしてキューに入れる - キューが空になるまでループ
- 取り出した頂点から隣接マスを見る
- 範囲チェック → 壁でないか → 未訪問か
- 条件を満たすマスを
visited = trueにしてキューに入れる
この「パターン」を身体に入れておくと、バグる余地が格段に減ると感じました。
3. char と "文字列" の違いは最初に叩き込む
- 1文字 →
'a'(シングルクォート) - 文字列 →
"abc"(ダブルクォート)
というルールを間違えると、コンパイルエラーが長々と出てきてつらいので、早めに慣れておくのが良さそうです。
4. 初期化はコンストラクタに任せる
vector<vector<bool>> used(h, vector<bool>(w));
と書いた時点で、自動的に全部 false で初期化されます。
ループの中で used[h][w] = false; みたいなことをしてしまうと、逆にバグの原因(範囲外アクセス)になります。
「コンストラクタで初期化できるものは素直に任せる」という感覚が大事だと感じました。
?クイズ
今回の解法の肝になっている部分を、軽いクイズ形式で振り返ります。
Q1. 次の条件分岐のうち、安全なのはどっち?
A:
if (s[nx][ny] == '#' && 0 <= nx && nx < h && 0 <= ny && ny < w) { ... }
B:
if (0 <= nx && nx < h && 0 <= ny && ny < w && s[nx][ny] == '#') { ... }
答えを見る
正解:B
if の条件は左から順に評価され、途中で false になったら以降は評価されません。
A のように最初に s[nx][ny] にアクセスしてしまうと、範囲外の添字でアクセスする可能性があり危険です。
B のように、先に範囲チェック → そのあと中身を読むのが正しい順番です。
Q2. BFS で始点をキューに入れるとき、used を true にするタイミングは?
- キューから取り出した直後に
usedを true にする - キューに入れる前に
usedを true にする
答えを見る
基本的には 2 が推奨。
- 「キューに入れる=訪問予定」とみなして、そのタイミングで
used = trueにしておくと、同じマスを重複してキューに入れるのを防げます。 - 1 の場合でもロジックによっては正しく書けますが、ミスしやすくなるので、定石としては 2 を覚えておくのが安全です。
Q3. 8 方向移動のとき、dr / dc の組として正しいものはどれ?
int dr[4] = {1, 0, -1, 0}; int dc[4] = {0, 1, 0, -1};
int dr[8] = {1, 0, -1, 0, 1, 1, -1, -1}; int dc[8] = {0, 1, 0, -1, 1, -1, 1, -1};
答えを見る
正解:2
1 は上下左右の 4 方向だけですが、今回の問題は 斜めも含めた 8 方向連結なので、2 のように斜め成分(±1, ±1 の組)も含める必要があります。
🪜 躓いたポイント
自分用の正直メモです。
- 2次元配列の宣言が長くて嫌になりがちだったけど、
vector<string>にしてだいぶスッキリした。 a[i][j] == "."と書いてエラー祭り。1文字は' 'で書くという超基本を忘れていた。nx < w、ny < hのように縦横を逆にして、範囲チェックを間違えていた。- BFS の始点で
used[i][j] = true;を忘れて、訪問管理がふわっとしていた。 - 「x 軸 / y 軸」と考えると毎回混乱するので、今後は 最初から
r, cで統一しようと決意。
🎯 次回の目標
今回の ABC325C を通して、実装力の基礎がまだまだだな…というのを痛感しました。
今後はこんな感じで進めていきたいです。
- グリッド問題をもう何問か解いて、BFS / DFS の型をしっかり身体に染み込ませる。
- 2次元配列まわり(宣言・初期化・範囲チェック)でミスをしないように、意識して練習する。
- 実戦で書いたコードを、後から「もっと短く・読みやすく」書き直す時間をとる。
- ABC の A〜D あたりを継続的に解きつつ、基礎本(アルゴリズム系)で理論も少しずつ補強していく。
「とりあえず AC」だけで満足せず、ちゃんと振り返って言語化する習慣をつけていきたいなと思います。
🔍 関連キーワード
【JSC2021 B - Xor of Sequences】対称差で解く2本ポインタ入門メモ
⚡ 1分で読める要約
- AtCoder【第二回日本最強プログラマー学生選手権 B - Xor of Sequences】を C++ で解いたときのメモです。
- 問題は「ビット演算の XOR」ではなく、集合の対称差(片方だけに含まれる要素)を求めるだけのシンプルな内容でした。
- 実装では 2 本ポインタ法、
vectorの使い方、i++と++i、size_tとintの違いなどにガッツリつまずきました。 - 最終的には標準 C++ らしい安全なコードにたどり着けたので、その過程をブログ風に振り返ります。
🏁 はじめに
最近、AtCoder で水色を目指してコツコツと C++ に慣れているところです。 今回は「第二回日本最強プログラマー学生選手権」の B 問題【Xor of Sequences】に取り組みました。
タイトルに Xor とついていたので「うわ、ビット演算か……」と一瞬身構えたのですが、よく読むと中身はただの集合の対称差。
とはいえ、C++ の配列や vector、インクリメント演算子、for 文、警告メッセージなど、細かいところで色々つまずいたので、その試行錯誤を自分用メモとして残しておきます。
「同じところでハマりそうな人の助けになればいいな」という気持ちで書いています。
💭 問題概要
問題
- 昇順にソートされた 2 つの整数列 A, B が与えられる(どちらも 狭義単調増加=同じ値は出てこない)。
- 「A か B のどちらか一方にだけ現れる整数」を、昇順にすべて出力する。 - 集合でいえば、$A \triangle B$(対称差)を出力する問題。
問題リンク
Xor といいつつ、ビット演算を直接使うわけではなく、あくまで「どちらか一方にだけある」という性質が XOR と似ているよね、というニュアンスの問題でした。
🧩 実装と試行錯誤
最初は、つい C っぽくこんな感じで書き始めました。
int n, m; cin >> n >> m; int A[n+1], B[m+1]; // 可変長配列 (VLA) ※標準C++だと非対応 for (int i = 1; i <= n; i++) cin >> A[i]; for (int j = 1; j <= m; j++) cin >> B[j]; int i = 0, j = 0; // ここから 0 始まりで走査してしまう vector<int> ans; while (i < n && j < m) { if (A[i] == B[j]) { ++i; j++; } else if (A[i] < B[j]) { ans.push_back(A[i++]); } else { ans.push_back(B[j++]); } }
このコードには問題がたくさんありました。
int A[n+1]/int B[m+1]- これは 可変長配列(VLA) で、GCC の拡張では動くけど標準 C++ ではNG。- 入力は
A[1..n]/B[1..m]に入れているのに、ループはi = 0, j = 0から始めてA[i],B[j]を読む →A[0],B[0]は未初期化で、未定義動作。 - 最後のループを
<=にしていたバージョンでは、A[n]/B[m]を読みに行ってしまい範囲外アクセス。
さらに途中で
vector<int> ans[max(n, m)];
なんて書いてしまい、「vector<int> の配列」を作ってしまうという事故もありました(本当に欲しかったのは vector<int> ans; だけ)。
コンパイル時にはこんな警告も出ました。
warning: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type'
int kとans.size()(これはsize_t=符号なし整数)を比較しているので、符号あり/なしの比較で警告が出るやつです。
そして Windows + PowerShell では、出力ファイル名に日本語を使っていて、
'.\第2回日本最強プログラマー学生選手権B.exe' は認識されません
と怒られるところでもつまずきました。
(素直に -o jsc2021_b.exe とか英数字にしておけばよかったやつです。)
🔍 改善の流れ
1. アルゴリズム:2 本ポインタでマージ+対称差
A, B はどちらも 昇順 かつ 重複なし なので、merge sort のマージ処理のように、2 本ポインタで左から順に見ていくのが素直です。
- ポインタ
iは A、jは B を指す。 - ループの中で A[i] と B[j] を比較:
-
A[i] == B[j]→ 両方にあるので答えに入れず、i++,j++。 -A[i] < B[j]→A[i]は A にだけ現れるので答えに入れ、i++。 -A[i] > B[j]→B[j]は B にだけ現れるので答えに入れ、j++。 - どちらかの列が尽きたら、もう片方の残りはすべて「片方だけにある」ので、そのまま答えに入れる。
この方法なら、計算量は O(n + m) で、余計なデータ構造も必要ありません。
2. 配列から vector へ
可変長配列をやめて、素直に vector を使うことにしました。
vector<long long> A(n), B(m); for (int i = 0; i < n; ++i) cin >> A[i]; for (int j = 0; j < m; ++j) cin >> B[j];
- 0 始まりに統一したおかげで、インデックスのミスがかなり減りました。
A[i]はi = 0 .. n-1の範囲で使う、という基本ルールが守れる。
3. インデックスと境界条件の整理
さっきの事故はほとんど「1 始まりで入れて 0 始まりで読み出す」という矛盾が原因でした。
- 入力時:
for (int i = 0; i < n; ++i) - ループ条件:
while (i < n && j < m) - 残り処理:
while (i < n) ...,while (j < m) ...
というふうに、**すべて 0 始まり / < n / < m にそろえることで、範囲外アクセスを防げます。
4. i++ vs ++i、int vs size_t
今回のコードの中にも ans.size() を使う部分がありました。
size()の戻り値はsize_t(符号なし整数)- for 文のカウンタは
intにしていたので、intvssize_tで警告
対応としては素直に size_t に揃えるのが一番早いです。
for (size_t k = 0; k < ans.size(); ++k) { if (k) cout << ' '; cout << ans[k]; }
また、インクリメントは i++ でも ++i でも動作は同じですが、
- 前置の
++iは「増やしてから返す」 - 後置の
i++は「返してから増やす(古い値のコピーが一度できる)」
という違いがあります。
単体で書く場合(i++; のようなケース)はどちらでもよいですが、C++界隈では「返り値が不要なら ++i」という文化があり、イテレータではこちらのほうが効率的なこともあります。
🚀 最終コード
最終的に、AC した C++ コードはこんな感じになりました。
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<long long> A(n), B(m); for (int i = 0; i < n; ++i) cin >> A[i]; for (int j = 0; j < m; ++j) cin >> B[j]; vector<long long> ans(n+m); int i = 0, j = 0; while (i < n && j < m) { if (A[i] == B[j]) { ++i; ++j; // 両方にあるのでスキップ } else if (A[i] < B[j]) { ans.push_back(A[i++]); // A にだけある } else { ans.push_back(B[j++]); // B にだけある } } // 残りをそのまま追加 while (i < n) ans.push_back(A[i++]); while (j < m) ans.push_back(B[j++]); // 出力(空白区切り・末尾に余計な空白を付けない) for (size_t k = 0; k < ans.size(); ++k) { if (k) cout << ' '; cout << ans[k]; } if (!ans.empty()) cout << '\n'; return 0; }
🧠 今回の学び
- 対称差と 2 本ポインタ - 昇順 & 重複なしの数列が 2 本あれば、2 本ポインタで「マージしながら差分を取る」のが鉄板パターン。
- 0-index / 1-index の統一 - 「入力も処理も 0 始まり」にしておくと、競プロでは圧倒的にバグりにくい。
vectorのサイズとpush_backの関係 -vector<int> v;で宣言した直後は size = 0 なので、v[0] = x;は NG。 データを入れるときはpush_backか、vector<int> v(n);/v.resize(n);でサイズを作ってから代入する。- 警告もちゃんと読む
-
-Wallをつけたおかげで、intvssize_tの比較警告などにも気づけた。 警告をつぶしていくと、地味にバグ率が下がる気がする。
?クイズ
Q1. 今回の解法では、while (i < n && j < m) と書きました。
もしここを while (i <= n && j <= m) に変えてしまうと、何が問題になるでしょうか?
A.
配列 A と B の有効なインデックスは 0..n-1, 0..m-1 なので、i == n や j == m の状態で A[i], B[j] を読むと範囲外アクセスになります。
C++ では範囲外アクセスは未定義動作なので、実行時エラーや謎のバグの原因になります。
Q2. A, B が「昇順かつ重複なし」であることは、今回の 2 本ポインタ法にどう役立っているでしょうか?
A.
- 昇順であるおかげで、i と j を一度進めた要素は二度と振り返らなくてよい → O(n + m) で終わる。
- 重複なしのおかげで、A[i] == B[j] のときに「この値は両方に 1 回ずつだけ存在する」と安心してスキップできる。
もし重複があれば、同じ値が何回出てくるか数える処理が必要になって、もう少しロジックが複雑になる。
Q3. vector<int> v(n); と vector<int> v; v.reserve(n); の違いは何でしょう?
A.
vector<int> v(n);- サイズが n の vector を作る。 -v.size() == nで、v[0]..v[n-1]にアクセスしてよい。 - 中身は 0(または型のデフォルト値)で初期化される。vector<int> v; v.reserve(n);- サイズは 0 のままだが、「内部的に n 個ぶんくらいのメモリを予約」する。 -v.size() == 0のままなので、v[0]にアクセスするとアウト。 - これを使うときは、push_backで要素を追加していく前提。
(option)今回のように ans にどんどん push_back していく場合は、reserve(n + m); であらかじめメモリだけ押さえておくと、再確保の回数が減ってちょっとだけ高速になります。
🪜 躓いたポイント
- 0 始まり / 1 始まりがごちゃ混ぜになった
- 入力は 1 始まり、処理は 0 始まり…みたいな状態になっていて、
A[0]を読んでしまったり、<= nで範囲外アクセスをしたりしていました。 vector<int> ans[max(n, m)];という謎構造体を生み出した - 本当に作りたかったのは「整数のリスト 1 つ」なのに、なぜか「vector の配列」を作ってしまい、ans.push_back(...)がそもそもコンパイルできない状態に。- 可変長配列(VLA)を素で書いてしまった
-
int A[n+1];みたいな書き方は C 的には見慣れていたけど、標準 C++ 的には非推奨・非対応。 - 今後は素直にvectorを使うことにしました。 - コンパイル警告とちゃんと向き合う習慣が足りなかった
-
-Wallのおかげで「符号付き / 符号なし」の比較に気づけたので、今後も警告はちゃんと読む癖をつけたい。
🎯 次回の目標
今回は比較的やさしめの B 問題だったので、 次は
- C 問題レベルの実装問題でも、落ち着いて 2 本ポインタやソート+貪欲が使えるようにすること
- C++ の文法であやしいところ(参照、const、ラムダなど)を少しずつ整えていくこと
- AtCoder の過去問(特に B, C)を中心に、手を動かす量を増やすこと
あたりを目標に練習していきたいと思います。
水色までの道のりはまだ長そうですが、今回みたいに「ハマったところをちゃんと文章にして残す」のを続けて、少しずつ基礎力を固めていきます。
🔍 関連キーワード
【ABC413B】文字列連結の重複(range-based for)
⚡ 1分で読める要約
- ABC413B を解きながら、set / unordered_set を使った重複排除の本質を理解した。
- range-based for の意味が腑に落ちて、C++ の書き方が一段スッキリした。
- 最初はカウント方法を間違えて沼ったが、集合に任せる設計に切り替えて改善。
🏁 はじめに
set・map といった連想配列の考え方や、range-based for の使い方が曖昧なままになっていて、
書いていても「これ正しいのかな?」と不安になる瞬間が多かった。
そこで今回、ABC413B を題材に 集合を使った重複管理、
そして for 文まわりの正しい考え方 を整理してみました。
自分の理解を深めつつ、未来の私のためにしっかり残しておきます。
💭 問題概要
ABC413B - Cutoff(AtCoder Beginner Contest 413)
https://atcoder.jp/contests/abc413/tasks/abc413_b
問題の要点
- N 個の文字列 S₁, S₂, …, Sₙ が与えられる
- i ≠ j のすべての組み合わせについて、文字列 Sᵢ + Sⱼ を作る
- 連結した文字列の「異なる種類数」を求める問題
一見シンプルだけど、実装方針を間違えると一気に迷走するタイプ。
🧩 実装と試行錯誤
最初は「同じ文字列が何回出るか」を数える方向に進んでしまい、完全に方向性を誤りました。
- s[i] == s[j] を比較して count++ しようとしてしまった
- でも問題が求めているのは 連結文字列の種類数
- 二重ループで i と j の境界管理をミスってバグを量産
- range-based for がわからず入力処理すらぎこちない
「なんか違う…」という違和感はあったけど、抜け出せずに小さく沼っていた。
🔍 改善の流れ
✔ set を使えば重複は勝手に消える
これに気付いた瞬間、一気にシンプルになった。
✔ i と j の全組を列挙、ただし i ≠ j
これは二重ループの素直な実装で OK。
✔ range-based for の理解
for (auto &x : s) cin >> x;
これが「s[i] を順に触る構文」だと理解できたのも大きかった。
🚀 最終コード
最終的に AC した C++ コードはこちら。
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<string> s(n); for (auto &x : s) cin >> x; set<string> st; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (i == j) continue; st.insert(s[i] + s[j]); } } cout << st.size() << '\n'; return 0; }
シンプルで迷いなく動く形になった。
🧠 今回の学び
- set / unordered_set の本質は「重複排除と高速検索」
- range-based for は配列全体を安全に操作するための構文
- i ≠ j の列挙は二重ループの素直な形でいい
- データ構造を正しく選べるとコードは一気に短くなる
特に C++ の癖が少しずつ身についてきた感覚があって良い時間だった。
?クイズ
Q1. set を使うメリットは?
- 自動的に重複を排除できる
- 挿入順を保持できる
- すべての操作が常に O(1)
答え:A
Q2. range-based for の auto &x の意味は?
- 値をコピーする
- 元の要素に参照してアクセスする
- 配列を逆順に処理する
答え:B
Q3. 連結文字列を列挙するとき必要な条件は?
- i < j
- i == j
- i ≠ j
答え:C
🪜 躓いたポイント
- set を使えば済むのに「手動で重複チェック」しようとして沼った
- 二重ループの i, j で
i==jを除外し忘れるバグ発生 - range-based for の参照
&の意味に混乱した - C++ の記法にまだ慣れておらず些細なところで時間を使った
同じところでまた迷わないように記録しておきます。
🎯 次回の目標
- map / unordered_map の違いをもっと理論的に理解したい
- C++ と Python の両方で同じ問題を解いて比較する
- ABC の B/C 問題を量でこなして実装速度を上げたい
- コーディングスタイルを安定させたい
小さな一歩だけど、確実に前進してる気がする。次もがんばろう。
🔍 関連キーワード
- AtCoder
- ABC413
- set / unordered_set
- range-based for
- 重複排除
![マネジメント[エッセンシャル版] マネジメント[エッセンシャル版]](https://m.media-amazon.com/images/I/41UGWMZuQRL._SL500_.jpg)

