Coding Test/Programmers

[프로그래머스] 리틀 프렌즈 사천성 / C++

peachh 2021. 2. 28. 22:51
728x90

문제 설명


언제나 맛있는 음식들이 가득한 평화로운 푸드 타운. 푸드 타운에서 행복하게 사는 리틀 프렌즈들은 마을에 있는 매직 스푼을 보물처럼 보관하고 있다.

매직 스푼은 재료만 준비해서 냄비에 넣고 휘젓기만 하면 순식간에 최고의 요리로 만들어주는 신비의 아이템. 어느 날 매직 스푼을 호시탐탐 노리는 악당들이 보물을 훔쳐간다.

매직 스푼을 되찾고 다시 마을에 평화를 가져오기 위해 프렌즈의 대모험이 시작되는데...

리틀 프렌즈 사천성은 프렌즈 사천성과 유사한 게임이다. 게임은 2차원 배열에서 진행되는데, 여러 가지 무늬로 구성된 타일이 배치되어 있으며 같은 모양의 타일은 두 개씩 배치되어 있다. 게임의 목적은 배치된 모든 타일을 제거하는 것으로, 같은 모양의 타일을 규칙에 따라 제거하면 된다. 타일을 제거할 수 있는 경우는 다음과 같다.

다음 조건을 만족하는 경로가 있을 때 두 타일을 제거할 수 있다.

  • 경로의 양 끝은 제거하려는 두 타일이다.
  • 경로는 두 개 이하의 수평/수직 선분으로 구성되어 있고, 이들은 모두 연결되어 있다. (즉, 경로를 한 번 이하로 꺾을 수 있다)
    • 참고: 프렌즈 사천성은 경로가 세 개 이하의 선분으로 구성되어야 한다는 점이 다르다. (즉, 경로를 두 번 이하로 꺾을 수 있다)
  • 경로 상에는 다른 타일 또는 장애물이 없어야 한다.

위의 배열에서 어피치 타일은 직선의 경로로 이을 수 있으므로 제거 가능하다. 라이언 타일 역시 한 번 꺾인 경로로 연결되므로 제거 가능하다. 무지 타일의 경우 다른 타일을 지나지 않는 경로는 두 번 꺾여야 하므로 제거할 수 없는 타일이며, 튜브 타일 역시 직선의 경로 사이에 장애물이 있으므로 제거 가능하지 않다.

타일 배열이 주어졌을 때, 규칙에 따라 타일을 모두 제거할 수 있는지, 그 경우 어떤 순서로 타일을 제거하면 되는지 구하는 프로그램을 작성해보자.

 

입력 형식

입력은 게임판의 크기를 나타내는 m과 n, 그리고 배치된 타일의 정보를 담은 문자열 배열 board로 주어진다. 이 배열의 크기는 m이며, 각각의 원소는 n글자의 문자열로 구성되어 있다. 입력되는 값의 제한조건은 다음과 같다.

  • 1 <= m, n <= 100
  • board의 원소는 아래 나열된 문자로 구성된 문자열이다. 각 문자의 의미는 다음과 같다.
    • .: 빈칸을 나타낸다.
    • *: 막힌 칸을 나타낸다.
    • 알파벳 대문자(A-Z): 타일을 나타낸다. 이 문제에서, 같은 글자로 이루어진 타일은 한 테스트 케이스에 항상 두 개씩만 존재한다.
    • board에는 알파벳 대문자가 항상 존재한다. 즉, 타일이 없는 입력은 주어지지 않는다.

 

출력 형식

해가 존재하는 경우 타일을 제거하는 순서대로 한 글자씩 이루어진 문자열을, 그렇지 않은 경우 IMPOSSIBLE을 리턴한다. 해가 여러 가지인 경우, 알파벳 순으로 가장 먼저인 문자열을 리턴한다.

 

입출력 예

m n board answer
3 3 ["DBA", "C*A", "CDB"] "ABCD"
2 4 ["NRYN", "ARYA"] "RYAN"
4 4 [".ZI.", "M.**", "MZU.", ".IU."] "MUZI"
2 2 ["AB", "BA"] "IMPOSSIBLE"

 

 

코드

#include <string>
#include <vector>
#include <queue>
#include <map>
#include <cstring>

using namespace std;

struct pos
{
    int m;      // m 좌표
    int n;      // n 좌표
    int r;      // 이동 방향
    bool turn;  // 꺾은 적이 있는지
    
    void init(int m, int n, int r, bool turn)
    {
        this->m = m;
        this->n = n;
        this->r = r;
        this->turn = turn;
    }
};

int M;
int N;
int d[] = {-1, 1, 0, 0};    		// 상/하/우/좌
bool visit[101][101][4];    		// visit[m][n][r]은 r 방향으로 이동해 (m, n)에 방문한 적이 있는지를 의미
map<char, vector<pair<int, int>>> tile; // 타일에 적힌 알파벳과 타일의 좌표

