[알고리즘] 백준- 14500 테트로미노(구현, dfs)

Sujung Shin·2023년 5월 8일
2
post-thumbnail

문제 풀러 바로가기 >> 14500 테트로미노

🔗 문제


폴리오미노란 크기가 1×1인 정사각형을 여러 개 이어서 붙인 도형이며, 다음과 같은 조건을 만족해야 한다.

  • 정사각형은 서로 겹치면 안 된다.
  • 도형은 모두 연결되어 있어야 한다.
  • 정사각형의 변끼리 연결되어 있어야 한다. 즉, 꼭짓점과 꼭짓점만 맞닿아 있으면 안 된다.

정사각형 4개를 이어 붙인 폴리오미노는 테트로미노라고 하며, 다음과 같은 5가지가 있다.
아름이는 크기가 N×M인 종이 위에 테트로미노 하나를 놓으려고 한다. 종이는 1×1 크기의 칸으로 나누어져 있으며, 각각의 칸에는 정수가 하나 쓰여 있다.

테트로미노 하나를 적절히 놓아서 테트로미노가 놓인 칸에 쓰여 있는 수들의 합을 최대로 하는 프로그램을 작성하시오.

테트로미노는 반드시 한 정사각형이 정확히 하나의 칸을 포함하도록 놓아야 하며, 회전이나 대칭을 시켜도 된다.


입력


첫째 줄에 종이의 세로 크기 N과 가로 크기 M이 주어진다. (4 ≤ N, M ≤ 500)

둘째 줄부터 N개의 줄에 종이에 쓰여 있는 수가 주어진다. i번째 줄의 j번째 수는 위에서부터 i번째 칸, 왼쪽에서부터 j번째 칸에 쓰여 있는 수이다. 입력으로 주어지는 수는 1,000을 넘지 않는 자연수이다.


출력


첫째 줄에 테트로미노가 놓인 칸에 쓰인 수들의 합의 최댓값을 출력한다.


😒 내가 생각한 아이디어


👀 예제를 통해 이해해보기


크게 2가지 풀이방법이 있다.

  1. 모든 경우의 수를 직접 구현해주기
  2. dfs

사실 맨 처음 떠오른 건 1번 풀이였다. 그러나... 구현하기에는 너무 잔혹(?)하다고 생각해서 잘 생각해보니까, '맵' 안에서 푸는 문제였기에 아, 혹시 bfs로 푸는 건가? 라고 생각했다.
그런데 잘 생각해보면, 이 'ㅁ' 모양은 bfs로 풀면 안된다.

그렇다면 dfs 는 어떨까? 잘 생각해보면, 보라색 'ㅜ'를 제외한 나머지 모양들은 depth가 3인 dfs로 구현할 수 있다.
(한붓그리기가 되기 때문이다!)

그렇다면 나머지 보라색 'ㅜ' 모양에 대한 처리를 어떻게 해주는 것인지가 관건이겠다.
왜냐하면 보라색 'ㅜ' 모양은 depth가 3인 dfs로 풀 수 없기에...
그래서 처음에는 '+' (십자가) 모양을 만든 다음에, 그 직전 값을 빼어주면 'ㅜ'에 대해 대칭/회전 한 'ㅜ', 'ㅓ', 'ㅗ' , 'ㅏ' 모양이 나올 거라고 생각해서 다음과 같이 코드를 구현했다:


처음에 짠 코드(틀린 코드입니다.)

// 14500 테트로미노 (구현, 백트래킹)
/*
    'ㅜ' 모양을 제외한 나머지는 depth가 0부터 시작하여 4으로 끝나는 백트래킹으로 처리할 수 있습니다.
    'ㅜ' 모양을 처리 할 때에는, '+' 모양을 먼저 만들어준 다음에 
    가운데 부분에서 가장 최소값을 빼어주는 형태로 구현해줍니다.
*/
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

const int dx[] = {1, -1, 0, 0}; // 동 서 남 북
const int dy[] = {0, 0, -1, 1}; 
int answer = 0;

// 'ㅜ'를 제외한 나머지 모양은 depth가 4인 dfs로 만들 수 있습니다.
void makeStandardShape(int &x, int &y, int depth, int sum, int &N, int &M, int TetroMino[][501], bool visited[][501]){
    if(depth == 4){
        answer = max(answer, sum);
        return;
    }
    
    for(int i = 0; i < 4; i++){
        int nx = x + dx[i];
        int ny = y + dy[i];

        if(nx < 0 || ny < 0 || nx >= N || ny >= M){
            // 맵의 탐색 범위를 벗어나면 제끼고
            continue; 
        }
        if(visited[nx][ny]){
            // 이미 방문한 좌표이면 제끼고
            continue;
        }

        visited[nx][ny] = true; // 방문 처리를 해준 후에,
        makeStandardShape(nx, ny, depth + 1, sum + TetroMino[nx][ny], N, M, TetroMino, visited);
        visited[nx][ny] = false;
    }
}

