크레인 인형뽑기

LEEHAKJIN-VV·2022년 5월 22일
0

프로그래머스

목록 보기
8/37
post-thumbnail

출처: 프로그래머스 코딩 테스트 연습

문제 설명

게임개발자인 "죠르디"는 크레인 인형뽑기 기계를 모바일 게임으로 만들려고 합니다.
"죠르디"는 게임의 재미를 높이기 위해 화면 구성과 규칙을 다음과 같이 게임 로직에 반영하려고 합니다.

게임 화면은 "1 x 1" 크기의 칸들로 이루어진 "N x N" 크기의 정사각 격자이며 위쪽에는 크레인이 있고 오른쪽에는 바구니가 있습니다. (위 그림은 "5 x 5" 크기의 예시입니다). 각 격자 칸에는 다양한 인형이 들어 있으며 인형이 없는 칸은 빈칸입니다. 모든 인형은 "1 x 1" 크기의 격자 한 칸을 차지하며 격자의 가장 아래 칸부터 차곡차곡 쌓여 있습니다. 게임 사용자는 크레인을 좌우로 움직여서 멈춘 위치에서 가장 위에 있는 인형을 집어 올릴 수 있습니다. 집어 올린 인형은 바구니에 쌓이게 되는 데, 이때 바구니의 가장 아래 칸부터 인형이 순서대로 쌓이게 됩니다. 다음 그림은 [1번, 5번, 3번] 위치에서 순서대로 인형을 집어 올려 바구니에 담은 모습입니다.

만약 같은 모양의 인형 두 개가 바구니에 연속해서 쌓이게 되면 두 인형은 터뜨려지면서 바구니에서 사라지게 됩니다. 위 상태에서 이어서 [5번] 위치에서 인형을 집어 바구니에 쌓으면 같은 모양 인형 두 개가 없어집니다.

크레인 작동 시 인형이 집어지지 않는 경우는 없으나 만약 인형이 없는 곳에서 크레인을 작동시키는 경우에는 아무런 일도 일어나지 않습니다. 또한 바구니는 모든 인형이 들어갈 수 있을 만큼 충분히 크다고 가정합니다. (그림에서는 화면표시 제약으로 5칸만으로 표현하였음)

게임 화면의 격자의 상태가 담긴 2차원 배열 board와 인형을 집기 위해 크레인을 작동시킨 위치가 담긴 배열 moves가 매개변수로 주어질 때, 크레인을 모두 작동시킨 후 터트려져 사라진 인형의 개수를 return 하도록 solution 함수를 완성해주세요.

제한 사항

  • board 배열은 2차원 배열로 크기는 "5 x 5" 이상 "30 x 30" 이하입니다.
  • board의 각 칸에는 0 이상 100 이하인 정수가 담겨있습니다.
    • 0은 빈 칸을 나타냅니다.
    • 1 ~ 100의 각 숫자는 각기 다른 인형의 모양을 의미하며 같은 숫자는 같은 모양의 인형을 나타냅니다.
  • moves 배열의 크기는 1 이상 1,000 이하입니다.
  • moves 배열 각 원소들의 값은 1 이상이며 board 배열의 가로 크기 이하인 자연수입니다.

입출력 예

boardmovesresult
[[0,0,0,0,0],[0,0,1,0,3],[0,2,5,0,1],[4,2,4,4,2],[3,5,1,3,1]][1,5,3,5,1,2,1,4]4

입출력 예 설명

입출력 예 #1

인형의 처음 상태는 문제에 주어진 예시와 같습니다. 크레인이 [1, 5, 3, 5, 1, 2, 1, 4] 번 위치에서 차례대로 인형을 집어서 바구니에 옮겨 담은 후, 상태는 아래 그림과 같으며 바구니에 담는 과정에서 터트려져 사라진 인형은 4개 입니다.

내가 제출한 코드

import Foundation

struct Stack<T> {
    private var items = [T]()
    
    public mutating func push(_ item: T) { // 배열의 마지막에 item 추가
        items.append(item)
    }
  
    public mutating func pop() -> T? { // 배열의 마지막 item을 반환
        return items.popLast()
    }
    
    public func peek() -> T? { // 배열의 마지막 item을 확인
        return items.last
    }
}

func solution(_ board:[[Int]], _ moves:[Int]) -> Int {
    var dollMachine: [Stack<Int>] = [] // 인형뽑기 기계에 담긴 인형들(마지막 인덱스는 인형을 옮길 바구니)
    let basketSize: Int = board.count // 바구니의 크기
    var result: Int = 0 // 결과값 (터트려 사라진 인형)
    
    for i in 0..<basketSize+1 { // 인형을 담을 바구니들을 만듬
        var tmpStack = Stack<Int>()
        dollMachine.append(tmpStack)
    }
    
    for stack in board.reversed() { // 만든 바구니에 인형을 담음
        for (index, item) in stack.enumerated() {
            if item != 0 {
                dollMachine[index].push(item)          
            }
        }
    }
     
    for move in moves { // 바구니에서 뽑은 인형을 마지막 바구니에 옮기는 과정
        if let movedItem = dollMachine[move-1].pop() { // movedItem이 nil인 경우는 빈 바구니에서 인형집기를 시도한 것
            if let basketLastPopItem = dollMachine[basketSize].peek() {
                if movedItem == basketLastPopItem {
                    dollMachine[basketSize].pop()
                    result += 2
                } else { // 뽑은 인형과 바구니의 마지막에 있는 인형이 다른 경우
                    dollMachine[basketSize].push(movedItem)
                }
            } else { // 바구니에 인형이 1개도 없는 경우
                dollMachine[basketSize].push(movedItem)
            }
        }
    }
    return result
}