// 범위 내의 좌표인지 확인
bool in_range(int m, int n)
{
    if (m < 0 || m >= M || n < 0 || n >= N)
        return false;
    return true;
}

// 주어진 알파벳의 나머지 타일을 찾음 (BFS)
bool find_pair(char target, vector<string> &board)
{
    queue<pos> q;
    pos curr, next;
    int i, m, n;
    
    // 초기화
    memset(visit, false, sizeof(visit));
    
    // 시작 타일의 좌표
    m = tile[target][0].first;
    n = tile[target][0].second;
    
    // 시작 타일에 대해 모든 방향의 타일을 큐에 삽입 및 방문 표시
    for (i = 0; i < 4; i++)
    {
        visit[m][n][i] = true;
        next.init(m + d[i], n + d[3 - i], i, false);
        if (!in_range(next.m, next.n))
        {
            continue;
        }
        q.push(next);
        visit[next.m][next.n][i] = true;
    }
    
    // 도착 타일을 찾음
    while (!q.empty())
    {
        curr = q.front();
        q.pop();
        
        // 찾았다면 타일을 제거하고 true 리턴
        if (board[curr.m][curr.n] == target)
        {
            board[m][n] = '.';
            board[curr.m][curr.n] = '.';
            return true;
        }
        
        // 빈칸이 아니면 더 이상 이동할 수 없으므로 패스
        if (board[curr.m][curr.n] != '.')
        {
            continue;
        }
        
        // 빈칸이면 다음 타일을 큐에 삽입 및 방문 표시
        for (i = 0; i < 4; i++)
        {
            // 다음 타일
            next.init(curr.m + d[i], curr.n + d[3 - i], i, curr.turn);
            
            // 방향이 달라서 꺾어야 하는 경우
            if (curr.r != i)
            {
                // 이미 꺾었다면 꺾을 수 없으므로 패스
                if (curr.turn)
                {
                    continue;
                }
                // 꺾었다는 것을 표시
                next.turn = true;
            }
            
            // 범위 밖이거나 방문한 적이 있다면 패스
            if (!in_range(next.m, next.n) || visit[next.m][next.n][next.r])
            {
                continue;
            }
            
            // 큐에 삽입 후 방문 표시
            q.push(next);
            visit[next.m][next.n][next.r] = true;
        }
    }
    // 도착 타일로 갈 수 없는 경우
    return false;
}

// 알파벳 순으로 타일을 제거
void find(string &answer, vector<string> &board)
{
    char target;    // 짝을 찾을 타일
    bool remove;    // 제거된 타일이 있는지
    
    // 모든 타일을 제거할 때까지
    while (!tile.empty())
    {
        remove = false;
        
        // 알파벳 순서대로 확인
        for (auto t : tile)
        {
            target = t.first;
            
            // 짝을 찾았다면 answer에 추가하고 제거
            if (find_pair(target, board))
            {
                remove = true;
                answer += target;
                tile.erase(target);
                break;
            }
        }
        
        // 더 이상 제거할 수 있는 타일이 없는 경우
        if (!remove)
        {
            answer = "IMPOSSIBLE";
            break;
        }
    }
}

// 존재하는 모든 타일을 확인
void check_tile(vector<string> &board)
{
    int i, j;
    
    // 초기화
    tile.clear();
    
    // 알파벳 대문자가 적인 타일의 좌표를 저장
    for (i = 0; i < M; i++)
    {
        for (j = 0; j < N; j++)
        {
            if (isalpha(board[i][j]))
            {
                tile[board[i][j]].push_back(make_pair(i, j));
            }
        }
    }
}

string solution(int m, int n, vector<string> board)
{
    string answer = "";
    
    M = m;
    N = n;
    check_tile(board);
    find(answer, board);
    return answer;
}

 

우선 제거해야 할 모든 타일을 check_tile() 함수를 통해 찾는다. 다음은 check_tile() 함수를 수행한 후 tile에 저장된 값을 나타낸다.

tile

tile을 순서대로 읽어서 알파벳 순으로 타일을 제거한다. 첫 번째 좌표에서 시작해서 두 번째 좌표로 갈 수 있는지 BFS를 통해 확인한다. 

나는 check_tile() 함수 내에서 tile을 초기화해주지 않아 실패가 떴었다. 전역 변수는 반드시 함수 내에서 초기화해야 한다.

 

링크

 

코딩테스트 연습 - 리틀 프렌즈 사천성

리틀 프렌즈 사천성 언제나 맛있는 음식들이 가득한 평화로운 푸드 타운. 푸드 타운에서 행복하게 사는 리틀 프렌즈들은 마을에 있는 매직 스푼을 보물처럼 보관하고 있다. 매직 스푼은 재료만

programmers.co.kr

 

inbdni/Programmers

Contribute to inbdni/Programmers development by creating an account on GitHub.

github.com

 

728x90