코딩테스트/백준 주제별

[백준 그래프이론] 1600 말이 되고픈 원숭이

scone 2022. 7. 11. 18:59

🥕 [ 백준 1600 ] 말이 되고픈 원숭이

문제 링크

url : https://www.acmicpc.net/problem/1600

 

1600번: 말이 되고픈 원숭이

첫째 줄에 정수 K가 주어진다. 둘째 줄에 격자판의 가로길이 W, 세로길이 H가 주어진다. 그 다음 H줄에 걸쳐 W개의 숫자가 주어지는데, 0은 아무것도 없는 평지, 1은 장애물을 뜻한다. 장애물이 있

www.acmicpc.net

 

🍒 문제 분석

원숭이는 체스의 나이트와 같은 이동 방식을 가진다.

단, K번만 그럴 수 있고, 그 외에는 인접한 네칸 으로만 움직일 수 있다.( 위, 아래, 왼쪽, 오른쪽 )

원숭이가 최소한의 동작으로 시작지점에서 도착지점 까지 갈 수 있는 방법을 알아내는 프로그램을 작성해보는 문제이다.

 

최소한의 동작으로 목적지를 도착 한다는건 다시말해

BFS 알고리즘으로 풀면 된다는 의미로 받아들이면 될 것 같다.

 

BFS 알고리즘 을 생각한 이유

1. 왜냐면 간선의 cost를 계산해야 하는 것도 아니라는 점.

2. 현재 위치하고 있는 노드를 기준으로 갈 수 있는 곳들을 다 q에 넣고 넣은 q들 다 빼고 다시 넣을 때,

3. 동작 값을 1씩 추가하는 방식으로 계산하면 도착지점 까지 가장 먼저 도착하는 경로를 찾을 수 있다.

 

이제 고려해야할 점으로

1. 단순히 인접한 4방향 뿐만 아니라 체스의 나이트 처럼도 이동할 수 있다는 점

2. 체스의 나이트 처럼 이동하는데 K번의 제약 조건이 있다는 점

3. 인접한 4방향으로 이동할 때와 체스의 나이트 처럼 이동할 때 간에 동선이 서로 겹칠 수 있다는 점.

 

따라서 접근 방식

1. BFS 알고리즘을 쓰자.

2. direction 리스트를 인접한 4방향 버전과 체스의 나이트 버전으로 만들어야겠다.

3. 방문한 곳, visited를 표시할 때 인접한 4방향으로 이동해서 간 버전과 체스의 나이트 버전으로 해서 만들어야겠다.

 

이제 문제를 풀어보겠습니다.

 

🥑 코드

from collections import deque

# 인접한 4곳
md = [(-1,0),(1,0),(0,1),(0,-1)]

# 말의 이동 방향
hd = [(2,1),(1,2),(-1,2),(-2,1),(-2,-1),(-1,-2),(1,-2),(2,-1)]

def check_visit(nx, ny, nk):
    if 0<=nx<H and 0<=ny<W and not visited[nx][ny][nk] and graph[nx][ny] == 0 :
        return True   # 가도 됨
    return False      # 가면 안됨

def bfs():
    q = deque()
    q.append([0,0,0])
    while q:
        x, y, k = q.popleft()

        # 도착점인 경우
        if x==(H-1) and y==(W-1):
            # 처음에 시작점에서 동작수에 1를 매겨줬기 때문에 1을 빼준다.
            return visited[x][y][k] - 1
        
        # 인접한 곳으로 움직일 때
        for i in range(4):
            nx = x + md[i][1]
            ny = y + md[i][0]

            if check_visit(nx,ny,k):     # 가도 되면
                q.append([nx,ny,k])      # 간다.
                visited[nx][ny][k] = visited[x][y][k] + 1
        
        # 말처럼 움직일 때
        if k < K:
            for i in range(8):
                nx = x + hd[i][1]
                ny = y + hd[i][0]
                nk = k + 1
                if check_visit(nx,ny,nk):
                    q.append([nx,ny,nk])
                    visited[nx][ny][nk] = visited[x][y][k] + 1
    return -1



# 말 처럼 이동 가능한 횟수
K = int(input())

# W 열 갯수(가로길이) => y, H 행 갯수(세로 길이) =>x
W, H = map(int, input().split())

# graph[행][열] => graph[H][W] => graph[x][y] 
graph = [list(map(int,input().split())) for _ in range(H)]

# 말의 이동을 고려하기 위한 3차원 배열
visited = [[[0]*(K+1) for _ in range(W)] for _ in range(H)]

# 시작점 방문
visited[0][0][0] = 1

print(bfs())

🍓 내 해결 과정

초록색 : 인접한 4곳

빨간색 : 말처럼 갈 수 있는 곳 8곳

 

저렇게 12개의 위치가 현재 위치(검은색) 에서 갈 수 있는 다음 노드가 됩니다.

 

bfs  알고리즘이기 때문에 순서는 다음과 같습니다.

1. stage1 의 노드를 que에 넣고

2. que를 pop 한 뒤, 거기서 갈 수 있는 모든 노드를 que에 넣습니다.

3. 그럼 stage2의 노드들이 que에 들어가게 되고

4. 다시 stage2의 노드들을 pop하면 stage 3의 노드들이 que에 들어가게 됩니다.

 

stage는 원숭이가 움직인 횟수를 의미합니다.

따라서 stage1 -> stage2 -> .. 를 거치다가 목적지에 도착하면 움직인 횟수를 반환하고

 

원숭이가 더 이상 갈 곳이 없게 되면 다음 stage가 존재하지 않기 때문에 que가 텅 비게 되고

알고리즘이 끝나며 따라서 -1을 반환합니다.

 

visited 테이블의 형태는 다음과 같습니다.

3차원 배열입니다.

 

검은색 점은 (2, 1, 0) 으로 말의 움직임 없이 원숭이가 좌표 2,1 에 도착한걸 의미하죠.

원숭이의 움직임으로는 좌표 2,1 까지 3번에 걸쳐 가야하므로 visited[2][1][0] 에는 값이 4가 적히게 될 겁니다.

 

초록색 점은 (2,1,1) 으로 말의 움직임을 한번 써서 좌표 2,1 에 도착한 걸 의미합니다.

start 위치 좌표 0, 0 에서 말의 움직임으로 한번에 좌표 2,1에 도착할 수 있으니 visited[2][1][1] 에는 값이 2가 적히게 되는거죠.

 

왜냐면 좌표 0,0 에 값이 1이 적혀있었는데 그 상태에서 한칸 이동한 것이니 1만큼 더해져 2가 되는 것입니다.

 

다음처럼 visited table을 3차원 배열로 만들게 되면 원숭이가 말의 움직임을 한 번 써서 그 좌표에 도착한 것인지 아니면 그냥 도착한 것인지,  그것도 아니면 말의 움직임을 2번 써서 도착한 것인지 등을 알 수 있게 됩니다.

 

만약 visited 값이 True 라면 이미 방문한 셈이니 그 곳에는 갈 수 없지만, 말의 움직임을 쓰지 않고는 그 곳에 간 적은 없기 때문에 이를 모두 고려해주기 위해 3차원 배열로 만들게 된 것입니다.

 

 

🍉 깨달은 점 및 정리

1. 최소 동작으로 어느 목적지에 도착하는 문제에 bfs 알고리즘을 쓸 수 있다.

2. 목적지 까지 가는 수단이 두개 이상이고 이를 혼용 할 수 있다면, visited table을 n차원으로 구성해보자.