코드 설명

문제는 자료구조 stack을 이용하여 해결하였다. stack은 LIFO(Last-In, First-Out) 구조이다.

코드는 다음 과정으로 진행된다.
1. 인형을 담을 바구니를 만든다.
2. 만든 바구니에 뽑을 인형을 담는다.
3. 바구니에서 뽑은 인형을 제일 오른쪽(마지막) 바구니에 담는다.
4. 마지막 바구니의 제일 위에 있는 인형과 방금 뽑은 인형이 뽑으면 두 인형을 터트린다.
5. 3,4번 과정을 반복한다.

그러면 각 과정을 세부적으로 살펴본다.

뽑을 인형을 담을 dollMachine 프로퍼티를 선언하였다. 이는 Stack을 담는 배열 타입이다. 인형 뽑기의 바구니가 "N x N"의 크기를 가질 때 이 프로퍼티는 N 개의 stack을 가진다. for loop에서 범위를 basketSize+1로 지정한 이유는 dollMachine 배열의 마지막 스택을 뽑은 인형을 담을 바구니로 사용하기 위해서다.

var dollMachine: [Stack<Int>] = [] // 인형뽑기 기계에 담긴 인형들(마지막 인덱스는 인형을 옮길 바구니)
    let basketSize: Int = board.count // 바구니의 크기
    var result: Int = 0 // 결과값 (터트려 사라진 인형)
    
    for i in 0..<basketSize+1 { // 인형을 담을 바구니들을 만듬
        var tmpStack = Stack<Int>()
        dollMachine.append(tmpStack)
    }

아래 코드는 만든 바구니에 인형을 담는 과정이다. 인형들이 담긴 배열 board 프로퍼티를 뒤에서부터 읽어 바구니에 담는다.

 for stack in board.reversed() { // 만든 바구니에 인형을 담음
        for (index, item) in stack.enumerated() {
            if item != 0 {
                dollMachine[index].push(item)          
            }
        }
    }

아래 코드는 인형들이 바구니에서 1개씩 인형을 뽑아 마지막 바구니에 담는 과정이다. 우선 stack의 pop메소드를 이용하여 인형을 뽑아야 할 바구니에서 인형을 꺼낸다. 다음으로, 제일 마지막에 있는 바구니와 같은 인형인지 비교한다.(peek 메소드는 pop 메소드처럼 인형을 스택에서 꺼내서 확인하는 것이 아닌, 꺼내지 않고 확인한다고 생각)
만약 두 인형이 같다면, 마지막 바구니에서 인형을 꺼내고 두 인형을 터트린다. 두 인형이 다르다면 push 메소드를 이용하여 뽑은 인형을 마지막 바구니 위에 쌓는다. 이 과정을 크레인이 작동시킨 만큼 반복하여 터트려진 인형의 개수를 반환한다.

for move in moves { // 바구니에서 뽑은 인형을 마지막 바구니에 옮기는 과정
        if let movedItem = dollMachine[move-1].pop() { // movedItem이 nil인 경우는 빈 바구니에서 인형집기를 시도한 것
            if let basketLastPopItem = dollMachine[basketSize].peek() {
                if movedItem == basketLastPopItem {
                    dollMachine[basketSize].pop()
                    result += 2
                } else { // 뽑은 인형과 바구니의 마지막에 있는 인형이 다른 경우
                    dollMachine[basketSize].push(movedItem)
                }
            } else { // 바구니에 인형이 1개도 없는 경우
                dollMachine[basketSize].push(movedItem)
            }
        }
    }

몰랐던 사실 or 기억하면 도움이 될 만한 사실

Swift의 Stack 구현

Swift에서는 Stack을 구현하는 라이브러리가 존재하지 않는다. 그러므로 구조체와 제네릭을 사용하여 직접 구현하여야한다. 제네릭을 사용하는 이유는 스택에 원하는 타입을 담기 위해서이다.

스택은 다음과 같이 구현하였다.

struct Stack<T> {
    private var items = [T]()
    
    public mutating func push(_ item: T) { // 배열의 마지막에 item 추가
        items.append(item)
    }
  
    public mutating func pop() -> T? { // 배열의 마지막 item을 반환
        return items.popLast()
    }
    
    public func peek() -> T? { // 배열의 마지막 item을 확인
        return items.last
    }
}

우선 스택에 저장할 값들을 관리하는 items 배열 프로퍼티를 선언한다. push 메소드는 파라미터로 전달된 item을 items 프로퍼티에 추가한다. pop메소드는 스택의 마지막(제일 최근에 들어온) item을 반환한다. 스택에 저장되어 있는 item이 1개도 없는 경우 nil을 반환한다. (pop 메소드에서 사용되는 popLast메소드는 배열의 마지막 원소가 없을 때 nil을 반환함) peek 메소드는 스택의 마지막 원소 값을 확인해주는 메소드다. pop메소드와 마찬가지로 스택에 item이 없는 경우, nil을 반환한다. 글쓴이가 구현한 메소드 말고 추가적으로 메소드를 구현할 수 있다.(isEmpty, count.. 등)

결론은 Swift에서 스택 or 큐와 같은 자료구조를 구현할 때는 구조체에 제네릭을 같이 사용하여 구현하도록 하자.

Array의 역순 순회

보통 배열을 순회할 때 for-in loop를 사용하여 순방향(인덱스0부터 시작)으로 읽는다.

그러나 reversed 메소드를 이용하여 컬렉션 타입을 역순으로 읽을 수 있다.

let tmp: [Int] = [1,3,5,7,9]

for value in tmp.reversed() {
    print(value)
}
// Prints "9"
// Prints "7"
// Prints "5"
// Prints "3"
// Prints "1"

0개의 댓글