칸을 세로로도 세자! 2차원 리스트
안녕, 나 코딩냥이다냥! 지금까지 다룬 리스트는 전부 한 줄이었다냥. 그런데 지도, 사진, 바둑판, 엑셀 표처럼 세상엔 가로세로로 된 것이 훨씬 많다냥. 오늘은 그걸 담는 그릇을 배운다냥. 새로운 문법은 거의 없고, 3장에서 배운 중첩 반복문을 그대로 쓴다냥.
오늘의 학습 목표
- 리스트 안의 리스트로 격자를 만들고
grid[r][c]로 읽는다. - 격자를 만들 때의 복사 함정을 피한다.
- 중첩 반복문으로 행 방향과 열 방향을 각각 훑는다.
- 이웃 칸을 볼 때 격자 밖으로 나가지 않게 막는다.
1단계 · 리스트 안에 리스트를 넣는다냥 (7분)
2차원 리스트는 이름이 거창하지만 실체는 간단하다냥. 리스트를 원소로 갖는 리스트라냥. 그게 전부라냥.
grid = [
[1, 2, 3],
[4, 5, 6],
]
print(grid[0]) # [1, 2, 3] — 0번 행 전체
print(grid[0][2]) # 3 — 0번 행의 2번 칸
print(len(grid)) # 2 — 행의 개수
print(len(grid[0])) # 3 — 열의 개수비유로 이해하기 — 아파트
grid[r][c] 를 아파트 주소라고 생각하면 편하다냥.
동을 먼저 찾고 호수를 찾는다냥. 그래서 항상 행이 먼저, 열이 나중이라냥.
grid[r][c] 에서 r과 c를 바꿔 쓰는 게 이 단원 실수의 절반이라냥.
헷갈릴 때는 "행 먼저, 열 나중" 만 외우자냥. 수학의 좌표 (x, y) 와
순서가 반대라서 더 헷갈린다냥. 격자에서는 (세로, 가로) 라냥.
2단계 · 격자를 만들 때의 함정 (8분)
여기가 오늘의 가장 중요한 부분이라냥. 이거 하나 때문에 몇 시간씩 헤매는 사람이 정말 많다냥. 집중해서 보자냥.
0 으로 채운 2행 3열 격자를 만들고 싶다냥. 이렇게 쓰면 어떻게 되냥?
grid = [[0] * 3] * 2 # 이러면 안 된다냥!
grid[0][0] = 9
print(grid) # [[9, 0, 0], [9, 0, 0]]한 칸만 바꿨는데 두 줄이 같이 바뀌었다냥.
* 2 는 안쪽 리스트를 복사하지 않는다냥. 같은 리스트를 가리키는
화살표만 두 개 만든다냥. 그래서 어느 쪽을 고쳐도 둘 다 바뀐다냥.
비유로 이해하기 — 공유 문서 링크
친구에게 문서 링크를 두 번 보낸 것과 같다냥. 링크가 두 개라고 문서가 두 개가 되진 않는다냥. 누가 고치든 같은 문서 하나가 바뀐다냥.
진짜 두 개가 필요하면 문서를 복사해서 각각 보내야 한다냥.
grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 9
print(grid) # [[9, 0, 0], [0, 0, 0]]for _ in range(2) 가 돌 때마다 [0] * 3 이 새로 실행된다냥. 그래서
서로 다른 리스트 두 개가 생긴다냥.
안쪽의 [0] * 3 은 왜 괜찮냥? 숫자 0 은 리스트가 아니라서 화살표 문제가
없다냥. 바깥쪽만 조심하면 된다냥.
3단계 · 입력으로 격자 받기 (6분)
채점 문제에서는 격자를 직접 적는 일이 거의 없다냥. 입력으로 받는다냥. 두 가지 모양만 익혀두면 웬만한 문제는 다 커버된다냥.
첫 줄에 크기가 오고, 그다음 줄부터 격자가 오는 형태라냥.
3 4
1 2 3 4
5 6 7 8
9 10 11 12
n, m = map(int, input().split())
grid = [list(map(int, input().split())) for _ in range(n)]숫자가 아니라 글자로 된 격자도 자주 나온다냥. 지도나 지뢰찾기 같은 거라냥.
3 3
*..
...
..*
n, m = map(int, input().split())
grid = [input().strip() for _ in range(n)]이때 grid 는 문자열의 리스트라냥. 그래도 grid[r][c] 로 읽는 건
똑같다냥. 문자열도 인덱스로 접근되기 때문이라냥.
다만 문자열은 바꿀 수 없다냥. 칸을 고쳐야 하면
list(input().strip()) 으로 받자냥.
4단계 · 행으로 훑고 열로 훑는다냥 (10분)
격자를 훑는 기본형은 중첩 반복문이라냥. 3장에서 구구단 만들 때 쓴 그 모양 그대로라냥.
for r in range(n):
for c in range(m):
print(grid[r][c], end=" ")
print()행의 합과 열의 합은 난이도가 다르다냥.
행의 합 — 안쪽 리스트 하나만 보면 된다냥.
for r in range(n):
print(sum(grid[r]))열의 합 — 모든 행을 가로질러야 한다냥. grid[c] 같은 건 없다냥.
for c in range(m):
total = 0
for r in range(n):
total += grid[r][c]
print(total)바깥이 c, 안쪽이 r 로 순서가 뒤집혔다는 게 핵심이라냥. 그래도
꺼낼 때는 여전히 grid[r][c] 라냥. 반복 순서가 바뀌어도 인덱스 순서는
안 바뀐다냥.
전치(행과 열 바꾸기)도 같은 원리라냥.
trans = [[grid[r][c] for r in range(n)] for c in range(m)]결과의 크기가 m행 n열 로 뒤바뀐다는 걸 놓치기 쉽다냥. 바깥 반복이
m번이라냥.
5단계 · 이웃 칸을 들여다본다냥 (12분)
격자 문제의 절반은 "내 옆칸이 어떻냥?" 을 묻는다냥. 지뢰찾기, 지도 탐색, 생명 게임 전부 그렇다냥. 여기서 배우는 방법은 나중에 배울 탐색 알고리즘에서도 그대로 쓴다냥.
칸 하나에서 갈 수 있는 방향을 표로 미리 적어두는 것이 요령이라냥.
# 상하좌우 네 방향이라냥
dr = [-1, 1, 0, 0]
dc = [0, 0, -1, 1]
for k in range(4):
nr = r + dr[k]
nc = c + dc[k]비유로 이해하기 — 나침반
dr, dc 는 "그 방향으로 한 발 갔을 때 주소가 얼마나 변하냥" 을 적어둔
나침반이라냥.
위로 가면 행 번호가 줄어든다냥. 화면은 위에서 아래로 그려지기 때문이라냥.
대각선까지 여덟 방향을 보려면 이렇게 쓰면 편하다냥.
for dr in (-1, 0, 1):
for dc in (-1, 0, 1):
if dr == 0 and dc == 0:
continue # 자기 자신은 빼고
nr = r + dr
nc = c + dc범위 검사를 반드시 해야 한다냥.
if 0 <= nr < n and 0 <= nc < m:
# 이 안에서만 grid[nr][nc] 를 읽는다냥다른 언어에서는 격자 밖을 읽으면 에러가 나서 바로 알아챈다냥. 그런데 파이썬은 다르다냥.
nr 이 -1 이면 grid[-1] 은 에러가 아니라 맨 마지막 행이라냥.
즉 왼쪽 위 칸을 보려다가 반대편 끝을 읽는다냥.
에러가 안 나니 프로그램은 멀쩡히 끝나고, 답만 조용히 틀린다냥. 이게 격자 문제에서 가장 찾기 힘든 버그라냥.
- 방향 표를 만든다 —
dr,dc - 더한다 —
nr = r + dr,nc = c + dc - 범위를 확인한다 —
0 <= nr < n and 0 <= nc < m
이 세 줄이 격자 문제의 뼈대라냥. 오늘 4번 문제에서 그대로 쓴다냥.
오늘 배운 내용 정리
먼저 스스로 답해보고, "정답 보기"를 눌러서 맞는지 확인해보자냥!
이제 직접 풀어볼 차례다냥
오늘 문제는 다섯 개라냥. 1번은 행과 열을 훑기, 23번은 새 격자 만들기,
4번은 이웃 세기, 5번은 순서 자체가 문제라냥. 막히면 작은 격자를 종이에
그리고 칸마다 주소를 적어보자냥. 그게 제일 빠르다냥. 가보자냥
