Lv3. [PCCP 기출문제] 4번 / 수레 움직이기

수레 움직이기

프로그래머스 문제

문제 설명

n x m 크기 격자 모양의 퍼즐판이 주어집니다.

퍼즐판에는 빨간색 수레와 파란색 수레가 하나씩 존재합니다. 각 수레들은 자신의 시작 칸에서부터 자신의 도착 칸까지 이동해야 합니다.

모든 수레들을 각자의 도착 칸으로 이동시키면 퍼즐을 풀 수 있습니다.

당신은 각 턴마다 반드시 모든 수레를 상하좌우로 인접한 칸 중 한 칸으로 움직여야 합니다. 단, 수레를 움직일 때는 아래와 같은 규칙이 있습니다.

  • 수레는 벽이나 격자 판 밖으로 움직일 수 없습니다.

  • 수레는 자신이 방문했던 칸으로 움직일 수 없습니다.

  • 자신의 도착 칸에 위치한 수레는 움직이지 않습니다. 계속 해당 칸에 고정해 놓아야 합니다.

  • 동시에 두 수레를 같은 칸으로 움직일 수 없습니다.

  • 수레끼리 자리를 바꾸며 움직일 수 없습니다.

시각화

아래와 같이 정보가 주어졌다.

수레움직이기-1.png

3턴이 지나면 각자 도착지점에 도착하게 된다.

수레움직이기-2.png

문제 해결 과정

제한사항을 보면 가로, 세로가 최대 4이다. 그렇기에 BFS 로 풀어되겠다고 생각했다. 어떻게 보면 제한사항이 많고 1턴마다 2개가 움직여야하는 것을 제외하고는 특별한 것은 없다.

문제 설명에 나온 것처럼 제한 사항은 아래와 같다.

  1. 각 수레는 같은 칸에 위치할 수 없다.
  2. 수레는 벽이나 격자 판 밖으로 움직일 수 없다.
  3. 수레는 자신이 방문했던 칸으로 움직일 수 없다.
  4. 자신의 도착 칸에 위치한 수레는 움직이지 않는다.
  5. 수레끼리 자리를 바꾸며 움직일 수 없다.

기본적인 로직을 작성하면서 위 조건을 만족하도록 코드를 작성해보자.

각 수레 좌표와 목표 좌표 찾기

1 ~ 5 값이 있는데 5는 벽이고 1, 2, 3, 4는 각각 빨간 수레, 파란 수레, 빨간 수레 목표, 파란 수레 목표이다.

        for y, row in enumerate(maze):
            for x, col in enumerate(row):
                if col == 1:
                    red_yx = (y, x)
                if col == 2:
                    blue_yx = (y, x)
                if col == 3:
                    target_red_yx = (y, x)
                if col == 4:
                    target_blue_yx = (y, x)

각 수레 움직임 경우의 수 구하기

product 는 데카르트 곱을 구하는 함수이다. 파이썬인 경우 간편하게 import 해서 사용하면 되지만 다른 언어의 경우 데카르트 곱을 구하는 함수를 만들어서 사용하면 된다.

        move_pos = list(product([(-1, 0), (1, 0), (0, 1), (0, -1)], repeat=2))

수레는 상하좌우로 움직일 수 있으므로 위와 같이 4가지 경우의 수를 구했다. 수레가 2개이기에 경우의 수도 4가지의 제곱이다.

즉 빨간 수레가 상하좌우로 움직일 수 있는 경우의 수는 4가지이고, 파란 수레가 상하좌우로 움직일 수 있는 경우의 수도 4가지이다. 그렇기에 두 수레가 움직일 수 있는 경우의 수는 4가지의 제곱이다.

BFS 로직 구현

아래 코드는 BFS 안에 들어갈 허용하지 않는 케이스이다.

좌표를 벗어난 경우
            if (red_yx[0] < 0 or red_yx[0] >= len(maze)) or (red_yx[1] < 0 or red_yx[1] >= len(maze[0])):
                continue
            if (blue_yx[0] < 0 or blue_yx[0] >= len(maze)) or (blue_yx[1] < 0 or blue_yx[1] >= len(maze[0])):
                continue
