[Problem Solving] 게임 맵 최단거리

Sean·2023년 1월 20일
0

Problem Solving

목록 보기
35/130

문제

ROR 게임은 두 팀으로 나누어서 진행하며, 상대 팀 진영을 먼저 파괴하면 이기는 게임입니다. 따라서, 각 팀은 상대 팀 진영에 최대한 빨리 도착하는 것이 유리합니다.

지금부터 당신은 한 팀의 팀원이 되어 게임을 진행하려고 합니다. 다음은 5 x 5 크기의 맵에, 당신의 캐릭터가 (행: 1, 열: 1) 위치에 있고, 상대 팀 진영은 (행: 5, 열: 5) 위치에 있는 경우의 예시입니다.

위 그림에서 검은색 부분은 벽으로 막혀있어 갈 수 없는 길이며, 흰색 부분은 갈 수 있는 길입니다. 캐릭터가 움직일 때는 동, 서, 남, 북 방향으로 한 칸씩 이동하며, 게임 맵을 벗어난 길은 갈 수 없습니다.
아래 예시는 캐릭터가 상대 팀 진영으로 가는 두 가지 방법을 나타내고 있습니다.

  • 첫 번째 방법은 11개의 칸을 지나서 상대 팀 진영에 도착했습니다.

  • 두 번째 방법은 15개의 칸을 지나서 상대팀 진영에 도착했습니다.

위 예시에서는 첫 번째 방법보다 더 빠르게 상대팀 진영에 도착하는 방법은 없으므로, 이 방법이 상대 팀 진영으로 가는 가장 빠른 방법입니다.

만약, 상대 팀이 자신의 팀 진영 주위에 벽을 세워두었다면 상대 팀 진영에 도착하지 못할 수도 있습니다. 예를 들어, 다음과 같은 경우에 당신의 캐릭터는 상대 팀 진영에 도착할 수 없습니다.

게임 맵의 상태 maps가 매개변수로 주어질 때, 캐릭터가 상대 팀 진영에 도착하기 위해서 지나가야 하는 칸의 개수의 최솟값을 return 하도록 solution 함수를 완성해주세요. 단, 상대 팀 진영에 도착할 수 없을 때는 -1을 return 해주세요.

조건

  • maps는 n x m 크기의 게임 맵의 상태가 들어있는 2차원 배열로, n과 m은 각각 1 이상 100 이하의 자연수입니다.
    • n과 m은 서로 같을 수도, 다를 수도 있지만, n과 m이 모두 1인 경우는 입력으로 주어지지 않습니다.
  • maps는 0과 1로만 이루어져 있으며, 0은 벽이 있는 자리, 1은 벽이 없는 자리를 나타냅니다.
  • 처음에 캐릭터는 게임 맵의 좌측 상단인 (1, 1) 위치에 있으며, 상대방 진영은 게임 맵의 우측 하단인 (n, m) 위치에 있습니다.

❌ 틀린 풀이

처음에는 생각나는 대로 바로 재귀함수를 이용한 DFS로 짜서 돌렸다. 그런데, 분명 풀이는 맞는데 오류가 떠서 봤더니 다음과 같았다.

Maximum call stack size exceeded

그래서 최단거리 유형의 문제에 대해서 더 찾아보았는데, 최단거리 유형의 문제는 DFS(재귀함수 이용)보단 BFS(Queue 이용)로 푸는 것이 더 효율적이고 빠르다는 것을 알았다.
(DFS는 쭉 내려갔을 때 그게 정답이 아닐 확률이 매우 높은데 BFS는 정답을 찾으면 그게 딱 최단거리 그 자체이다.)

⭕ 통과한 풀이

