문제 보기 · Level 4 · 2017 카카오코드 본선 · 정답률 8% · 푼 사람 318명 (2026-09-30 기준)1
문제
m×n 파티장의 왼쪽 위 (0, 0)에서 오른쪽 아래 (m − 1, n − 1)까지 상하좌우로 한 칸씩 간다. 칸마다 그 칸 친구와 나눌 대화 시간이 적혀 있고, −1인 칸은 테이블이라 못 지나간다. 시작 칸과 도착 칸의 대화 시간은 0이다.
대화 시간 합이 s를 넘는 길은 쓸 수 없다. 쓸 수 있는 길 가운데 가장 짧은 길을 고르고, 길이가 같으면 대화 시간 합이 적은 길을 고른다. [길의 걸음 수, 대화 시간 합]을 돌려준다.
- 2 ≤ m, n ≤ 50
- 1 ≤ s ≤ \(2^{31}-1\), 대화 시간은 −1 ~ \(2^{31}-1\)
- 조건에 맞는 길이 늘 하나 이상 있다
풀이
막히면 한 단계씩 펼친다. 칸은 (행, 열)로 적고 0부터 센다.
필요한 개념
동적 계획법: “정확히 k걸음에 이 칸에 왔을 때의 최소 대화 시간”을 걸음 수 순서로 채운다. 한 층씩 넓혀 가는 모양은 너비 우선 탐색과 같다. 다만 칸마다 한 번만 들르지 않는다.
1단계 · 작은 예로 손으로 풀기
문제의 세 번째 예시 판이다. s = 12다.
0 1 1 1 1
9 9 9 1 9
1 1 1 1 9
1 1 5 9 9
1 1 1 1 0
- 가장 짧은 길은 8걸음이다. 하지만 8걸음 길은 어떻게 가도 9나 5를 밟아, 대화 시간이 가장 적은 길도 15다. 12를 넘으니 못 쓴다.
- 10걸음 길은 가장 적어도 13이라 역시 못 쓴다.
- 12걸음에 11인 길이 있다. 오른쪽 3칸 → 아래 2칸 → 왼쪽 2칸 → 아래 2칸 → 오른쪽 3칸으로 1만 밟는다. 답은 [12, 11]이다.
같은 판에서 s만 바꾸면 답이 이렇게 바뀐다.
| s | 11 ~ 12 | 13 ~ 14 | 15 이상 |
|---|---|---|---|
| 답 | [12, 11] | [10, 13] | [8, 15] |
s가 빠듯할수록 멀리 돌아간다. 걸음 수와 대화 시간을 함께 따져야 한다.
2단계 · 가장 단순한 방법과 그 속도
같은 칸을 두 번 밟지 않는 길을 모두 만들고(백트래킹), 대화 시간이 s 이하인 길 가운데 (걸음, 대화 시간)이 가장 작은 것을 고른다. 답은 맞지만 길의 수가 너무 많다. 벽 없는 빈 판의 길 수를 세어 보면 4×4에 184개, 5×5에 8,512개, 6×6에 1,262,816개다. 한 변이 1 늘 때마다 수십~백 배 넘게 는다. 50×50은 어림없다.
3단계 · 핵심 아이디어
- 걸음 수로 층을 나눈다: k걸음에 닿는 칸들을 한 층으로 본다. 칸마다 “정확히 k걸음에 왔을 때의 최소 대화 시간”을 적는다. 다음 층의 값은 이웃 칸의 값에 그 칸의 대화 시간을 더한 것 중 가장 작은 값이다.
- 도착 칸이 처음 나온 층이 답이다: 층을 0, 1, 2, …로 늘리다 도착 칸이 처음 나오면, 그 k가 가장 짧은 길이다. 그 층에 적힌 값이 그 길이의 최소 대화 시간이다. 대화 시간은 0 이상이라 한 번 s를 넘은 길은 다시 내려오지 않는다. 그래서 s를 넘는 값은 바로 버린다.
- 같은 칸에 다시 와도 된다: 이 표는 왔던 칸을 다시 밟는 길도 센다. 그래도 답은 안 바뀐다. 길에 고리가 있으면 고리를 빼도 걸음은 줄고 대화 시간은 늘지 않는다. 그러니 가장 짧은 답 길에는 고리가 없다. 칸을 다시 밟아도 되는 길을 보행(walk), 다시 밟지 않는 길을 경로(path)라 한다. 이 논리는 경로와 연결성의 “가장 짧은 보행은 경로다”를 대화 시간까지 따지도록 넓힌 것이다.
- 버려도 되는 상태: 어떤 칸에 더 적은 걸음으로, 같거나 적은 대화 시간으로 온 적이 있으면 지금 상태는 버린다. 지금 상태로 갈 수 있는 곳은 그 전 상태가 더 빨리, 더 싸게 갈 수 있다. 그래서 best[칸]에 앞 층들의 최소 대화 시간을 적어 두고, 이보다 작을 때만 층에 넣는다. (걸음, 대화 시간) 쌍을 “둘 다 같거나 작다”로 비교하면 부분순서가 된다. 6단계에서 칸 (2, 2)에 온 (4걸음, 12)와 (6걸음, 6)은 어느 쪽도 다른 쪽보다 작지 않아 둘 다 남는다. 칸마다 남는 상태는 이 순서의 극소 원소(자기보다 작은 것이 없는 원소)다.
4단계 · 코드 흐름
- best를 모두 무한대로 두고 best[0][0] = 0. 첫 층은 {(0, 0): 0}, k = 0.
- 층이 비지 않는 동안 되풀이한다. 도착 칸이 층에 있으면 [k, 그 값]을 돌려준다.
- 층의 칸마다 네 방향 이웃을 본다. 판 밖이나 −1이면 건너뛴다. t = 지금 값 + 이웃의 대화 시간.
- t ≤ s이고, t < best[이웃]이고, 다음 층에 적힌 값보다 작을 때만 다음 층에 넣는다.
- 층을 다 보면 다음 층의 값으로 best를 고치고, 다음 층으로 넘어가 k를 1 늘린다.
5단계 · 파이썬 코드
DIRS = ((1, 0), (-1, 0), (0, 1), (0, -1))
def solution(m, n, s, time_map):
INF = float("inf")
best = [[INF] * n for _ in range(m)] # 앞 층들에서 그 칸에 온 최소 대화 시간
best[0][0] = 0
layer = {(0, 0): 0} # 정확히 k걸음에 온 칸 → 최소 대화 시간
k = 0
while layer:
if (m - 1, n - 1) in layer:
return [k, layer[(m - 1, n - 1)]]
nxt = {}
for (r, c), talk in layer.items():
for dr, dc in DIRS:
nr, nc = r + dr, c + dc
if 0 <= nr < m and 0 <= nc < n and time_map[nr][nc] != -1:
t = talk + time_map[nr][nc]
if t <= s and t < best[nr][nc] and t < nxt.get((nr, nc), INF):
nxt[(nr, nc)] = t
for (r, c), t in nxt.items():
best[r][c] = min(best[r][c], t)
layer = nxt
k += 1
return [-1, -1]
layer를 딕셔너리로 둔다: 같은 층에서 한 칸에 여러 길로 오면 가장 작은 값 하나만 남는다.t < best[nr][nc]: 앞 층에서 같거나 더 싸게 온 적이 있으면 버린다. 시작 칸은 best가 0이라 다시 들어가지 않는다.nxt.get((nr, nc), INF): 아직 다음 층에 없는 칸이면 무한대를 꺼내 비교한다.
6단계 · 실행 추적
1단계의 판(s = 12)에서 층마다 남는 칸과 대화 시간이다.
| k | 층에 남은 칸 (대화 시간) |
|---|---|
| 0 | (0, 0) 0 |
| 1 | (0, 1) 1, (1, 0) 9 |
| 2 | (0, 2) 2, (1, 1) 10, (2, 0) 10 |
| 3 | (0, 3) 3, (1, 2) 11, (2, 1) 11, (3, 0) 11 |
| 4 | (0, 4) 4, (1, 3) 4, (2, 2) 12, (3, 1) 12, (4, 0) 12 |
| 5 | (2, 3) 5 |
| 6 | (2, 2) 6 |
| 7 | (2, 1) 7, (3, 2) 11 |
| 8 | (2, 0) 8, (3, 1) 8, (4, 2) 12 |
| 9 | (3, 0) 9, (4, 1) 9 |
| 10 | (4, 0) 10, (4, 2) 10 |
| 11 | (4, 3) 11 |
| 12 | (4, 4) 11 |
- k = 5: 12인 칸들은 어디로 가도 12를 넘는다. (0, 4)와 (1, 3)에서 (1, 4)로 가면 13이라 버린다. (1, 3) → (2, 3)의 5만 남는다.
- k = 6: (2, 2)가 다시 나온다. 4걸음에 12로 왔던 칸인데, 6걸음에 6으로 오니 더 싸서 남긴다. 이 6짜리가 답 길을 이어 간다.
- k = 10: (4, 2)도 8걸음 12에서 10걸음 10으로 다시 들어간다.
- k = 12: 도착 칸이 처음 나온다. 답은 [12, 11]이다.
7단계 · 복잡도와 흔한 실수
층마다 칸은 많아야 mn개다. 층은 많아야 mn개다. mn걸음 이상인 길에는 고리가 생기고, 고리를 뺀 더 짧은 길이 같거나 적은 대화 시간으로 이미 와 있어 버려지기 때문이다. mn걸음을 걸으면 시작 칸을 포함해 mn + 1번 칸을 밟는데 칸은 많아야 mn개라, 비둘기집 원리로 어떤 칸은 두 번 밟는다. 그래서 최악은 \(O((mn)^2)\)이다. 실제로는 칸이 대화 시간이 줄 때만 층에 다시 들어가 훨씬 적다. 50×50 판 23개(값이 \(2^{31}-1\)까지인 판, 벽이 30%인 판, 1,274걸음을 돌아가는 판 포함)를 모두 풀어도 0.01초쯤 걸렸다.
- 칸마다 한 번만 들르는 너비 우선 탐색을 쓴다. 1단계 판에서 (2, 2)에 4걸음 12로 먼저 와서 칸을 막아 버린다. 6걸음 6으로 오는 길이 막혀 도착 칸에 못 간다.
- 대화 시간만 줄이는 다익스트라를 쓴다. 1단계 판에서 s = 100이면 답은 [8, 15]다. 그런데 대화 시간이 가장 적은 길은 12걸음에 11이다. 걸음 수가 먼저다.
- [대화 시간, 걸음 수] 순서로 돌려준다. 순서는 [걸음 수, 대화 시간]이다.
- C++이나 자바로 옮기면 대화 시간 합이 int 범위를 넘을 수 있다. 파이썬 정수는 크기 제한이 없다.
-
프로그래머스 스쿨, “튜브의 소개팅” (https://school.programmers.co.kr/learn/courses/30/lessons/1839), 2026-10-01 조회. ↩