[Python] 백준/BOJ 3197번: 백조의 호수 (Platinum 5)

2025. 7. 3. 18:59·Algorithm/백준 (BOJ)
반응형

💻 Problem

문제 보러 가기

 

두 마리의 백조가 호수에서 살고 있었다. 그렇지만 두 마리는 호수를 덮고 있는 빙판으로 만나지 못한다.

호수는 행이 R개, 열이 C개인 직사각형 모양이다. 어떤 칸은 얼음으로 덮여있다.

호수는 차례로 녹는데, 매일 물 공간과 접촉한 모든 빙판 공간은 녹는다. 두 개의 공간이 접촉하려면 가로나 세로로 닿아 있는 것만 (대각선은 고려하지 않는다) 생각한다.

아래에는 세 가지 예가 있다.

...XXXXXX..XX.XXX ....XXXX.......XX .....XX.......... 
....XXXXXXXXX.XXX .....XXXX..X..... ......X.......... 
...XXXXXXXXXXXX.. ....XXX..XXXX.... .....X.....X..... 
..XXXXX..XXXXXX.. ...XXX....XXXX... ....X......XX.... 
.XXXXXX..XXXXXX.. ..XXXX....XXXX... ...XX......XX.... 
XXXXXXX...XXXX... ..XXXX.....XX.... ....X............ 
..XXXXX...XXX.... ....XX.....X..... ................. 
....XXXXX.XXX.... .....XX....X..... ................. 
      처음               첫째 날             둘째 날

백조는 오직 물 공간에서 세로나 가로로만(대각선은 제외한다) 움직일 수 있다.

며칠이 지나야 백조들이 만날 수 있는지 계산하는 프로그램을 작성하시오.

  • 1 ≤ R, C ≤ 1500.

 

💡 Approach

문제를 읽었을 때 예전에 풀었던 치즈 문제가 생각났다.

그래서 처음엔 치즈 문제처럼 풀었다.

bfs로 백조 둘이 만날 수 있는지 체크하고, bfs로 모든 물 칸에서 인접한 빙판 녹이고...

import sys
from collections import deque
input = sys.stdin.readline

# 백조들이 만날 수 있는지 확인하기
def can_meet():
    queue = deque([swan[0]])
    visited = [[False] * C for _ in range(R)]
    
    while queue:
        y, x = queue.popleft()
        
        for i in range(4):
            ny = y + dy[i]
            nx = x + dx[i]
            
            # 백조 둘이 만났으면 종료
            if (ny, nx) == swan[1]:
                return True
            
            # 호수 범위 안에 들고 방문한 적이 없는 칸
            if 0 <= ny < R and 0 <= nx < C and not visited[ny][nx]:
                # 물 칸이면 이어서 탐색하기
                if lake[ny][nx] == '.':
                    queue.append((ny, nx))
                    visited[ny][nx] = True
    
    return False

# 빙판 녹이기 (하루)
def melt(y, x):
    queue = deque([(y, x)])
    
    while queue:
        y, x = queue.popleft()
        
        for i in range(4):
            ny = y + dy[i]
            nx = x + dx[i]
            
            # 호수 범위 안에 들고 방문한 적이 없는 칸
            if 0 <= ny < R and 0 <= nx < C and not visited[ny][nx]:
                # 빙하 칸이면 녹이기
                if lake[ny][nx] == 'X':
                    lake[ny][nx] = '.'
                    visited[ny][nx] = True
                # 물 칸이면 이어서 탐색하기
                elif lake[ny][nx] == '.':
                    queue.append((ny, nx))
                    visited[ny][nx] = True

R, C = map(int, input().split())
lake = [list(input().rstrip()) for _ in range(R)] # 호수 정보
swan = [] # 백조 위치
days = 0 # 백조들이 만나는 데까지 걸리는 날

dy = [-1, 1, 0, 0] # 상하좌우
dx = [0, 0, -1, 1]

# 백조의 위치 찾기
for i in range(R):
    for j in range(C):
        if lake[i][j] == 'L':
            swan.append((i, j))