왔던 곳인 경우
            if (red_yx != target_red_yx and ''.join(map(str, list(red_yx))) in red_history) or (
                    blue_yx != target_blue_yx and ''.join(map(str, list(blue_yx))) in blue_history):
                continue
벽에 부딪힌 경우
            if maze[red_yx[0]][red_yx[1]] == 5 or maze[blue_yx[0]][blue_yx[1]] == 5:
                continue
두 수레가 같은 위치에 있는 경우
            if red_yx == blue_yx:
                continue
두 수레가 서로 위치를 바꾸는 경우
                if next_red_yx == blue_yx and next_blue_yx == red_yx:
                    continue
이미 한 수레가 목표에 도달한 경우

한 수레가 이미 목표에 도착한 경우 움직이지 않아야한다.

            next_red_yx = red_yx if red_yx == target_red_yx else (red_yx[0] + v[0][0], red_yx[1] + v[0][1])
            next_blue_yx = blue_yx if blue_yx == target_blue_yx else (blue_yx[0] + v[1][0], blue_yx[1] + v[1][1])

위 논리를 조합해서 BFS 를 구현하는 아래 전체 코드와 같다.

전체 코드

위 논리를 코드로 적으면 아래와 같다.

from collections import deque
from itertools import product


def solution(maze):
    def bfs():
        min_dist = float('inf')
        red_yx, blue_yx, target_red_yx, target_blue_yx = (0, 0), (0, 0), (0, 0), (0, 0)

        move_pos = list(product([(-1, 0), (1, 0), (0, 1), (0, -1)], repeat=2))

        for y, row in enumerate(maze):
            for x, col in enumerate(row):
                if col == 1:
                    red_yx = (y, x)
                if col == 2:
                    blue_yx = (y, x)
                if col == 3:
                    target_red_yx = (y, x)
                if col == 4:
                    target_blue_yx = (y, x)

        q = deque([((red_yx[0], red_yx[1]), (blue_yx[0], blue_yx[1]), {}, {})])

        while q:
            red_yx, blue_yx, red_history, blue_history = q.popleft()

            if min_dist <= len(red_history):
                continue

            if red_yx == target_red_yx and blue_yx == target_blue_yx:
                if max(len(red_history), len(blue_history)) < min_dist:
                    min_dist = max(len(red_history), len(blue_history))
                continue

            if (red_yx[0] < 0 or red_yx[0] >= len(maze)) or (red_yx[1] < 0 or red_yx[1] >= len(maze[0])):
                continue
            if (blue_yx[0] < 0 or blue_yx[0] >= len(maze)) or (blue_yx[1] < 0 or blue_yx[1] >= len(maze[0])):
                continue
            if maze[red_yx[0]][red_yx[1]] == 5 or maze[blue_yx[0]][blue_yx[1]] == 5:
                continue
            if (red_yx != target_red_yx and ''.join(map(str, list(red_yx))) in red_history) or (
                    blue_yx != target_blue_yx and ''.join(map(str, list(blue_yx))) in blue_history):
                continue
            if red_yx == blue_yx:
                continue

            red_history[''.join(map(str, list(red_yx)))] = True
            blue_history[''.join(map(str, list(blue_yx)))] = True

            for v in move_pos:
                next_red_yx = red_yx if red_yx == target_red_yx else (red_yx[0] + v[0][0], red_yx[1] + v[0][1])
                next_blue_yx = blue_yx if blue_yx == target_blue_yx else (blue_yx[0] + v[1][0], blue_yx[1] + v[1][1])

                if next_red_yx == blue_yx and next_blue_yx == red_yx:
                    continue

                q.append((next_red_yx, next_blue_yx, dict(red_history), dict(blue_history)))
        return min_dist

    answer = bfs()
    return 0 if answer == float('inf') else answer

수레움직이기-3.png


Lv2. 연속된 부분 수열의 합

연속된 부분 수열의 합

Lv3. 연속 펄스 부분 수열의 합

수레 움직이기

NCloud LB & SourcePipeline 구축하기
tech collection 서비스 성능 개선하기
Selenium 복권 구매 자동화 만들어보기
디자인 패턴
책 리뷰
블로그 챌린지