Python AtCoder入門 第8講 2次元リストとグリッド問題

-- Views

September 21, 26

スライド概要

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

Python AtCoder入門 第8講 2次元リストとグリッド問題 AtCoderのB問題には、マス目 を扱う問題がよく出ます。 迷路、盤面、地図、表。 形は違っても、正体はすべて「縦H行・横W列に並んだマス」です。 1

2.

今回のテーマ こうした問題を扱うための道具が、 2次元リスト です。 リストの中にリストが入っているもの、と考えてください。 2

3.

この講のポイント 新しい概念は少なめです。 使うのは主に、 第4講の二重ループ 第5講のリスト 第6講の内包表記 です。 3

4.

重要な罠 ただし、1つだけ知らないと必ずハマる罠があります。 [[0] * W] * H です。 これは一見便利ですが、2次元リストでは使ってはいけません。 4

5.

コードファイル名の方針 この講でも、コード例ごとにファイル名を付けます。 grid_basic.py bad_grid.py char_grid.py numeric_grid.py neighbors.py answer_8_1.py 5

6.

8-1 2次元リストの基本 2次元リストは、 リストを要素に持つリスト です。 grid = [[1, 2, 3], [4, 5, 6]] これは2行3列のマス目を表します。 6

7.

grid[i][j] 要素には、 grid[i][j] でアクセスします。 print(grid[0][0]) print(grid[1][2]) 出力: 1 6 7

8.

iが行、jが列 grid[i][j] の、 が行 j が列 です。 「上からi番目の行を取り出して、その中のj番目」と読みます。 どちらも0始まりです。 i 8

9.

行数と列数 H = len(grid) W = len(grid[0]) が行数 len(grid[0]) が列数 です。 grid は「行のリスト」だからです。 len(grid) 9

10.

内包表記による初期化 すべて0で埋めたH行W列のマス目は、次のように作ります。 grid = [[0] * W for _ in range(H)] 「長さWの行を、H個作る」という意味です。 10

11.

grid_basic.py H = 3 W = 4 # Create an H x W grid filled with zeros grid = [[0] * W for _ in range(H)] # Set some cells grid[0][0] = 1 grid[1][2] = 5 grid[2][3] = 9 # Print row by row for row in grid: print(*row) # Size of the grid print(len(grid), len(grid[0])) 11

12.

grid_basic.py の出力 1 0 0 0 0 0 5 0 0 0 0 9 3 4 出力は、 for row in grid: print(*row) で1行ずつ行います。 12

13.

print(grid) は答え向きではない print(grid) とすると、角括弧だらけになります。 AtCoderの答えとしては、多くの場合WAです。 1行ずつ取り出して、 print(*row) で出力しましょう。 13

14.

[[0] * W] * H がダメな理由 次の2つは一見同じに見えます。 grid = [[0] * W for _ in range(H)] grid = [[0] * W] * H # 正しい # 壊れる 下の書き方は、絶対に使ってはいけません。 14

15.

何が壊れるのか [[0] * W] * H は、行をH個複製しているように見えます。 しかし実際には、 同じ1つの行への参照をH個並べているだけ です。 15

16.

1マス変えると全行が変わる 同じ行を何度も指しているため、 bad[0][0] = 1 とすると、すべての行の先頭が 1 になります。 第5講の「コピーの罠」と同じです。 16

17.

bad_grid.py H = 3 W = 4 # The wrong way bad = [[0] * W] * H bad[0][0] = 1 for row in bad: print(*row) print(bad[0] is bad[1]) print("---") # The right way good = [[0] * W for _ in range(H)] good[0][0] = 1 for row in good: print(*row) print(good[0] is good[1]) 17

18.

bad_grid.py の出力 1 0 0 0 1 0 0 0 1 0 0 0 True --1 0 0 0 0 0 0 0 0 0 0 0 False bad では3行すべてが変わっています。 18

19.

is の意味 bad[0] is bad[1] は、 まったく同じものか を調べています。 True なら、0行目と1行目が同じリストを指しているという意味です。 19

20.

鉄則 2次元リストは必ず内包表記で作ります。 grid = [[0] * W for _ in range(H)] 1次元なら [0] * W で問題ありません。 罠になるのは「リストを * で繰り返したとき」です。 20

21.

8-2 グリッド入力の受け取り AtCoderでよく出るのが、 # と . などが並んだマス目です。 3 4 #..# .##. #..# 1行目にHとW、続くH行にマス目が与えられます。 21

22.

文字グリッドの受け取り 文字グリッドは、次の形で受け取れます。 H, W = map(int, input().split()) S = [input() for _ in range(H)] これで S は文字列のリストになります。 22