while True:
    # 백조들이 만날 수 있는지 확인하고 만났다면 종료
    if can_meet():
        print(days)
        break
    
    # 빙판 녹이기
    visited = [[False] * C for _ in range(R)]
    
    for i in range(R):
        for j in range(C):
            # 아직 방문하지 않은 물 칸이면 녹이기
            if not visited[i][j] and lake[i][j] == '.':
                melt(i, j)        
    
    days += 1

그랬더니 시간 초과..!

 

위 코드의 문제점은 하루에 두 번 bfs를 돌리는데 매번 방문 배열을 새로 생성하고 전체 격자를 반복한 것이다.

  • can_meet(): bfs를 매번 처음부터 새로 수행하고 있음
  • melt(): 매번 물 칸 전체를 순회해서 인접한 빙판 칸을 찾고 있음

R, C의 최댓값은 각각 1500이다.

1500 * 1500 배열에서 만약 백조가 (0, 0), (1499, 1499)에 있다고 하면 하루에 대각선으로 한 줄씩 녹으니 약 1500일 정도가 소요될 것이다.

  • can_meet()이 1500 * 1500 = 2 * 10^6
  • melt()가 1500 * 1500 = 2 * 10^6

이게 1500일 반복한다고 생각하면 1500 * 4 * 10 ^ 6 = 6 * 10 ^ 9

얼추 계산한 거라 계수는 애매하지만 10^9만 생각해도 시간 초과다.

 


 

🔨 틀린 코드 최적화

그럼 시간 초과를 해결하기 위해 백조들이 만날 수 있는지 확인하는 함수(can_meet())와 빙하를 녹이는 함수(melt())를 최적화해 보자.

 

1️⃣ 백조들이 만날 수 있는지 확인하기

현재는 매일 bfs를 처음부터 다시 하고 있었다.

시작 백조 위치부터 bfs를 돌려서 물 칸이면 이동해서 목표 백조 위치까지 이동할 수 있는지 확인했다.

하지만 이렇게 되면 아래의 상황일 때 (1, 1)까지는 이동할 수 있는 걸 알고 있음에도 (0, 1)부터 다시 탐색해야 한다.

.L
..
XX
..
.L

배열의 크기가 클수록 손해 보는 계산이 많을 것이다.

만약 어제 (1, 1)까지 이동할 수 있는 걸 기록해 뒀더라면 (0, 1)에서 시작하지 않고 바로 (1, 1)에서 시작해도 될 것이다.

 

그래서 큐를 두 개 만들어 오늘 탐색할 큐(cur_swan_q)와 내일 탐색할 큐(next_swan_q)를 나누었다.

오늘 탐색할 큐에는 인접한 물 칸을 넣고, 내일 탐색할 큐에는 인접한 빙하 칸을 넣는다.

탐색하면서 만나는 빙하 칸은 물 칸과 인접한 빙하 칸이기 때문에 내일이면 녹아있을 것이다.

 

즉, 오늘은 못 갔지만 빙하가 한 칸만 녹으면 내일 갈 수 있는 곳을 저장하는 게 포인트이다.

마찬가지로 방문 배열도 통합해서 관리한다.

 

여기까지 최적화했을 때는 메모리 초과가 떴다.

 

2️⃣ 빙하 녹이기

빙하 녹이는 과정도 비슷하다.

현재는 매일 호수의 모든 물 칸을 돌고 있고 방문 배열도 하루마다 초기화하고 있다.

백조들이 만날 수 있는 함수를 최적화한 것처럼 큐를 두 개로 나눠서 물 칸 주변의 빙하 칸만 골라서 녹이도록 한다.

 

처음에 백조 위치를 찾을 때 물 위치도 같이 찾아서 전부 큐에 넣는다.

큐에 들어있는 물 칸을 전부 탐색하고 빙하 칸을 만나면 다음에 탐색할 물 위치 큐에 삽입한다.

 

이렇게 최적화해서 통과한 코드가 아래 코드이다.

import sys
from collections import deque
input = sys.stdin.readline

