알고리즘

[백준] 16236번 아기 상어 - Python 풀이(Feat. 1년 전 코드 다시보기)

샥쿠 2023. 7. 4. 14:16

✏️

1년 전 처음으로 백준 사이트에 가입할 때 이 문제를 보고 제목이 귀여워서 아이디를 baby_shark로 지었었다.

닉값을 하기 위해 아기 상어를 풀고 싶었지만 귀여운 제목과는 달리 골드 난이도의 문제였고, 당시에 나는 프로그래밍 공부를 시작하는 단계였기 때문에 BFS가 뭔지 몰라서 풀 수 없었다😂

이 문제를 풀고싶어서 알고리즘을 열심히 공부한 것도 일정 부분 있지 않을까? 아무튼 골드 문제 중에서 처음으로 도전했던 문제이고, 기념비 삼아 첫 게시물로 올려본다.

...그런데 1년이 지나고 코드를 다시 보니 개선될 여지가 많아서 성능 및 가독성을 향상시킨 v2 코드를 새로 작성하였다.

  • 1년 전 풀이
    N = int(input())
    fish = [list(map(int, input().split())) for _ in range(N)]
    
    
    # 다음에 먹을 물고기의 좌표(w)와 거리(nearest)를 반환하는 함수
    # 조건: 1)최소거리, 2)위, 왼쪽 순서로 우선
    def nearest_fish(arr, n, start, shark_size):
        queue = [start]
        distance = [[None] * n for _ in range(n)]   # 거리 함수
        distance[start[0]][start[1]] = 0
        nearest = 0     # 가장 가까운 거리
        edible = []     # 먹을 수 있는 물고기 좌표 저장
    
        # BFS 탐색
        while queue:
            v = queue.pop(0)
            dij = [[-1, 0], [0, -1], [0, 1], [1, 0]] # 상좌우하 델타이동
            for di, dj in dij:
                w = [v[0] + di, v[1] + dj]  # 주변 좌표
                if (0 <= w[0] < N and 0 <= w[1] < N and distance[w[0]][w[1]] == None):
                    # 물고기 없으면: 지나감
                    if arr[w[0]][w[1]] == 0:
                        queue.append(w)
                        distance[w[0]][w[1]] = distance[v[0]][v[1]] + 1
                    # 큰 물고기: 못지나감(큐에 추가x), 못먹음
                    elif arr[w[0]][w[1]] > shark_size:
                        continue
                    # 같은 물고기: 지나감, 못먹음
                    elif arr[w[0]][w[1]] == shark_size:
                        queue.append(w)
                        distance[w[0]][w[1]] = distance[v[0]][v[1]] + 1
                    # 작은 물고기: 먹을 수 있음
                    elif 0 < arr[w[0]][w[1]] < shark_size:
                        queue.append(w)
                        distance[w[0]][w[1]] = distance[v[0]][v[1]] + 1
                        
                        edible.append(w)
                        # nearest에 처음으로 저장되는 값이 최소거리(BFS 탐색)
                        if nearest == 0:
                            nearest = distance[w[0]][w[1]]
        
        # <중요> edible에 포함된 좌표를 정렬해줘야, 순서 조건을 만족할 수 있음
        edible.sort()
        for fish in edible:
            if distance[fish[0]][fish[1]] == nearest:
                return fish, nearest
        return False, False
    
    
    # 먹을 수 있는 물고기 다 찾기까지 걸리는 시간을 반환하는 함수
    def sorted_BFS_shark(arr, n, shark):
        result = 0  # 총 시간
        size = 2    # 아기상어 크기
        ate = 0     # 먹은 개수
        arr[shark[0]][shark[1]] = 0
        
        queue = [shark]
        while queue:
            v = queue.pop(0)    # 현재 상어 좌표
            w, time = nearest_fish(arr, n, v, size)   # 다음 먹을 물고기 위치, 걸리는 시간
            
            # 먹을 물고기가 물고기가 없다면, 결과 반환
            if not w:
                return result
    
            # 아기상어 크기 체크
            ate += 1
            if ate == size:
                size += 1
                ate = 0
            
            # 먹은 후: 시간 체크, 먹은 위치부터 다시 while문 시작
            result += time  # 총 시간 체크
            arr[w[0]][w[1]] = 0         # 먹은 물고기 제거
            queue = [w]                 # 큐 초기화
        return result
    
    
    # 아기상어 최초 위치 찾기
    is_shark = False
    for i in range(N):
        for j in range(N):
            if fish[i][j] == 9:
                shark = [i, j]
                is_shark = True
                break
        if is_shark:
            break
    
    print(sorted_BFS_shark(fish, N, shark))