23.

文字列のままアクセスできる 文字列はインデックスでアクセスできます。 print(S[0][0]) print(S[1][1]) でi行目の文字列。 その [j] でj文字目です。 2次元リストと同じ感覚で使えます。 S[i] 23

24.

書き換えたい場合 文字列はイミュータブルなので、書き換えられません。 マス目を書き換えたい場合は、1文字ずつのリストにします。 S = [list(input()) for _ in range(H)] S[0][0] = "." 読むだけなら文字列のままで十分です。 24

25.

char_grid.py # Read the grid size H, W = map(int, input().split()) # Read the grid as a list of strings S = [input() for _ in range(H)] # Count the '#' cells count = 0 for i in range(H): for j in range(W): if S[i][j] == "#": count += 1 print(count) 25

26.

char_grid.py の実行例 入力例: 3 4 #..# .##. #..# 出力例: 6 全マスを二重ループで見ています。 26

27.

全マス走査が基本 グリッド問題の基本は、 for i in range(H): for j in range(W): # マス (i, j) について処理 です。 第4講の二重ループそのものです。 27

28.

数値グリッド 数値が空白区切りで並ぶ形式もあります。 2 3 1 2 3 4 5 6 この場合は、各行を整数リストとして受け取ります。 28

29.

数値グリッドの受け取り A = [list(map(int, input().split())) for _ in range(H)] 第1講の入力テンプレートを、H行ぶん繰り返しています。 内包表記で短く書けます。 29

30.

numeric_grid.py # Read the grid size H, W = map(int, input().split()) # Read the numeric grid A = [list(map(int, input().split())) for _ in range(H)] # Sum of each row for i in range(H): print(sum(A[i])) # Sum of each column for j in range(W): total = 0 for i in range(H): total += A[i][j] print(total) 30

31.

numeric_grid.py の実行例 入力例: 2 3 1 2 3 4 5 6 出力例: 6 15 5 7 9 31

32.

行の合計 行の合計は簡単です。 sum(A[i]) A[i] がi行目のリストそのものだからです。 32

33.

列の合計 列の合計は、一発では取れません。 A[0][j] A[1][j] A[2][j] のように、縦にたどる必要があります。 「何を固定し、何を動かすか」を意識しましょう。 33

34.

8-3 グリッド上の探索 グリッド問題の基本は、 二重ループで全マスを見る ことです。 for i in range(H): for j in range(W): # マス (i, j) について処理 34

35.

count_cells.py # Read the grid size and the threshold H, W = map(int, input().split()) A = [list(map(int, input().split())) for _ in range(H)] K = int(input()) # Count the cells whose value is K or more count = 0 for i in range(H): for j in range(W): if A[i][j] >= K: count += 1 print(count) 35

36.

count_cells.py の実行例 入力例: 2 3 1 5 3 8 2 9 5 出力例: 3 5 , 8 , 9 の3つです。 36

37.

隣接マス グリッド問題では、 あるマスの上下左右を調べる 処理がよく出ます。 マス (i, j) の隣は、 上: (i - 1, j) 下: (i + 1, j) 左: (i, j - 1) 右: (i, j + 1) 37

38.

方向ベクトル 上下左右の移動量をリストにまとめます。 directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] これを 方向ベクトル と呼びます。 中身はタプルのリストです。 38

39.

方向ベクトルを使う for di, dj in directions: ni = i + di nj = j + dj は行の変化量 dj は列の変化量 ni , nj は隣のマス です。 di 39

40.

範囲外チェック 隣を見るときは、必ず範囲外チェックをします。 if 0 <= ni < H and 0 <= nj < W: # ここで初めてアクセスしてよい 範囲外のマスにアクセスすると、エラーやWAの原因になります。 40

41.

負のインデックスに注意 Pythonでは、 S[-1] がエラーになりません。 最後の行を指してしまいます。 範囲外チェックを忘れると、エラーが出ずに間違った答えになることがあります。 41

42.
[beta]
neighbors.py
# Read the grid
H, W = map(int, input().split())
S = [input() for _ in range(H)]
# Four directions: up, down, left, right
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# For each cell, count the adjacent '#' cells
for i in range(H):
row = []
for j in range(W):
count = 0
for di, dj in directions:
ni = i + di
nj = j + dj
if 0 <= ni < H and 0 <= nj < W:
if S[ni][nj] == "#":
count += 1
row.append(count)
print(*row)

42

43.

neighbors.py の実行例 入力例: 3 3 .#. ### .#. 出力例: 2 1 2 1 4 1 2 1 2 43

