C++で2次元配列を扱いたい場合、よく使われるのが vector を入れ子にした方法です。
通常の配列では、サイズをあらかじめ決めておく必要があります。
一方で vector を使うと、実行時に入力された行数や列数に応じて、柔軟にサイズを決められます。
たとえば、競技プログラミングや実務のデータ処理では、「縦が何行、横が何列になるか」が実行時まで分からないことがあります。
そのような場面では、固定長配列よりも vector を使った2次元配列の方が扱いやすいです。
2次元vectorの基本的な考え方
2次元vectorとは
2次元vectorは、簡単に言うと「vectorの中にvectorが入っている構造」です。
外側のvectorが「行」を管理し、内側のvectorが「各行の列」を管理します。
イメージとしては、次のような表です。
1行目に複数の要素があり、2行目にも複数の要素があり、3行目にも複数の要素がある、という形です。
つまり、2次元vectorは「表」「グリッド」「行列」のようなデータを扱うときに便利です。
行と列の考え方
2次元配列では、基本的に「行」と「列」で位置を指定します。
一般的には、最初の添字が行、次の添字が列を表します。
たとえば、ある要素を指定するときは、「何行目の何列目」という形で考えます。
ただし、C++の添字は0から始まります。
そのため、1行目は0番目の行、2行目は1番目の行、3行目は2番目の行として扱います。
同じように、1列目は0番目の列、2列目は1番目の列です。
この0始まりの考え方は、2次元vectorを扱ううえで非常に重要です。
2次元vectorの作り方
行数と列数を指定して作る
2次元vectorでは、行数と列数を指定して、あらかじめ必要な大きさの表を作ることができます。
たとえば、3行4列のような形です。
この場合、外側に3つの行を用意し、それぞれの行に4つの要素を持たせる、という考え方になります。
整数型の2次元vectorであれば、初期値を指定しない場合、各要素は基本的に0で初期化されます。
ただし、実際のコードを書くときには、初期値を明示した方が読みやすく、安全なことが多いです。
初期値を指定して作る
2次元vectorでは、すべての要素に同じ初期値を設定して作ることもできます。
たとえば、すべての要素を0にしたい場合、すべてをマイナス1にしたい場合、すべてを非常に大きな値にしたい場合などです。
特に、探索や動的計画法では初期値の指定がよく使われます。
たとえば、まだ訪問していない場所をマイナス1で表す、到達できない距離を大きな値で表す、未処理の状態を0で表す、といった使い方があります。
要素へのアクセス
添字を使ってアクセスする
2次元vectorの要素には、行と列を指定してアクセスします。
考え方は通常の2次元配列と同じです。
たとえば、「2行目の3列目の値を取り出す」「1行目の4列目に値を代入する」といった操作ができます。
ただし、C++では添字が0から始まるため、人間が考える「2行目」は、プログラム上では1番目の行になります。
このズレを意識しておかないと、範囲外アクセスの原因になります。
範囲外アクセスに注意する
2次元vectorで特に注意すべきなのが、範囲外アクセスです。
たとえば、3行4列のデータであれば、行として使える添字は0、1、2です。
列として使える添字は0、1、2、3です。
存在しない4行目や5列目にアクセスしようとすると、プログラムが正しく動作しない可能性があります。
C++では範囲外アクセスをしても、必ず分かりやすいエラーになるとは限りません。
場合によっては、実行時エラーになったり、予期しない値が出たり、原因の分かりにくいバグにつながったりします。
そのため、2次元vectorを扱うときは、常に行数と列数の範囲を意識することが大切です。
入力を受け取る場合
整数の表を入力する
2次元vectorは、標準入力から表形式のデータを受け取るときによく使われます。
たとえば、最初に行数と列数が与えられ、その後に各マスの値が与えられる形式です。
このような場合、まず入力された行数と列数に合わせて2次元vectorを作り、その後、二重ループで各要素を順番に読み込むのが一般的です。
外側のループで行を進め、内側のループで列を進める、という考え方です。
出力するときも二重ループを使う
2次元vectorの中身を表示したい場合も、基本的には二重ループを使います。
外側のループで1行ずつ処理し、内側のループでその行の各要素を出力します。
1行分の出力が終わったら改行することで、表のような形で表示できます。
この「入力も出力も二重ループで行う」という考え方は、2次元配列全般で非常によく使われます。
文字の2次元配列を扱う場合
文字グリッドにはvector stringが便利
迷路や盤面のような文字のグリッドを扱う場合、2次元vectorを使うこともできますが、実際には vector<string> がよく使われます。
たとえば、ドットやシャープで構成されたマップ、英小文字で構成された盤面、障害物と通路を表すグリッドなどです。
vector<string> は、文字列を行として持つ構造です。
各文字列の中の文字にアクセスすれば、実質的に2次元の文字配列のように扱えます。
vector stringが向いている場面
vector<string> は、各行が空白を含まない文字列として与えられる場合に特に便利です。
競技プログラミングでよくある、迷路や盤面の入力では、ほとんどの場合この形が使えます。
行ごとに文字列として読み込めるため、文字を1つずつ入力するよりも簡潔に書けます。
空白を含む入力には注意する
ただし、行の中に空白が含まれる場合は注意が必要です。
通常の入力方法では、空白で文字列が区切られてしまうため、行全体をそのまま読み取ることができません。
空白を含む1行をそのまま扱いたい場合は、行全体を読み込む方法を使う必要があります。
通常のグリッド問題では空白を含まないことが多いため、基本的には vector<string> を使えば十分です。
行数と列数の取得
行数の取得
2次元vectorでは、外側のvectorのサイズを確認することで行数を取得できます。
つまり、外側にいくつの行があるかを調べれば、それが行数になります。
列数の取得
列数は、特定の行のサイズを確認することで取得できます。
通常、すべての行の長さが同じであれば、最初の行のサイズを見れば列数が分かります。
ただし、2次元vectorが空の場合、最初の行は存在しません。
その状態で最初の行にアクセスしようとすると危険です。
そのため、列数を取得する前に、行が存在するかどうかを確認するのが安全です。
行ごとに列数が違う可能性もある
2次元vectorは、必ずしもすべての行の列数が同じである必要はありません。
各行は独立したvectorなので、1行目は3列、2行目は5列、3行目は1列というような形も作れます。
このような構造は「ジャグ配列」や「不規則な2次元配列」のように考えることができます。
ただし、一般的な表やグリッドとして使う場合は、すべての行の列数をそろえることが多いです。
範囲for文での走査
読み取りだけなら参照を使う
2次元vectorは、範囲for文を使って走査することもできます。
各行を順番に取り出し、その行の中の要素をさらに順番に取り出す、という形です。
読み取りだけであれば、各行をコピーせずに参照として受け取ると効率的です。
特に、2次元vectorはデータ量が大きくなりやすいため、不要なコピーは避けた方がよいです。
値を書き換える場合も参照が必要
要素の値を書き換えたい場合は、行だけでなく要素も参照として扱う必要があります。
参照を使わずに値を取り出すと、元のデータではなくコピーを変更してしまう場合があります。
そのため、2次元vectorの中身を直接変更したい場合は、参照を使うという意識が重要です。
行や要素の追加
行を追加する
2次元vectorでは、後から行を追加できます。
たとえば、最初は空の状態にしておき、必要に応じて1行ずつ追加していくことができます。
この方法は、入力される行数が事前に分からない場合や、条件に応じて行を増やしたい場合に便利です。
特定の行に要素を追加する
各行はそれぞれ独立したvectorなので、特定の行にだけ要素を追加することもできます。
その結果、行ごとに列数が異なる2次元vectorを作ることもできます。
ただし、通常のグリッドとして扱う場合は、行ごとの列数が違うと処理が複雑になりやすいです。
そのため、表や盤面として扱うなら、なるべく全行の列数をそろえた方が分かりやすいです。
サイズ変更の注意点
resizeの基本
2次元vectorでは、後から行数を変更することができます。
ただし、行数を変更する操作と、各行の列数を変更する操作は別物です。
外側のvectorをリサイズすると、行数は変わります。
しかし、すでに存在している各行の列数が自動的に変わるとは限りません。
既存の行はそのまま残る
特に注意すべきなのは、すでにデータが入っている2次元vectorに対してサイズ変更を行う場合です。
新しく追加される行には指定した列数や初期値を設定できますが、既存の行の列数は基本的にそのまま残ります。
そのため、「完全に指定した行数・列数の表に作り直したい」という場合には、単なるサイズ変更ではなく、全体を作り直す方法を使った方が分かりやすいです。
作り直したい場合はassignが便利
既存の内容を破棄して、新しい行数・列数・初期値で作り直したい場合は、再代入や全体の再初期化を行うのが適しています。
これにより、古い行の列数が残るといった問題を避けられます。
2次元vectorのサイズ変更では、「行数だけを変えたいのか」「全体を作り直したいのか」を明確に区別することが大切です。
関数に渡すときの考え方
読み取りだけならconst参照
2次元vectorを関数に渡すときは、基本的にコピーを避けるべきです。
2次元vectorはサイズが大きくなりやすいため、値渡しをすると全体のコピーが発生し、処理が重くなることがあります。
読み取りだけでよい場合は、変更しない参照として渡すのが一般的です。
これにより、コピーを避けつつ、関数の中で誤って内容を変更することも防げます。
書き換えるなら通常の参照
関数の中で2次元vectorの内容を変更したい場合は、変更可能な参照として渡します。
たとえば、すべての要素を同じ値にする、訪問済みの状態を更新する、距離配列を書き換える、といった場合です。
この場合、関数内で変更した内容は、呼び出し元の2次元vectorにも反映されます。
値渡しは基本的に避ける
2次元vectorを値渡しすると、関数呼び出しのたびにデータ全体がコピーされます。
小さなデータであれば大きな問題にならないこともありますが、基本的には避けた方がよいです。
特に、グリッドやDPテーブルのように大きな配列を扱う場合、値渡しはパフォーマンス低下の原因になります。
よく使う実用パターン
距離配列
幅優先探索などでは、各マスまでの距離を管理するために2次元vectorを使います。
このとき、まだ訪問していないマスをマイナス1で表すことがよくあります。
マイナス1で初期化しておけば、「そのマスに到達済みかどうか」を簡単に判定できます。
訪問済み配列
探索処理では、あるマスや状態をすでに訪問したかどうかを管理するために、訪問済み配列を使います。
真偽値で管理する方法もありますが、C++の vector<bool> は特殊な実装になっているため、初心者のうちは整数型や文字型で管理する方が分かりやすい場合があります。
たとえば、未訪問を0、訪問済みを1として扱う方法はシンプルで理解しやすいです。
コスト配列
最短経路問題や動的計画法では、各マスや各状態のコストを管理するために2次元vectorを使うことがあります。
この場合、初期値として非常に大きな値を入れておき、より小さいコストが見つかったら更新する、という形がよく使われます。
DPテーブル
動的計画法でも2次元vectorはよく使われます。
たとえば、「何番目まで見たか」と「現在の状態は何か」のように、2つの軸で状態を管理する場合です。
DPテーブルでは、行数や列数を1つ多めに確保することもよくあります。
これは、0番目の状態や空の状態を扱いやすくするためです。
vector<vector>のメモリ構造
全体が連続しているとは限らない
2次元vectorは、見た目としては表のように扱えますが、メモリ上で全要素が1本の連続した領域に並んでいるとは限りません。
外側のvectorが各行のvectorを持ち、それぞれの行が独立してメモリを確保しています。
そのため、各行の中身は連続していても、行同士が連続している保証はありません。
通常の用途では問題になりにくい
多くの場合、この点を強く意識する必要はありません。
通常の入力処理、グリッド探索、DP、表データの管理などでは、2次元vectorで十分扱いやすく、実用上も問題ありません。
ただし、非常に大きなデータを扱う場合や、性能を細かく最適化したい場合には、メモリ配置が重要になることがあります。
1次元vectorで2次元配列のように扱う方法
1次元で持つ考え方
高速化やメモリ効率を意識する場合、2次元のデータを1次元vectorとして持つ方法もあります。
この方法では、行数と列数をもとに、全体を1本の配列として管理します。
見た目は1次元ですが、添字の計算によって「何行目の何列目」に対応する位置を求めます。
メリット
1次元vectorで管理すると、全体が連続したメモリ領域に並ぶため、キャッシュ効率が良くなる場合があります。
また、行ごとにvectorを持つ必要がないため、管理上のオーバーヘッドを減らせることがあります。
巨大な配列を扱う場合や、処理速度を重視する場面では有効です。
デメリット
一方で、1次元vectorで2次元データを扱う場合、添字の計算が必要になります。
通常の2次元vectorのように、「行」と「列」をそのまま指定して直感的にアクセスすることはできません。
そのため、コードの読みやすさはやや下がります。
添字計算を間違えるとバグにつながりやすいため、初心者のうちはまず2次元vectorに慣れるのがおすすめです。
vector<vector>と通常の配列の違い
通常の配列はサイズ固定
通常の2次元配列では、サイズをコンパイル時に決めておく必要があります。
つまり、プログラムを書く段階で行数と列数が分かっている場合には使いやすいですが、入力によってサイズが変わる場合には扱いにくいです。
一部のコンパイラでは、実行時に決まるサイズの配列を書ける場合もありますが、これは標準C++の機能ではありません。
移植性を考えるなら、実行時にサイズが決まる配列にはvectorを使う方が安全です。
vectorはサイズを動的に決められる
vectorの大きな利点は、実行時にサイズを決められることです。
入力された行数や列数に応じて、必要な分だけメモリを確保できます。
また、後から要素を追加したり、サイズを変更したりできるため、通常の配列より柔軟です。
vectorという選択肢
列数が固定なら使える
列数があらかじめ決まっている場合には、vectorとarrayを組み合わせる方法もあります。
この方法では、行数はvectorで動的に管理し、各行の列数はarrayで固定します。
たとえば、「列数は常に4だが、行数だけは入力によって変わる」というような場面では使えます。
入力で列数が決まる場合には向かない
ただし、arrayのサイズはコンパイル時に決まっている必要があります。
そのため、列数が入力によって変わる場合には使いにくいです。
行数も列数も実行時に決めたい場合は、やはり2次元vectorを使うのが自然です。
初期化には注意が必要
vectorとarrayを組み合わせる場合、初期化の挙動を意識する必要があります。
明確に0で初期化したい場合は、初期化方法をきちんと指定する方が安全です。
初心者がまず覚えるべきなのは、vectorとarrayの組み合わせではなく、2次元vectorの基本形です。
よくある間違い
外側と内側の型を混同する
2次元vectorでは、外側のvectorの要素は「内側のvector」です。
そのため、外側のvectorを作るときに、単なる整数を渡してしまうと型が合いません。
行数だけでなく、「各行にどのようなvectorを入れるのか」を指定する必要があります。
この点は初心者がよく間違えるポイントです。
添字の範囲を超える
2次元vectorで最も多いミスの1つが、添字の範囲外アクセスです。
行数がHなら、使える行の添字は0からHマイナス1までです。
列数がWなら、使える列の添字は0からWマイナス1までです。
H番目の行やW番目の列にはアクセスできません。
空のvectorで最初の行にアクセスする
列数を取得しようとして、最初の行にアクセスすることがあります。
しかし、行数が0の場合、最初の行は存在しません。
その状態で最初の行を参照すると危険です。
列数を確認する前に、2次元vectorが空でないかを確認する習慣を持つと安全です。
resizeで全体が作り直されると誤解する
resizeは便利ですが、既存の行の列数まで自動でそろえてくれるわけではありません。
すでにある行は基本的にそのまま残り、新しく追加される行にだけ指定した形が適用されます。
完全に新しいサイズで作り直したい場合は、再初期化する方法を使うべきです。
初心者がまず覚えるべき使い方
基本は2次元vectorで十分
C++で2次元配列を扱うなら、まずは2次元vectorを使う方法を覚えるのがよいです。
特に、行数と列数が入力で与えられる場合には、2次元vectorが非常に便利です。
整数の表、距離配列、DPテーブル、盤面情報など、幅広い場面で使えます。
文字グリッドはvector stringが便利
文字で構成されたグリッドを扱う場合は、2次元vectorよりも vector<string> の方が簡潔に書けることが多いです。
競技プログラミングの迷路問題や盤面問題では、こちらの形がよく使われます。
高速化が必要なら1次元vectorも検討する
通常は2次元vectorで問題ありませんが、巨大なデータを扱う場合や、メモリ効率・キャッシュ効率を意識したい場合は、1次元vectorで2次元データを表す方法もあります。
ただし、可読性は少し下がるため、まずは2次元vectorを使いこなせるようになるのが先です。
前回内容の正確性について
大きな間違いはない
前回の説明内容には、大きな間違いはありません。
2次元vectorの基本的な作り方、要素へのアクセス方法、入力と出力の方法、文字グリッドでの使い方、関数への渡し方、1次元vectorで代用する方法などは、いずれも正しい内容です。
そのまま学習用として使って問題ありません。
補足した方がよい点はある
ただし、より正確にするなら、いくつか補足した方がよい点があります。
特に、サイズ変更の挙動、文字型で訪問済み配列を作る場合の初期値、vectorとarrayを組み合わせる場合の初期化については、少し詳しく説明した方が安全です。
これらは重大な誤りではありませんが、初心者が誤解しやすい部分です。
特に注意すべき補足点
resizeは既存行の列数を変えない
空の2次元vectorに対してサイズを設定する場合は、指定した行数と列数の形になります。
しかし、すでに行が存在している場合、後からresizeしても既存の行の列数は変わらないことがあります。
新しく追加された行だけが、指定した列数や初期値で作られます。
そのため、「完全に指定した行数・列数に作り直したい」ときは、resizeだけに頼らない方が安全です。
char型の訪問済み配列は0初期化が自然
訪問済み管理にchar型を使うことはできます。
ただし、char型に真偽値のfalseを入れるよりも、数値の0で初期化する方が自然で分かりやすいです。
未訪問を0、訪問済みを1として扱うと、整数型と同じ感覚で使えます。
vector boolは特殊な実装になっている
C++の vector<bool> は、通常のvectorとは少し異なる特殊な実装になっています。
メモリ効率を良くするために、bool値をビット単位で詰めて管理するような仕組みになっています。
そのため、普通のvectorとまったく同じ感覚で扱うと、場合によっては違和感のある挙動に出会うことがあります。
初心者のうちは、訪問済み管理に整数型を使う方が理解しやすいです。
vector arrayは列数固定の場合だけ便利
vectorとarrayを組み合わせる方法は、列数がコンパイル時に決まっている場合には便利です。
しかし、入力によって列数が変わる場合には使えません。
また、初期化についても少し注意が必要です。
そのため、汎用的に使うなら、2次元vectorの方が分かりやすく安全です。
まとめ
C++で2次元配列を扱うなら、基本は2次元vectorを使う方法を覚えれば十分です。
2次元vectorは、行数と列数を実行時に決められるため、入力に応じて柔軟にサイズを変えられます。
要素には、行と列を指定してアクセスします。
ただし、添字は0から始まるため、範囲外アクセスには注意が必要です。
整数の表やDPテーブル、距離配列、訪問済み配列などには2次元vectorがよく使われます。
文字のグリッドを扱う場合は、2次元vectorよりも vector<string> の方が便利なことが多いです。
また、非常に大きなデータを扱う場合や高速化を意識する場合は、1次元vectorで2次元配列のように扱う方法もあります。
前回の説明は全体として正しく、学習用として問題ありません。
ただし、より正確に理解するなら、次の点を押さえておくとよいです。
resizeは既存行の列数まで自動で変更するわけではありません。
char型の訪問済み配列では、falseよりも0で初期化する方が自然です。
vector boolは特殊な実装になっているため、初心者のうちは整数型で訪問済みを管理する方が分かりやすいです。
vector arrayは列数が固定の場合には使えますが、入力で列数が決まる場合には向いていません。
まずは、2次元vectorの基本的な考え方、行と列の扱い、0始まりの添字、範囲外アクセスの注意点をしっかり理解することが大切です。
以上、C++のvectorで2次元配列を扱う方法についてでした。
最後までお読みいただき、ありがとうございました。
