💻 백준 17070: 파이프 옮기기 1
- 문제 링크: https://www.acmicpc.net/problem/17070
- 알고리즘 분류: 다이나믹 프로그래밍, 그래프 탐색, 너비 우선 탐색
문제 소개 🧐
N×N 크기의 집에서 2칸을 차지하는 파이프를 (1, 2) 위치에서 (N, N) 위치까지 옮기는 경우의 수를 찾는 문제다. 파이프는 가로, 세로, 대각선 3가지 상태를 가질 수 있으며, 각 상태에 따라 이동할 수 있는 방향이 정해져 있다. 파이프는 항상 빈 칸만 차지해야 한다.
입력 📝
- 첫째 줄: 집의 크기 N (3 ≤ N ≤ 16)
- 둘째 줄부터 N개의 줄: 집의 상태 (0은 빈 칸, 1은 벽)
출력 📤
파이프의 한쪽 끝을 (N, N)으로 이동시키는 방법의 수를 출력한다.
제한 ❌
- 시간: 1초
- 메모리: 512 MB
나의 풀이 과정 🌊
첫 번째 시도: BFS
Idea:
처음에는 이 문제를 그래프 탐색 문제로 보고 BFS(너비 우선 탐색)로 접근했다. 큐에 (현재 파이프 끝 좌표, 파이프 방향)
을 저장하고, 각 상태에서 이동할 수 있는 모든 경우를 탐색하여 (N, N)에 도달할 때마다 카운트를 1씩 증가시키는 방식이다.
파이프의 방향(가로: 0, 대각선: 1, 세로: 2)에 따라 다음에 이동할 수 있는 방향과 좌표, 그리고 벽을 체크하는 조건을 꼼꼼하게 설정하여 구현했다.
Code:
import sys
from collections import deque
n = int(sys.stdin.readline().rstrip())
house = [list(map(int, sys.stdin.readline().split())) for _ in range(n)]
# 각 방향(가로, 대각선, 세로)에서 이동 가능한 다음 방향들
# (dr, dc)와 다음 방향(nd)을 튜플로 묶어 관리하면 더 깔끔했을 것 같다.
move = [[(0, 1), (1,1)], [(0, 1), (1,1), (1,0)], [(1,0), (1,1)]] # 가로, 대각선, 세로
start_pos = (0,1)
q = deque()
q.append((start_pos, 0)) # (현재 좌표, 파이프 방향)
answer = 0
while q:
cur_pos, direction = q.popleft()
r, c = cur_pos
if r == n-1 and c == n-1:
answer += 1
continue # 도착했으면 더 이상 진행 안 함
# 현재 방향에서 가능한 다음 움직임들을 탐색
for dr, dc in move[direction]:
n_r, n_c = r + dr, c + dc
if n_r >= n or n_c >= n or house[n_r][n_c] == 1:
continue
# 다음 파이프 방향 결정
n_dr = 0
if dr == 1 and dc == 1: # 대각선
n_dr = 1
# 대각선 이동 시에는 추가로 2칸의 벽을 더 확인해야 함
if house[n_r-1][n_c] == 1 or house[n_r][n_c-1] == 1:
continue
elif dr == 1 and dc == 0: # 세로
n_dr = 2
else: # 가로
n_dr = 0
q.append(((n_r, n_c), n_dr))
print(answer)
Result: 시간 초과 😭
BFS로 모든 경로를 탐색하다 보니, N이 커질수록 중복되는 계산이 많아져 시간 초과가 발생한 것 같다.
두 번째 시도: DP (다이나믹 프로그래밍)
Idea:
BFS의 시간 초과를 겪고 나서, 이 문제는 DP로 풀어야 한다는 것을 깨달았다. (N, N)까지 도달하는 경우의 수를 누적해서 계산하는 방식이 더 효율적이다.
3차원 DP 테이블 dp[x][y][dir]
를 ‘파이프의 끝이 (x, y)에 있고, 방향이 dir
일 때의 경우의 수’로 정의했다.
- 가로 (dir=0):
dp[i][j][0] = dp[i][j-1][0] + dp[i][j-1][2]
- 세로 (dir=1):
dp[i][j][1] = dp[i-1][j][1] + dp[i-1][j][2]
- 대각선 (dir=2):
dp[i][j][2] = dp[i-1][j-1][0] + dp[i-1][j-1][1] + dp[i-1][j-1][2]
위 점화식을 기반으로 DP 테이블을 채워나가면 최종적으로 dp[n-1][n-1]
에 있는 모든 방향의 경우의 수를 합산하여 답을 구할 수 있다.
Code:
import sys
from collections import deque
n = int(sys.stdin.readline().rstrip())
house = [list(map(int, sys.stdin.readline().split())) for _ in range(n)]
dp = [[[0 for _ in range(3)] for _ in range(n)] for _ in range(n)] # 가로 세로 대각 의 형태로 온 경우의 수
dp[0][1][0] = 1 # 가로로 놓여있으니깐
for i in range(2,n): # 첫행 채워주기
if house[0][i] == 1:
break
dp[0][i][0] = dp[0][i-1][0]
for i in range(1, n): # dp[i][j] = max(dp[i-1][j][1] + dp[i-1][j][2], dp[i][j-1][0] + dp[i][j-1][2], dp[i-1][j-1][2])
for j in range(1, n):
if house[i][j] == 0:
dp[i][j][0] = dp[i][j-1][0] + dp[i][j-1][2]
dp[i][j][1] = dp[i-1][j][1] + dp[i-1][j][2]
if house[i-1][j] == 0 and house[i][j-1] == 0:
dp[i][j][2] = dp[i-1][j-1][0] + dp[i-1][j-1][1] + dp[i-1][j-1][2]
print(sum(dp[n-1][n-1]))
Result: 성공! ✅
DP를 사용하니 중복 계산을 피하고 효율적으로 경우의 수를 누적하여 시간 내에 문제를 해결할 수 있었다.
배운 점 및 느낀 점 ✍️
- BFS vs DP: 경로의 ‘수’를 구하는 문제는 BFS/DFS로 모든 경로를 탐색하면 시간 초과가 발생하기 쉽다. 이럴 땐 DP로 점화식을 세워 접근하는 것이 훨씬 효율적이라는 것을 다시 한번 느꼈다.
- 상태 정의의 중요성: DP 문제를 풀 때
dp
테이블의 상태를 어떻게 정의하느냐가 가장 중요하다. 이 문제에서는(x, y, 방향)
3가지를 상태로 정의해야 했다. - 꼼꼼한 조건 체크: 특히 대각선으로 이동할 때는 추가로 확인해야 할 벽이 있다는 점을 놓치기 쉬웠다. DP 점화식을 세울 때 항상 모든 제약 조건을 꼼꼼히 반영해야겠다.