// 'ㅜ' 모양은 '+'모양에서, 동서남북 방향에서 하나씩 빼어주면 됩니다.
void makeWOOShape(int &x, int &y, int sum, int &N, int &M, int TetroMino[][501], bool visited[][501]){
    sum = TetroMino[x][y]; // 해당 좌표는 'ㅜ' 모양에서 가운데 부분의 좌표입니다.
    int sub = 987654321; // 가운데 부분에서 상, 하, 좌, 우로 뻗어나갈 좌표 중 가장 최솟값을 골라냅니다.
    // 가장 최솟값을 제거하면, 만들 수 있는 'ㅜ' 'ㅓ' 'ㅗ' 'ㅏ' 모양 중 가장 큰 값을 골라 낼 수 있습니다.

    /* '+' 모양 만들어주기 */ 
    for(int i = 0; i < 4; i++){
        int nx = x + dx[i];
        int ny = y + dy[i];
        if(nx < 1|| ny < 1 || nx > N || ny > M){
            // 한 칸 이동했을 때의 값이 맵의 탐색 범위를 넘어가면 제끼고
            continue;
        }
        sub = min(sub, TetroMino[nx][ny]); // 가장 작은 값을 골라냅니다.
        sum += TetroMino[nx][ny]; // 상하좌우에 있는 값을 모조리 더하면 '+' 모양이 완성됩니다.
    }
    /* 이제 상하좌우에 있는 값 중 작은 값을 빼어서 'ㅓ' 'ㅗ' 'ㅏ' 'ㅜ' 모양을 만듭니다. */
    for(int i = 0; i < 4; i++){
        int nx = x + dx[i];
        int ny = y + dy[i];
         if(nx < 1 || ny < 1 || nx > N|| ny > M){
            continue;
            answer = max(answer, sum - sub);
        }
    }
}

int main(){
    ios_base::sync_with_stdio(false); cin.tie(0);
    // 입력 받는다.
    int N, M;
    cin >> N >> M;
    int TetroMino[501][501];
    bool visited[501][501] = {false, }; 
    for(int i = 1; i <= N; i++){
        for(int j = 1; j <= M; j++){
            cin >> TetroMino[i][j];
        }
    }

    for(int i = 1; i <= N; i++){
        for(int j = 1; j <= M; j++){
            makeWOOShape(i, j, 0, N, M, TetroMino, visited);
            visited[i][j] = true;
            makeStandardShape(i, j, 0, 0, N, M, TetroMino, visited);
            visited[i][j] = false;
        }
    }

    cout << answer << "\n";
}

그런데 무슨 이유에선지(ㅠㅠ), 내가 실력이 부족한 탓에 자꾸 2번째, 3번째 인풋 케이스가 자꾸 다른 값이 나와서 틀려버렸다. (추후에 알게 되면 다시 수정하여 포스팅하겠다.)

그래서 하는 수 없이 'ㅜ' 모양에 대해서는 직접 구현하여 마무리했다.


⚒️ 뚝딱뚝딱 설계


'ㅜ'를 제외한 나머지 모양들에 대해서는, depth가 3인 dfs로 구현할 수 있다.
그 중에서 최댓값들을 업데이트해가면서, 맵을 채워가면 된다.
여기서 visited 배열로 방문한 좌표에 대해서는 또 다시 방문하지 않는다는 것을 구현할 수도 있겠으나,
함수를 설계할 때 dfs를 호출할 때마다 '직전에 저장된 값'들을 기억하면서 step-by-step으로 dfs를 구현하면 어차피 방문한 좌표에 대해서 방문하지 않는다.[❣️공간을 절약하는 방법!]




따라서 일단

(1) 'ㅜ' 를 제외한 나머지 모양을 채울 수 있는 dfs 함수를 설계해보자.


int makeStandardShape(int x, int y, int prev_x, int prev_y, int TetroMino[][], int depth) {
	// depth가 3이면
    // 최댓값을 업데이트하고 그 값을 리턴하며, dfs를 종료합니다.
    
    // 아니라면 4방향 벡터에 대하여 다음 방문할 좌표 후보군을 탐색합니다.
    // 다음 후보군 좌표가 맵의 탐색 범위 밖에 있으면 제끼고
    // 다음 후보군 좌표가 이미 방문한 좌표라면 제끼고
    // 아니면 현재 좌표에, depth를 증가한 dfs를 호출한 값을 더해가며 
    // 최댓값을 업데이트
}

