

実行時間制限: 0 msec / メモリ制限: 0 KiB
キーポイント
- 二次元配列 = リストを要素として持つリストで、数値が縦横に並んだデータ
- 二次元配列
a
に対してa[i][j]
とすることでその i 行 j 列の要素にアクセスできる - 二次元配列
a
に対してlen(a)
とすることでa
の行数を、len(a[0])
とすることでa
の列数を取得できる [list(map(int, input().split())) for _ in range(N)]
とすることで行数 N の二次元配列を入力から受け取ることができる[[0]*M for _ in range(N)]
とすることですべての値が 0 のサイズ N \times M の二次元配列を作ることができる[[] for _ in range(N)]
とすることでサイズ N\times 0 の二次元配列(空リストを N 個含むリスト)を作ることができる
二次元配列
第一章では複数の要素を含むデータ構造であるリストを扱いました。
l = [1,3,5,7]
実はリストの要素は数値に限る必要はありません。次のように様々な型の要素を並べて保持することが可能です:
l = [1, None, "hoge", False]
特に、「数値を要素に持つリスト」を要素として持つリストのことを二次元配列といいます。
a = [[1,3,5],[2,4,6]]
この場合、リストの 0 番目の要素は [1,3,5]
というリスト、1 番目の要素は [2,4,6]
というリストになります。
print(a[0]) print(a[1])
出力
[1, 3, 5] [2, 4, 6]
このように数値が行方向・列方向に並んだものは数学においては行列と呼ばれますが、競技プログラミングにおいても頻出するデータ構造になります。
行列の中の数値に直接アクセスしたい場合、カッコをつなげることでが可能です。
print(a[0][0]) print(a[1][1]) print(a[0][-1])
出力
1 4 5
二次元配列のイメージがうまく湧かない方は C++ 版のこちらの記事も参考にしてみてください。
二重ループと二次元配列
理解を深めるために、2.02節で扱った多重 for 文を利用することで二次元配列の要素を出力してみましょう1。
a = [[1,3,5],[2,4,6]] for i in range(len(a)): print(f"リストの第 {i} 成分は {a[i]} です") for j in range(len(a[i])): print(f"リストの第 ({i}, {j}) 成分は {a[i][j]} です")
出力
リストの第 0 成分は [1, 3, 5] です リストの第 (0, 0) 成分は 1 です リストの第 (0, 1) 成分は 3 です リストの第 (0, 2) 成分は 5 です リストの第 1 成分は [2, 4, 6] です リストの第 (1, 0) 成分は 2 です リストの第 (1, 1) 成分は 4 です リストの第 (1, 2) 成分は 6 です
上記の例では行 (i) 方向と列 (j) 方向について二次元配列全体を走査しています。
具体的には以下のような処理が行われています:
len(a)
とすることで二次元リストの長さ(ここでは2)を取得していますi
を 0 以上len(a)
未満の整数値で動かしながらa[i]
とすることで内側のリストにアクセスしていますlen(a[i])
とすることで内側のリストの長さ(ここでは常に3)を取得していますj
を 0 以上len(a[i])
未満の整数値で動かしながらa[i][j]
とすることで内側のリストの要素(数値)にアクセスしています
練習も兼ねて、同じことをするプログラムの別の書き方の挙げます:
a = [[1,3,5],[2,4,6]] for item in a: print(item) for val in item: print(val)
出力
[1, 3, 5] 1 3 5 [2, 4, 6] 2 4 6
こちらの例では以下のような流れで二次元リスト全体を走査しています:
* for item in a:
とすることで変数 item
に二次元配列の要素(=内側のリスト)を順に格納しています
* for val in item
とすることで内側のリストの要素(=数値)を変数 val
に順に格納しています
二次元配列を入力から取得する
競技プログラミングでは入力として二次元配列が与えられることがしばしばあります。
練習として、次のような入力が与えられる状況を考えます:
N M A_{11} A_{12} \cdots A_{1M} A_{21} A_{22} \cdots A_{2M} \vdots A_{N1} A_{N2} \cdots A_{NM}
1行目に与えられる N は二次元配列の行の数を、M は二次元配列の列の数を示しており、2行目以降には二次元配列の値が N 行にわたって与えられます。各行は M 個の整数からなっています。
この入力から二次元配列を取得してみましょう。以下に3つの方法を示します:
方法 1
N, M = list(map(int, input().split())) a = [] for i in range(N): a.append(list(map(int, input().split())))
方法 2
N, M = list(map(int, input().split())) a = [None]*N for i in range(N): a[i] = list(map(int, input().split()))
方法 3
N, M = list(map(int, input().split())) a = [list(map(int, input().split())) for _ in range(N)]
出力
[[1, 3, 5], [2, 4, 6]]
3つの方法はいずれも、1行目で N, M の値を受け取っています。
list(map(int ...
の書き方は 1.10 節 で扱った、1行に空白区切りで与えられる数値をリストで取得する方法です。1行の中の要素数(ここでは 2)がわかっている場合はこのように 2 つの変数に直接代入することが可能です。
3つの方法それぞれ2行目以降で二次元配列を受け取っています。
- 方法1 においては
a
を最初に空のリストとして定義し、 N 個与えられる行を都度リストに変換してa
にappend
することで二次元配列を得ています。 - 方法2 においては
a
を長さ N のリストとして定義し、i を 0 以上 N 未満の範囲で動かし、N 個与えられる行を都度リストに変換してa[i]
に代入することで二次元配列を得ています。 - 方法3 は前節で扱った内包表記を用いており、1行を読み込んでリストにする処理を N 回行い、二次元配列を得ています。
二次元配列を作成する
要素がすべて 0 の二次元配列を作る
競技プログラミングでは要素がすべて 0 の二次元配列を用意したくなることがしばしばあります。
以下のように書くことでサイズ N \times M の二次元配列を作ることができます:
N = 2 M = 3 a = [[0 for _ in range(M)] for _ in range(N)] # a = [[0] * M for _ in range(N)] としてもよい print(a)
出力
[[0, 0, 0], [0, 0, 0]]
この例では内側の [0 for _ in range(M)]
2 が 0 を M 回取得したリスト、すなわちサイズ M の配列を作る処理になっており、それを N 回行うことでサイズ N \times M の二次元配列を作っています。
コメントしているように、内側のサイズ M の配列を作る処理は [0] * M
のように記述することも可能です。
N\times 0 の二次元配列を作る
後から配列に要素を追加して使う場合などに N\times 0 の配列を宣言することがあります。
以下のように書くと、N\times 0 の二次元配列になります。
N = 10 a = [[] for _ in range(N)] print(a)
出力
[[], [], [], [], [], [], [], [], [], []]
この書き方では内包表記で「空のリストを作る」処理を N 回繰り返すことで N\times 0 の二次元配列を作成しています。
二次元配列を作成する際の注意
行数と列数の指定方法の注意
サイズ N \times M の二次元配列を作る際に、[[0 for _ in range(N)] for _ in range(M)]
のように、N と M を逆にしてしまわないように気を付けてください。内包表記の動作から、最後の range
の中に書いた数値が外側のリストのサイズになります。そのため、N を最後に書くことになります。
二次元配列の初期化方法の注意
サイズ N \times M の二次元配列を初期化する際、次のように書いてしまわないように注意してください:
a = [[0] * M] * N
このように書くと配列自体は作成できますが、いずれかのリストに対する変更が全てのリストに共有されるという不具合が生じてしまいます:
a = [[0] * 2] * 3 print("更新前:", a) a[0][0] = 1 print("更新後:", a)
出力
更新前: [[0, 0], [0, 0], [0, 0]] 更新後: [[1, 0], [1, 0], [1, 0]]
上記のコードでは (0,0) 成分に 1 を代入したため、[[1, 0], [0, 0], [0, 0]]
となることが期待されますが、出力を見ると配列の1列目の値がすべて 1 に変更されてしまっています。
この原因の詳細は「ポインタ」等の未解説の概念が必要になるため省略しますが、この記法では二次元配列内のリストがすべて同じ実体を共有するためこのようなことが起こってしまいます。
同様に、N\times 0 の二次元配列を作る際に以下のように書いてしまわないように注意してください:
N = 10 a = [[]] * N
このように書くと、二次元配列内の空リストがすべて同じ実体を持つため、いずれかのリストに対する変更がすべてのリストに共有されてしまいます:
N = 10 a = [[]] * N print("更新前:", a) a[0].append(100) print("更新後:", a)
出力
更新前: [[], [], [], [], [], [], [], [], [], []] 更新後: [[100], [100], [100], [100], [100], [100], [100], [100], [100], [100]]
各要素の長さが異なる二次元配列
これまでの例では二次元配列の中のすべてのリストが同じ長さを持っていましたが、リストを使っていれば各要素の長さは異なっていても問題ありません。
a = [[] for _ in range(4)] a[0].append(10) a[0].append(20) a[2].append(30) a[3].append(40) a[3].append(50) a[3].append(60) print(a)
出力
[[10, 20], [], [30], [40, 50, 60]]
多次元配列
二次元配列を拡張して、三次元、四次元... の配列を作ることも可能です。
次の例では三次元配列を内包表記によって初期化し、次に三重ループでその要素を書き換えています:
N = 2 M = 2 D = 2 lst3d = [[[0]*D for _ in range(M)] for _ in range(N)] for i in range(N): for j in range(M): for k in range(D): lst3d[i][j][k] = (i,j,k) print(lst3d)
出力
[[[(0, 0, 0), (0, 0, 1)], [(0, 1, 0), (0, 1, 1)]], [[(1, 0, 0), (1, 0, 1)], [(1, 1, 0), (1, 1, 1)]]]
[[[0]*D for _ in range(M)] for _ in range(N)]
の部分で三次元配列を作っています。
この中の [[0]*D for _ in range(M)]
はサイズ M \times D の配列を作っています。この処理を後ろから for _ in range(N)
で囲むことで、「サイズ M \times D の配列 N 個からなるリスト」すなわちサイズ N \times M \times D の配列を得ています。
より一般に、K 次元配列を作るためには一般には複数の K-1 次元配列を作り、それをリストに入れればよいです。
ABCの問題
ここまでの知識で解ける問題をAtCoder Beginner Contestの過去問題から紹介します。練習問題だけでは物足りない人は挑戦してみてください。
問題
リンク先の問題を解いてください。