개선사항

성능 관련

  • BFS 탐색 시 queue.pop(0) 대신 collections.deque 라이브러리를 임포트하여 queue.popleft() 함수로 변경
  • 델타이동을 위해 순회하는 값인 drc = [(-1, 0), (0, -1), (0, 1), (1, 0)]를 while문 밖으로 이동

가독성 관련

  • 다음에 먹을 물고기 정보를 반환하는 함수가 BFS 알고리즘을 사용하는 점이 명확해지도록 함수 이름 수정 (nearest_fish() -> nearest_fish_bfs())
  • 상어가 먹는 시간을 반환하는 함수임을 알 수 있도록 함수 이름 수정 (sorted_BFS_shark() -> get_eating_time())
  • 델타이동 시 주변 좌표 값을 w[0], w[1]로 호출하지 않고 nr, nc = r + dr, c + dc 로 간결하게 표현

제출코드 (V2)

from collections import deque

N = int(input())
fish = [list(map(int, input().split())) for _ in range(N)]
drc = [(-1, 0), (0, -1), (0, 1), (1, 0)]  # 상좌우하 델타이동


# 다음에 먹을 물고기의 좌표(w)와 거리(nearest)를 반환하는 함수
# 조건: 1)최소거리, 2)위, 왼쪽 순서로 우선
def nearest_fish_bfs(arr, n, start, shark_size):
    queue = deque([start])
    distance = [[None] * n for _ in range(n)]  # 거리 함수
    distance[start[0]][start[1]] = 0
    nearest = 0  # 가장 가까운 거리
    edible = []  # 먹을 수 있는 물고기 좌표 저장

    # BFS 탐색
    while queue:
        r, c = queue.popleft()
        for dr, dc in drc:
            nr, nc = r + dr, c + dc  # 주변 좌표
            if 0 <= nr < N and 0 <= nc < N and distance[nr][nc] == None:
                # 물고기 없으면: 지나감
                if arr[nr][nc] == 0:
                    queue.append((nr, nc))
                    distance[nr][nc] = distance[r][c] + 1
                # 큰 물고기: 못지나감(큐에 추가x), 못먹음
                elif arr[nr][nc] > shark_size:
                    continue
                # 같은 물고기: 지나감, 못먹음
                elif arr[nr][nc] == shark_size:
                    queue.append((nr, nc))
                    distance[nr][nc] = distance[r][c] + 1
                # 작은 물고기: 먹을 수 있음
                elif 0 < arr[nr][nc] < shark_size:
                    queue.append((nr, nc))
                    distance[nr][nc] = distance[r][c] + 1

                    edible.append((nr, nc))
                    # nearest에 처음으로 저장되는 값이 최소거리(BFS 탐색)
                    if nearest == 0:
                        nearest = distance[nr][nc]

    # <중요> edible에 포함된 좌표를 정렬해줘야, 순서 조건을 만족할 수 있음
    edible.sort()
    for fish in edible:
        if distance[fish[0]][fish[1]] == nearest:
            return fish, nearest
    return False, False


# 먹을 수 있는 물고기 다 찾기까지 걸리는 시간을 반환하는 함수
def get_eating_time(arr, n, shark):
    result = 0  # 총 시간
    size = 2  # 아기상어 크기
    ate = 0  # 먹은 개수
    arr[shark[0]][shark[1]] = 0

    while True:
        w, time = nearest_fish_bfs(arr, n, shark, size)  # 다음 먹을 물고기 위치, 걸리는 시간

        # 먹을 물고기가 없다면, 결과 반환
        if not w:
            return result

        # 아기상어 크기 체크
        ate += 1
        if ate == size:
            size += 1
            ate = 0

        # 먹은 후: 시간 체크, 먹은 위치부터 다시 while문 시작
        result += time  # 총 시간 체크
        arr[w[0]][w[1]] = 0  # 먹은 물고기 제거
        shark = w  # 상어를 변경된 위치로 이동


# 아기상어 최초 위치 찾기
is_shark = False
for i in range(N):
    for j in range(N):
        if fish[i][j] == 9:
            shark = (i, j)
            is_shark = True
            break
    if is_shark:
        break

print(get_eating_time(fish, N, shark))

리팩토링한 코드를 돌려본 결과 아래와 같이 실행 시간이 줄어든 것을 확인할 수 있었다.