그러면, 우선 'ㅜ' 모양을 제외한 블럭들에 대하여 위에 설계한 함수로 dfs를 돌려주고, 그 다음 'ㅜ' 모양에 대해 처리해주면 된다.



(2) 'ㅜ', 'ㅓ', 'ㅗ', 'ㅏ' 모양을 처리할 수 있는 함수를 설계해보자.


우선 시작 좌표를 어디로 잡을까? 부터 생각해줘야 한다. 시작 좌표는 대칭/회전에 대하여 어떤 일관된 기준과 성질을 지니고 있으면 된다.
따라서 나는 좌표를 기준으로 3방향을 탐색할 수 있는 경우의 수를 생각해봤다.

즉, 해당 블럭들이 맵의 범위를 넘어가지 않는 선에서 맵에 있는 값을 더해가며, 최댓값을 업데이트해주면 된다.
정답은 따라서 이중 for문 돌리면서 (1)에서 dfs 돌려 리턴된 값 OR (2) 'ㅜ' 모양 구현한 함수 에서 가장 큰 값 이 되겠다.

void makeWOOShape(int x, int y, int answer) {
	// 전제 : 맵의 범위를 넘어가지 않는 선에서
    
	// 'ㅜ' 모양 만들기
    // 'ㅏ' 모양 만들기
    // 'ㅓ' 모양 만들기
    // 'ㅗ' 모양 만들기
}

🖥️ 정답 코드


// 14500 테트로미노
/*
  'ㅗ' 모양을 제외한 나머지 모양은 depth가 3로 끝나는 dfs로 만들 수 있습니다.
*/
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;
const int MAX = 504;
const int dx[] = {1, -1, 0, 0};
const int dy[] = {0, 0, -1, 1};
int makeStandardShape(int x, int y, int prev_x, int prev_y, int depth, int TetroMino[MAX][MAX], int &N, int &M){
  int answer = 0;
  if(depth == 3){
    return TetroMino[x][y];
  }
  for(int i = 0; i < 4; i++){
      int nx = x + dx[i];
      int ny = y + dy[i];
      if(nx < 0 || ny < 0 || nx >= N || ny >= M) {
        continue;
      }
      if(prev_x == nx && prev_y == ny) {
        continue;
      }
      answer = max(answer, TetroMino[x][y] + makeStandardShape(nx, ny, x, y, depth+1, TetroMino, N, M));
    }
  return answer;
}

int makeWOOShape(int x, int y, int N, int M, int answer, int Tetromino[MAX][MAX]){
    if (x - 1 >= 0 && y - 1 >= 0 && y + 1 < M) {
         int tmp = Tetromino[x - 1][y] + Tetromino[x][y - 1] + Tetromino[x][y] + Tetromino[x][y + 1];
          answer = max(answer, tmp);
    }
    if (y - 1 >= 0 && x + 1 < N && y + 1 < M) {
         int tmp = Tetromino[x][y - 1] + Tetromino[x][y] + Tetromino[x][y + 1] + Tetromino[x + 1][y];
         answer = max(answer, tmp);
    }
    if (x - 1 >= 0 && y - 1 >= 0 && x + 1 < N) {
         int tmp = Tetromino[x - 1][y] + Tetromino[x][y - 1] + Tetromino[x][y] + Tetromino[x + 1][y];
          answer = max(answer, tmp);
    }
    if (x - 1 >= 0 && x + 1 < N && y + 1 < M) {
          int tmp = Tetromino[x - 1][y] + Tetromino[x][y] + Tetromino[x][y + 1] + Tetromino[x + 1][y];
          answer = max(answer, tmp);
    }
  return answer;
}

int main(){
  ios_base::sync_with_stdio(false); cin.tie(0);
  int N, M;
  int Tetromino[MAX][MAX];
  int answer = 0;
  cin >> N >> M;
  for(int i = 0; i < N; i++){
    for(int j = 0; j < M; j++){
      cin >> Tetromino[i][j];
    }
  }
  for(int i = 0; i < N; i++) {
  	for(int j = 0; j <= M; j++) {
      answer = max(answer, makeStandardShape(i, j, -1, -1, 0, Tetromino, N, M));
      answer = max(answer, makeWOOShape(i, j, N, M, 0, Tetromino));
    }
  }
  cout << answer << "\n";
}
profile
백문이불여일타

0개의 댓글

관련 채용 정보