그래서 BFS를 사용하여 풀어보았다.
(참고한 풀이: https://hogumachu.tistory.com/9)

설명

  • visited 배열
    게임판과 똑같은 모양으로(2차원 배열) 만들어서 방문하지 않은 칸은 false, 방문한 칸은 true로 표시한다. (Array.from() 메소드와 Array.prototype.fill() 메소드를 이용하여 2차원 배열을 선언 및 0으로 초기화시킨다.)

  • queue 배열
    우리가 앞으로 방문해야 할 게임판의 칸의 정보를 담고 있다. (각 원소는 [row, col, count] 식의 정보를 담음)
    queue에서 하나씩 빼면서 해당 칸을 방문한 적 없으면 visit 함수를 실행하고, 해당 칸이 목적지이면 바로 answer에 대입해서 반환한다.

  • visit 함수: 방문 처리 및 다음 턴에 방문할 칸들을 찾아서 큐에 넣어주는 함수
    파라미터로 전달받은 칸을 visited 배열에 추가하고, 해당 칸을 동/서/남/북으로 움직였을 때 각 칸이 유효한 칸인지 검사한 다음, 유효한 칸만 queue에 push해준다.

코드

function solution(maps) {
    const n = maps.length;
    const m = maps[0].length;
    
    let answer = -1;
    
    // 이미 방문한 적 있는 노드를 저장할 배열 (맵과 같은 모양으로)
    let visited = Array.from(Array(n), () => Array(m).fill(false));
    
    // 방문해야 할 노드를 채워넣은 큐 (초기값도 같이 넣어준다)
    let queue = [[0, 0, 1]];
    
    // 동서남북의 움직임을 표현하기 위한 배열 선언
    let dx = [1, -1, 0, 0];
    let dy = [0, 0, -1, 1];
    
    // BFS를 진행: 방문할 노드가 없을 때까지 계속한다.
    while(queue.length > 0){
        // 일단 queue 맨 앞을 꺼낸다.
        const current = queue.shift();
        
        // 만약 이게 도착지이면
        if(current[0] === n-1 && current[1] === m-1) {
            answer = current[2];
            break;
        }
        
        // 꺼낸 값이 방문하지 않았던 위치라면, 방문시킨다.
        if(!visited[current[0]][current[1]])
            visit(current[0], current[1], current[2]);
    }
    
    
    
    function visit(row, col, count) {
        visited[row][col] = true;
        
        // 현재 방문한 위치에서 새로 이동할 곳을 찾아준다.
        for(let i=0; i<4; i++) {
            const next_row = row + dx[i];
            const next_col = col + dy[i];
            
            // 1. 다음 위치는 게임의 맵 내부에 있어야 하며,
            // 2. 방문하지도 않았어야 하고,
            // 3. 벽이 아니어야 한다.
            // 위 3개의 조건을 만족한다면 방문할 큐에 넣어준다.
            if(next_row >= 0 && next_row < n && next_col >= 0 && next_col < m && !visited[next_row][next_col] && maps[next_row][next_col] === 1)
                queue.push([next_row, next_col, count+1])
        }
    }
    
    return answer;
    
}

파이썬 코드

이 코드 같은 경우엔 통과하긴 했지만, 사실 잘못 된 코드이다.
BFS에서는 방문처리를 큐에 삽입할 때 해야 한다!! (진짜 방문했을 때 방문처리 하는 건 DFS에서 하는 것)

from collections import deque

def solution(maps):
    n = len(maps)   #행 수
    m = len(maps[0]) #열 수
    #상하좌우 정의
    dx = [0, 0, -1, 1]
    dy = [-1, 1, 0, 0]
    answer = -1
    visited = [[False] * m for _ in range(n)]
    
    def bfs():
        queue = deque()
        queue.append([0, 0, 1])
        nonlocal answer
        
        while len(queue):
            currentRow, currentCol, cnt = queue.popleft()
            if visited[currentRow][currentCol] == False:
                visited[currentRow][currentCol] = True
                if currentRow == n-1 and currentCol == m-1:
                    answer = cnt
                    break
                for i in range(4):
                    nextRow = currentRow + dy[i]
                    nextCol = currentCol + dx[i]
                    if nextCol>=0 and nextRow>=0 and nextCol<m and nextRow<n and maps[nextRow][nextCol] != 0 and not visited[nextRow][nextCol]:
                        queue.append([nextRow, nextCol, cnt+1])
        
    bfs()
    
    return answer
profile
여러 프로젝트보다 하나라도 제대로, 깔끔하게.

0개의 댓글