새소식

PS/프로그래머스

[프로그래머스/LV3] 사라지는 발판 (Python)

  • -

Problem :https://school.programmers.co.kr/learn/courses/30/lessons/92345

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

Status : Solved

 

Time : 00:31:12

 


 

문제 설명

 

더보기

플레이어 A와 플레이어 B가 서로 게임을 합니다. 당신은 이 게임이 끝날 때까지 양 플레이어가 캐릭터를 몇 번 움직이게 될지 예측하려고 합니다.

각 플레이어는 자신의 캐릭터 하나를 보드 위에 올려놓고 게임을 시작합니다. 게임 보드는 1x1 크기 정사각 격자로 이루어져 있으며, 보드 안에는 발판이 있는 부분과 없는 부분이 있습니다. 발판이 있는 곳에만 캐릭터가 서있을 수 있으며, 처음 캐릭터를 올려놓는 곳은 항상 발판이 있는 곳입니다. 캐릭터는 발판이 있는 곳으로만 이동할 수 있으며, 보드 밖으로 이동할 수 없습니다. 밟고 있던 발판은 그 위에 있던 캐릭터가 다른 곳으로 이동하여 다른 발판을 밞음과 동시에 사라집니다. 양 플레이어는 번갈아가며 자기 차례에 자신의 캐릭터를 상하좌우로 인접한 4개의 칸 중에서 발판이 있는 칸으로 옮겨야 합니다.

다음과 같은 2가지 상황에서 패자와 승자가 정해지며, 게임이 종료됩니다.

 

  • 움직일 차례인데 캐릭터의 상하좌우 주변 4칸이 모두 발판이 없거나 보드 밖이라서 이동할 수 없는 경우, 해당 차례 플레이어는 패배합니다.
  • 두 캐릭터가 같은 발판 위에 있을 때, 상대 플레이어의 캐릭터가 다른 발판으로 이동하여 자신의 캐릭터가 서있던 발판이 사라지게 되면 패배합니다.

게임은 항상 플레이어 A가 먼저 시작합니다. 양 플레이어는 최적의 플레이를 합니다. 즉, 이길 수 있는 플레이어는 최대한 빨리 승리하도록 플레이하고, 질 수밖에 없는 플레이어는 최대한 오래 버티도록 플레이합니다. '이길 수 있는 플레이어'는 실수만 하지 않는다면 항상 이기는 플레이어를 의미하며, '질 수밖에 없는 플레이어'는 최선을 다해도 상대가 실수하지 않으면 항상 질 수밖에 없는 플레이어를 의미합니다. 최대한 오래 버틴다는 것은 양 플레이어가 캐릭터를 움직이는 횟수를 최대화한다는 것을 의미합니다.

아래 그림은 초기 보드의 상태와 각 플레이어의 위치를 나타내는 예시입니다.

 

위와 같은 경우, 플레이어 A는 실수만 하지 않는다면 항상 이길 수 있습니다. 따라서 플레이어 A는 이길 수 있는 플레이어이며, B는 질 수밖에 없는 플레이어입니다. 다음은 A와 B가 최적의 플레이를 하는 과정을 나타냅니다.

  1. 플레이어 A가 초기 위치 (1, 0)에서 (1, 1)로 이동합니다. 플레이어 A가 (0, 0)이나 (2, 0)으로 이동할 경우 승리를 보장할 수 없습니다. 따라서 무조건 이길 방법이 있는 (1, 1)로 이동합니다.
  2. 플레이어 B는 (1, 1)로 이동할 경우, 바로 다음 차례에 A가 위 또는 아래 방향으로 이동하면 발판이 없어져 패배하게 됩니다. 질 수밖에 없는 플레이어는 최대한 오래 버티도록 플레이하기 때문에 (1, 1)로 이동하지 않습니다. (1, 2)에서 위쪽 칸인 (0, 2)로 이동합니다.
  3. A가 (1, 1)에서 (0, 1)로 이동합니다.
  4. B에게는 남은 선택지가 (0, 1)밖에 없습니다. 따라서 (0, 2)에서 (0, 1)로 이동합니다.
  5. A가 (0, 1)에서 (0, 0)으로 이동합니다. 이동을 완료함과 동시에 B가 서있던 (0, 1)의 발판이 사라집니다. B가 패배합니다.
  6. 만약 과정 2에서 B가 아래쪽 칸인 (2, 2)로 이동하더라도 A는 (2, 1)로 이동하면 됩니다. 이후 B가 (2, 1)로 이동, 다음 차례에 A가 (2, 0)으로 이동하면 B가 패배합니다.

위 예시에서 양 플레이어가 최적의 플레이를 했을 경우, 캐릭터의 이동 횟수 합은 5입니다. 최적의 플레이를 하는 방법은 여러 가지일 수 있으나, 이동한 횟수는 모두 5로 같습니다.

게임 보드의 초기 상태를 나타내는 2차원 정수 배열 board와 플레이어 A의 캐릭터 초기 위치를 나타내는 정수 배열 aloc, 플레이어 B의 캐릭터 초기 위치를 나타내는 정수 배열 bloc이 매개변수로 주어집니다. 양 플레이어가 최적의 플레이를 했을 때, 두 캐릭터가 움직인 횟수의 합을 return 하도록 solution 함수를 완성해주세요.

 

입력 및 출력

 

더보기

