알고리즘 입문
개념 배우기직접 풀어보기
놀면 뭐 하니,
그냥 하는거야.

칸을 세로로도 세자! 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아파트 단지 전체
grid[r]r동 (한 줄 전체)
grid[r][c]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 는 "그 방향으로 한 발 갔을 때 주소가 얼마나 변하냥" 을 적어둔 나침반이라냥.

방향drdc
위-10
아래+10
왼쪽0-1
오른쪽0+1

위로 가면 행 번호가 줄어든다냥. 화면은 위에서 아래로 그려지기 때문이라냥.

대각선까지 여덟 방향을 보려면 이렇게 쓰면 편하다냥.

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] 은 에러가 아니라 맨 마지막 행이라냥. 즉 왼쪽 위 칸을 보려다가 반대편 끝을 읽는다냥.

에러가 안 나니 프로그램은 멀쩡히 끝나고, 답만 조용히 틀린다냥. 이게 격자 문제에서 가장 찾기 힘든 버그라냥.

정리이웃 보기 3종 세트라냥
  1. 방향 표를 만든다 — dr, dc
  2. 더한다 — nr = r + dr, nc = c + dc
  3. 범위를 확인한다 — 0 <= nr < n and 0 <= nc < m

이 세 줄이 격자 문제의 뼈대라냥. 오늘 4번 문제에서 그대로 쓴다냥.

오늘 배운 내용 정리

코딩냥

먼저 스스로 답해보고, "정답 보기"를 눌러서 맞는지 확인해보자냥!

Q1.grid[2][3] 은 무엇을 가리키냥?
Q2.[[0] * 3] * 2 로 격자를 만들면 왜 안 되냥?
Q3.열의 합을 구할 때 반복문을 어떻게 쓰냥?
Q4.dr, dc 는 무엇을 담아두는 거냥?
Q5.이웃 칸을 볼 때 범위 검사를 빼먹으면 파이썬에서 무슨 일이 생기냥?

이제 직접 풀어볼 차례다냥

코딩냥

오늘 문제는 다섯 개라냥. 1번은 행과 열을 훑기, 23번은 새 격자 만들기, 4번은 이웃 세기, 5번은 순서 자체가 문제라냥. 막히면 작은 격자를 종이에 그리고 칸마다 주소를 적어보자냥. 그게 제일 빠르다냥. 가보자냥