# 백조들이 만날 수 있는지 확인하기
def can_meet():
    global next_swan_q
    
    cur_swan_q = next_swan_q
    next_swan_q = deque()

    # 오늘 이동 가능한 칸만 탐색하기
    while cur_swan_q:
        y, x = cur_swan_q.popleft()
        
        for i in range(4):
            ny = y + dy[i]
            nx = x + dx[i]
            
            # 백조 둘이 만났으면 종료하기
            if (ny, nx) == swan[1]:
                return True
            
            # 호수 범위 안에 들고 방문한 적이 없는 칸
            if 0 <= ny < R and 0 <= nx < C and not swan_visited[ny][nx]:
                # 물 칸이면 이어서 탐색하기
                if lake[ny][nx] == '.':
                    cur_swan_q.append((ny, nx))
                # 빙하 칸이면 다음에 이동할 큐에 넣기
                elif lake[ny][nx] == 'X':
                    next_swan_q.append((ny, nx))
                swan_visited[ny][nx] = True

    return False

# 빙판 녹이기 (하루)
def melt():
    global next_water_q
    
    cur_water_q = next_water_q
    next_water_q = deque()
    
    while cur_water_q:
        y, x = cur_water_q.popleft()
        
        for i in range(4):
            ny = y + dy[i]
            nx = x + dx[i]
            
            # 호수 범위 안에 들고 방문한 적이 없는 칸
            if 0 <= ny < R and 0 <= nx < C and not water_visited[ny][nx]:
                # 빙하 칸이면 녹이기
                if lake[ny][nx] == 'X':
                    lake[ny][nx] = '.'
                    next_water_q.append((ny, nx))
                    water_visited[ny][nx] = True

dy = [-1, 1, 0, 0] # 상하좌우
dx = [0, 0, -1, 1]

R, C = map(int, input().split())
lake = [list(input().rstrip()) for _ in range(R)] # 호수 정보
swan = [] # 백조 위치
days = 0 # 백조들이 만나는 데까지 걸리는 날

cur_water_q = deque() # 물 칸
next_water_q = deque() # 내일 탐색할 물 칸
water_visited = [[False] * C for _ in range(R)]

# 백조의 위치와 물 위치 찾기
for i in range(R):
    for j in range(C):
        if lake[i][j] in ('.', 'L'):
            if lake[i][j] == 'L':
                swan.append((i, j))
            next_water_q.append((i, j))
            water_visited[i][j] = True

cur_swan_q = deque() # 백조가 오늘 갈 수 있는 칸
next_swan_q = deque([swan[0]]) # 빙하가 오늘 녹으면 백조가 내일 갈 수 있는 칸
swan_visited = [[False] * C for _ in range(R)]
swan_visited[swan[0][0]][swan[0][1]] = True

while True:
    # 백조들이 만날 수 있는지 확인하고 만났다면 종료하기
    if can_meet():
        print(days)
        break
    
    # 빙판 녹이기
    melt()
    days += 1

 

✏️ Solution

위 코드에서 global을 지우고 함수에서 큐를 return 하도록 정리한 코드이다.

import sys
from collections import deque
input = sys.stdin.readline

# 백조들이 만날 수 있는지 확인하기
def move_swan():
    next_swan_q = deque()

    # 오늘 이동 가능한 칸만 탐색하기
    while cur_swan_q:
        y, x = cur_swan_q.popleft()
        
        for i in range(4):
            ny = y + dy[i]
            nx = x + dx[i]
            
            # 백조 둘이 만났으면 종료하기
            if (ny, nx) == swan[1]:
                return True, None
            
            # 호수 범위 안에 들고 방문한 적이 없는 칸
            if 0 <= ny < R and 0 <= nx < C and not swan_visited[ny][nx]:
                # 물 칸이면 이어서 탐색하기
                if lake[ny][nx] == '.':
                    cur_swan_q.append((ny, nx))
                # 빙하 칸이면 다음에 이동할 큐에 넣기
                elif lake[ny][nx] == 'X':
                    next_swan_q.append((ny, nx))
                swan_visited[ny][nx] = True

    return False, next_swan_q

# 빙판 녹이기 (하루)
def melt_ice():
    next_water_q = deque()
    
    while cur_water_q:
        y, x = cur_water_q.popleft()
        
        for i in range(4):
            ny = y + dy[i]
            nx = x + dx[i]
            
            # 호수 범위 안에 들고 방문한 적이 없는 칸
            if 0 <= ny < R and 0 <= nx < C and not water_visited[ny][nx]:
                # 빙하 칸이면 녹이기
                if lake[ny][nx] == 'X':
                    lake[ny][nx] = '.'
                    next_water_q.append((ny, nx))
                    water_visited[ny][nx] = True
    
    return next_water_q