제한사항

  • 1 ≤ board의 세로 길이 ≤ 5
  • 1 ≤ board의 가로 길이 ≤ 5
  • board의 원소는 0 또는 1입니다.
    • 0은 발판이 없음을, 1은 발판이 있음을 나타냅니다.
    • 게임 보드의 좌측 상단 좌표는 (0, 0), 우측 하단 좌표는 (board의 세로 길이 - 1, board의 가로 길이 - 1)입니다.
  • aloc과 bloc은 각각 플레이어 A의 캐릭터와 플레이어 B의 캐릭터 초기 위치를 나타내는 좌표값이며 [r, c] 형태입니다.
    • r은 몇 번째 행인지를 나타냅니다.
    • 0 ≤ r < board의 세로 길이
    • c는 몇 번째 열인지를 나타냅니다.
    • 0 ≤ c < board의 가로 길이
    • 초기 보드의 aloc과 bloc 위치는 항상 발판이 있는 곳입니다.
    • aloc과 bloc이 같을 수 있습니다.
  • 상대 플레이어의 캐릭터가 있는 칸으로 이동할 수 있습니다.

 

입출력

 

board aloc bloc result
[[1, 1, 1], [1, 1, 1], [1, 1, 1]] [1, 0] [1, 2] 5
[[1, 1, 1], [1, 0, 1], [1, 1, 1]] [1, 0] [1, 2] 4
[[1, 1, 1, 1, 1]] [0, 0] [0, 4] 4
[[1]] [0, 0] [0, 0] 0

 

 


 

풀이

 

대망의 마지막 LV3 문제. 이젠 당분간 LV3문제는 쳐다보지 않아도 되겠다 ! ㅎㅎ

 

Min-max 알고리즘을 알고 있으면 꽤 쉬워지는 문제라고 볼 수 있다. 어차피 보드판의 크기는 최대 5x5고, 중복 방문이 불가능하므로 최대 25번의 move가 가능하다. 즉 모든 경우의 수에 대해 서로 가장 이익이 되는 방향으로 움직이면 된다.

 

  • 자신이 무조건 질 때 : 자신이 패배하므로, 가장 많은 횟수를 움직일 수 있는 방향으로 움직인다
  • 자신이 이기는 수가 있을 때 : 자신이 승리하므로 가장 적은 횟수를 움직일 수 있는 방향으로 움직인다.

 

자, 이제 전제조건을 살펴보았으니 모든 경우를 한 번 구체적으로 따져보자.

 

우선 자신이 더 이상 움직일 칸이 없거나, 혹은 자신의 현재 서 있는 칸이 비어있는 경우 자신은 무조건 패배한다. 즉 지금의 패배 선언과(False) 총 move횟수를 반환하면 된다. 움직임의 경우의 수를 트리로 만들 때, 리프 노드는 무조건 그 상황의 플레이어의 패배를 의미한다.

 

그리고 자신이 움직임이 가능한 경우에 대해, 움직일 수 있는 다음 상태는 현재 상태의 자식 노드와 같다. 다음을 따져보자.

  • 어떤 움직임(들)에 상대방이 패배 선언을 했을 경우 : 즉 그 움직임을 취하면 자신 및 상대방의 최적의 움직임을 통해 무조건 승리할 수 있다. 패배 선언한 경우의 move횟수 중 가장 작은 값과 함께 자신의 승리 선언(True)을 반환한다.
  • 모든 움직임에 상대방이 승리 선언을 했을 경우 : 모든 움직임에 의해 자신이 패배하므로, 모든 move횟수 중 가장 큰 값과 함께 자신의 패배 선언(False)을 반환한다.

A가 선공이므로, A, B를 반복적으로 DFS로 함수를 호출하여 모든 경우를 따져 볼 수 있을 것이다. 이러한 재귀적인 상호 함수 호출을 통해 최종적인 min-max move를 얻어낼 수 있다.

 

핵심 함수는 바로 위 경우의 수를 따져보는 상황의 구현이므로, 이번엔 설명을 생략하도록 하자.

 

 

풀이 코드



dr = [-1, 1, 0, 0]
dc = [0, 0, -1, 1]
MAX = float('inf')


def move_A(board, cnt, aloc, bloc) :
    global N, M
    
    ar, ac = aloc
    if board[ar][ac] == 0 :
        return False, cnt
    
    board[ar][ac] = 0
    is_movable = False
    win_cnt = 0
    min_cnt, max_cnt = MAX, -MAX
    
    for k in range(4) :
        _ar, _ac = ar + dr[k], ac + dc[k]
        if -1 < _ar < N and -1 < _ac < M and board[_ar][_ac] :
            is_b_win, _cnt = move_B(board, cnt+1, [_ar, _ac], bloc)
            is_movable = True
            if is_b_win :
                max_cnt = max(max_cnt, _cnt)
            else :
                win_cnt += 1
                min_cnt = min(min_cnt, _cnt)
            
            
    board[ar][ac] = 1
    if not is_movable :
        return False, cnt
    if not win_cnt :
        return False, max_cnt
    return True, min_cnt
    
def move_B(board, cnt, aloc, bloc) :
    global N, M
    
    br, bc = bloc
    if board[br][bc] == 0 :
        return False, cnt
    
    board[br][bc] = 0
    is_movable = False
    win_cnt = 0
    min_cnt, max_cnt = MAX, -MAX
    
    for k in range(4) :
        _br, _bc = br + dr[k], bc + dc[k]
        if -1 < _br < N and -1 < _bc < M and board[_br][_bc] :
            is_a_win, _cnt = move_A(board, cnt+1, aloc, [_br, _bc])
            is_movable = True
            if is_a_win :
                max_cnt = max(max_cnt, _cnt)
            else :
                win_cnt += 1
                min_cnt = min(min_cnt, _cnt)
                
    board[br][bc] = 1
    if not is_movable :
        return False, cnt
    if not win_cnt :
        return False, max_cnt
    return True, min_cnt

def solution(board, aloc, bloc):
    global N, M 
    N, M = len(board), len(board[0])

    _, cnt = move_A(board, 0, aloc, bloc)
    
    return cnt

 

풀이 완료~

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.