44.

全マス × 4方向 このコードは三重ループに見えます。 しかし、いちばん内側は必ず4回です。 つまり計算量は、 H × W × 4 です。 グリッド問題で何度も使う骨格です。 44

45.

章末まとめ 2次元リストは、 リストのリスト です。 grid[i][j] の、 が行 j が列 です。 どちらも0始まりです。 i 45

46.

章末まとめ:初期化 2次元リストの初期化は、 grid = [[0] * W for _ in range(H)] です。 これは使ってはいけません。 grid = [[0] * W] * H 46

47.

章末まとめ:入力 文字グリッド: S = [input() for _ in range(H)] 書き換えるなら: S = [list(input()) for _ in range(H)] 数値グリッド: A = [list(map(int, input().split())) for _ in range(H)] 47

48.

章末まとめ:走査 グリッド問題の基本は、二重ループです。 for i in range(H): for j in range(W): # マス (i, j) 全マスを1つずつ見ます。 48

49.

章末まとめ:隣接マス 上下左右は方向ベクトルで扱います。 directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] 範囲外チェックを必ず書きます。 if 0 <= ni < H and 0 <= nj < W: 49

50.

練習問題 8-1 黒いマスの数 H行W列のマス目があります。 各マスは、 # :黒 . :白 です。 黒いマスの個数を出力してください。 50

51.

練習問題 8-1:入力と出力 入力: H W S_1 S_2 ... S_H 入力例: 3 4 #..# .##. #..# 出力例: 6 51

52.

answer_8_1.py # Read the grid H, W = map(int, input().split()) S = [input() for _ in range(H)] # Count the '#' cells count = 0 for i in range(H): for j in range(W): if S[i][j] == "#": count += 1 print(count) 全マス走査とカウントパターンです。 52

53.

answer_8_1_count.py # Read the grid H, W = map(int, input().split()) S = [input() for _ in range(H)] # Count '#' in each row and sum them up print(sum(row.count("#") for row in S)) 文字列の count() を使うと短く書けます。 53

54.

練習問題 8-2 行と列の最大合計 H行W列の数値が並んだ表があります。 各行の合計の最大値と、各列の合計の最大値を、この順に空白区切りで出力してください。 54

55.

練習問題 8-2:入力と出力 入力例: 2 3 1 2 3 4 5 6 出力例: 15 9 行の最大合計は 15 。 列の最大合計は 9 です。 55

56.

answer_8_2.py # Read the numeric grid H, W = map(int, input().split()) A = [list(map(int, input().split())) for _ in range(H)] # Sum of each row row_sums = [] for i in range(H): row_sums.append(sum(A[i])) # Sum of each column col_sums = [] for j in range(W): total = 0 for i in range(H): total += A[i][j] col_sums.append(total) print(max(row_sums), max(col_sums)) 56

57.

行の合計は内包表記でも書ける row_sums = [sum(A[i]) for i in range(H)] 行は A[i] でそのまま取り出せます。 列は縦にたどる必要があります。 57

58.

練習問題 8-3 孤立した黒マス H行W列のマス目があります。 黒いマスのうち、 上下左右のいずれにも黒いマスが隣接していない ものを「孤立している」と呼びます。 孤立している黒いマスの個数を出力してください。 58

59.

練習問題 8-3:入力と出力 入力例: 3 4 #..# .##. #..# 出力例: 4 四隅の4つの # が孤立しています。 59

60.
[beta]
answer_8_3.py
# Read the grid
H, W = map(int, input().split())
S = [input() for _ in range(H)]
# Four directions: up, down, left, right
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# Count isolated '#' cells
answer = 0
for i in range(H):
for j in range(W):
if S[i][j] != "#":
continue
neighbors = 0
for di, dj in directions:
ni = i + di
nj = j + dj
if 0 <= ni < H and 0 <= nj < W:
if S[ni][nj] == "#":
neighbors += 1
if neighbors == 0:
answer += 1
print(answer)

60

61.

answer_8_3.py のポイント 白いマスは判定する必要がありません。 if S[i][j] != "#": continue で次のマスへ進みます。 隣接する黒マスの数が 0 なら、孤立しています。 61

62.

第8講まとめ この講では、 2次元リストとグリッド問題 を学びました。 B問題では、マス目を二重ループで走査する問題がよく出ます。 62

63.

次回予告 次の第9講では、 関数と組み込み関数 を扱います。 ここまで書いてきた処理に名前を付けて整理する方法と、Pythonが用意している便利な関数を学びます。 63