R, C = map(int, input().split())
lake = [list(input().rstrip()) for _ in range(R)] # 호수 정보

dy = [-1, 1, 0, 0] # 상하좌우
dx = [0, 0, -1, 1]

swan = [] # 백조 위치
cur_water_q = deque() # 물 칸
water_visited = [[False] * C for _ in range(R)]

# 초기 위치 찾기
for i in range(R):
    for j in range(C):
        if lake[i][j] in ('.', 'L'):
            if lake[i][j] == 'L':
                swan.append((i, j))
            cur_water_q.append((i, j))
            water_visited[i][j] = True

cur_swan_q = deque([swan[0]]) # 백조가 오늘 갈 수 있는 칸
swan_visited = [[False] * C for _ in range(R)]
swan_visited[swan[0][0]][swan[0][1]] = True

days = 0 # 백조들이 만나는 데까지 걸리는 날

while True:
    # 백조들이 만날 수 있는지 확인하고 만났다면 종료하기
    can_meet, next_swan_q = move_swan()
    if can_meet:
        print(days)
        break
    
    # 빙판 녹이기
    cur_water_q = melt_ice()
    
    # 하루 마무리하기
    cur_swan_q = next_swan_q
    days += 1

처음 코드와 비교했을 때 바꾼 코드는 매일 bfs가 중복으로 반복되는 것이 아니기 때문에 days를 곱하지 않아도 된다.

바꾼 코드의 시간 복잡도는 O(R * C)로 볼 수 있다.

  • 5 * R * C + 5 * R * C = 10 * R * C = 10 * 1500 * 1500 = 약 2 * 10^7

반응형

'Algorithm > 백준 (BOJ)' 카테고리의 다른 글

[Python] 백준/BOJ 14929번: 귀찮아 (SIB) (Silver 4)  (0) 2025.07.11
[Python] 백준/BOJ 17086번: 아기 상어 2 (Silver 2)  (0) 2025.07.10
[Python] 백준/BOJ 1655번: 가운데를 말해요 (Gold 2)  (0) 2025.06.28
[Python] 백준/BOJ 24499번: blobyum (Silver 4)  (0) 2025.06.20
'Algorithm/백준 (BOJ)' 카테고리의 다른 글
  • [Python] 백준/BOJ 14929번: 귀찮아 (SIB) (Silver 4)
  • [Python] 백준/BOJ 17086번: 아기 상어 2 (Silver 2)
  • [Python] 백준/BOJ 1655번: 가운데를 말해요 (Gold 2)
  • [Python] 백준/BOJ 24499번: blobyum (Silver 4)
올콩
올콩
콩 심은 데 콩 난다
  • 올콩
    콩스토리
    올콩
  • 전체
    오늘
    어제
    • 분류 전체보기 (161) N
      • SSAFY (10)
      • Algorithm (85) N
        • 이론 (6)
        • 백준 (BOJ) (78) N
        • 프로그래머스 (1)
      • Trouble Shooting (8)
      • Frontend (6)
      • React (14) N
      • Next.js (3)
      • Vue (4)
      • Node.js (2)
      • HTML (9)
      • DevOps (3)
        • Git (3)
      • Language (9)
        • JavaScript (0)
        • Java (9)
      • Embedded (1)
      • CS (5)
        • Network (1)
        • Blockchain (4)
      • 자격증 (2)
  • 블로그 메뉴

    • GitHub
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    우선순위큐
    Next.js
    재귀
    강의
    블록체인
    html5
    SSAFY
    SSAFYcial
    Algorithm
    싸피
    백준
    파이썬
    vue
    dfs
    DP
    브루트포스
    Heap
    오블완
    리액트
    해시
    알고리즘
    구현
    딕셔너리
    React
    Error
    누적합
    티스토리챌린지
    힙
    Java
    bfs
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
올콩
[Python] 백준/BOJ 3197번: 백조의 호수 (Platinum 5)
상단으로

티스토리툴바