6 분 소요



https://www.acmicpc.net/problem/14500



🔍 문제 풀이

문제 도식화

  1. DFS로 연속된 4칸을 탐색하며 최대값을 찾는다.
  2. DFS로 만들 수 없는 ㅗ, ㅏ, ㅜ, ㅓ 모양은 별도로 시뮬레이션 처리를 하여 최대값을 찾는다.
    • ‘ㅗ’ 모양은 중심점에서 세 방향으로 뻗는 구조
    • DFS로는 깊이 4까지 가는 도중 가지 하나가 끊기게 되므로 별도 처리 필요

14500 도식화


알고리즘 선택

이 문제에서는 모든 칸에서 시작해, 상하좌우로 4칸을 탐색해야 하므로 DFS(깊이 우선 탐색) + 백트래킹이 가장 적합하다.

  • DFS (깊이 우선 탐색)

    • 한 방향으로 끝까지 들어간 뒤, 막히면 뒤로 돌아오는 방식 (Backtracking)
    • 재귀 기반 -> 백트래킹 가능
  • BFS (너비 우선 탐색)

    • 현재에서 가까운 모든 위치를 먼저 탐색
    • 큐 기반 -> 레벨 단위 탐색 -> 중간 분기 어려움


아래 그림은 DFS와 BFS가 탐색하는 순서를 비교한 것이다.

테트로미노

위 그림을 통해 BFS는 가능하긴 하나, 큐 상태에서 깊이 추적이 어렵다는 것을 확인할 수 있다.

따라서 가까운 칸부터 넓게 탐색하는 BFS보다, 한 방향으로 깊게 탐색하는 DFS가 더 적합하다.


단, ‘ㅗ’, ‘ㅜ’, ‘ㅏ’, ‘ㅓ’ 모양 도형은 DFS로 만들 수 없다.

  • DFS는 한 방향으로만 계속 탐색하기 때문에, 중간에서 양쪽으로 분기하는(3방향으로 퍼지는) 구조를 만들 수 없다.
  • 그럼 어떻게 처리해야할까? 알아보자.


‘ㅗ’모양은 어떻게 처리하지?

‘ㅗ’, ‘ㅏ’, ‘ㅜ’, ‘ㅓ’ 모양 도형은 상하좌우 탐색으로는 불가능하다.

따라서 checkTShape(x, y) 함수를 통해 현재 좌표 (x, y)를 중심으로 3방향을 더해 ㅗ 모양 도형을 만들 수 있도록 해줬다.

방향 좌표 변화
왼쪽 x, y - 1
오른쪽 x, y + 1
위쪽 x - 1, y
아래쪽 x + 1, y
int[][] Tshapes = {
    {0, -1, -1, 0, 0, 1}, // ㅗ
    {-1, 0, 0, 1, 1, 0}, // ㅏ
    {0, -1, -1, 0, 1, 0}, // ㅓ
    {0, -1, 0, 1, -1, 0} // ㅜ
};

테트로미노2

  • 이처럼, 중심 좌표 (x, y)를 기준으로 양 방향으로 뻗어나가도록 해야 한다.
  • 즉, 중심 좌표를 기준으로 3개의 방향으로 확장하여 총 4칸을 구성하는 방식이다.


예를 들어 ㅗ 모양의 경우:

  • 중심: (x, y)
  • 확장: 왼쪽 (x, y - 1), 오른쪽 (x, y + 1), 아래 (x + 1, y)

이러한 방식으로 ㅏ, ㅓ, ㅜ, ㅗ 총 4가지 모양을 구현할 수 있다.

이처럼 모든 칸에서 모든 방향의 도형을 직접 시도하여 누적 최대값을 찾는다.


‘ㅗ’모양을처리하는 두 가지 방법

배열 반복 방식

  • dx1 dy1, dx2 dy2, dx3 dy3 형태로 3개의 날개 좌표를 한 줄의 배열로 나열해서 처리한다.
  • 중심 좌표 (x, y)를 기준으로 3개의 방향만 더해주기 때문에 간결하다.
  • 도형 한 묶음을 검사하다가 실패하면 그 도형만 건너뛰어야 하므로 continue 사용(도형별 검사)
public static void Tshape(int x, int y) {
    // 중심(x, y) 기준 상대 좌표: 3개 날개
    int[][] cases = {
        {  0, -1,  -1,  0,   0,  1 },  // ㅗ
        { -1,  0,   0,  1,   1,  0 },  // ㅏ
        {  0, -1,   1,  0,   0,  1 },  // ㅜ
        { -1,  0,   0, -1,   1,  0 }   // ㅓ
    };

    for (int[] c : cases) {
        int nx1 = x + c[0], ny1 = y + c[1];
        int nx2 = x + c[2], ny2 = y + c[3];
        int nx3 = x + c[4], ny3 = y + c[5];

        if (nx1 < 0 || nx1 >= n || ny1 < 0 || ny1 >= m) continue;
        if (nx2 < 0 || nx2 >= n || ny2 < 0 || ny2 >= m) continue;
        if (nx3 < 0 || nx3 >= n || ny3 < 0 || ny3 >= m) continue;

        int sum = paper[x][y]
                + paper[nx1][ny1]
                + paper[nx2][ny2]
                + paper[nx3][ny3];

        maxSum = Math.max(maxSum, sum);
    }
}


좌표 분리 방식

  • 4개의 좌표 쌍을 각각 { {0,0}, {0,-1}, {0,1}, {-1,0} }처럼 배열로 분리하여 표현한다.
  • 반복문에서 바로 nx, ny를 계산하여 합산할 수 있어 편리하다.
  • 좌표 하나씩 검사하다가 실패하면 그 도형 전체 중단해야 하므로 break + isValid 사용 (좌표별 검사)
public static void Tshape(int x, int y) {
    int[][][] shapes = {
        { {0, 0}, {0, -1}, {-1, 0}, {0, 1} },  // ㅗ
        { {0, 0}, {-1, 0}, {0, 1}, {1, 0} },   // ㅏ
        { {0, 0}, {0, -1}, {1, 0}, {0, 1} },   // ㅜ
        { {0, 0}, {-1, 0}, {0, -1}, {1, 0} }   // ㅓ
    };

    for (int[][] shape : shapes) { // 하나의 도형
        int sum = 0;
        boolean isValid = true;

        for (int[] d : shape) { // 각 도형의 날개 묶음
            int nx = x + d[0];
            int ny = y + d[1];

            if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
                isValid = false;
                break;
            }
            sum += paper[nx][ny];
        }

        if (isValid) maxSum = Math.max(maxSum, sum);
    }
}


💥 메모리를 줄여보자

테트로미노 문제를 풀었을 때, 정답은 맞췄지만 메모리 사용량이 무려 283080KB가 나와서 깜짝 놀랐다.

다른 사람들은 보통 3~8만 KB 정도였는데, 나는 왜 이렇게 많이 나왔을까?


원인 분석

처음에는 dfs() 함수 안에 다음과 같은 방향 벡터를 매번 선언했다.

public void dfs(...) {
    int[] dx = {-1, 1, 0, 0};
    int[] dy = {0, 0, -1, 1};
}
  • 이 코드는 DFS가 호출될 때마다 새로운 int[] 배열을 생성한다.
  • 테트로미노 문제에서는 DFS가 수만 번 이상 호출되므로 결과적으로 int[] 객체 수천~수만 개가 힙에 쌓이게 된다.
  • 이게 JVM 메모리 폭증의 주범이었다.


해결

문제의 방향 벡터를 함수 내부에서 선언하지 않고,
전역으로 빼주기만 해도 메모리 사용량이 눈에 띄게 줄어든다.

이렇게 고정된 배열은 한 번만 생성되고 모든 DFS 호출에서 재사용되기 때문에
힙 메모리 누적을 방지할 수 있다.

static int[] dx = {-1, 1, 0, 0};
static int[] dy = {0, 0, -1, 1};

확실히 개선된 것을 알 수 있다.



💻 전체 코드

import java.io.*;
import java.util.*;

public class Main {
    static int[][] paper;
    static boolean[][] visited;

    static int n, m;
    static int maxSum = 0;

    static int[] dx = {-1, 1, 0, 0};
    static int[] dy = {0, 0, -1, 1};

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        // 입력
        StringTokenizer st = new StringTokenizer(br.readLine());
        n = Integer.parseInt(st.nextToken()); // 행
        m = Integer.parseInt(st.nextToken()); // 열

        paper = new int[n][m];
        visited = new boolean[n][m];

        // 초기화
        for(int i = 0; i < n; i++){
            st = new StringTokenizer(br.readLine());
            for(int j = 0; j < m ; j++){
                paper[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        // buildBlock, Tshape 호출
        for(int i = 0; i < n; i++){
            for(int j = 0; j< m ; j++){
                visited[i][j] = true;

                // i,j 현재 블록의 좌표 (시작점이자 첫 번째 칸)
                // depth=1부터 시작(시작 좌표 포함),
                buildBlock(i, j, 1, paper[i][j]); // 시작 좌표에서 DFS 시작
                visited[i][j] = false; // 시작점 탐색 후 백트래킹

                Tshape(i, j);
            }
        }

        System.out.println(maxSum);
    }

    // 연속된 4칸 블록 놓기 (ㅗ 모양 제외)
    public static void buildBlock(int x, int y, int depth, int total) {
        if (depth == 4) {
            maxSum = Math.max(maxSum, total);
            return;
        }

        // 상하좌우로만 이동 가능
        for (int i = 0; i < 4; i++) {
            int nx = x + dx[i];
            int ny = y + dy[i];

            if(nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
            if (!visited[nx][ny]) {
                visited[nx][ny] = true;
                buildBlock(nx, ny, depth + 1, total + paper[nx][ny]);
                visited[nx][ny] = false; // 새로운 경로 탐색 후 백트래킹
            }
        }
    }

    // ㅗ 모양 탐색
    public static void Tshape(int x, int y){
        // 방향백터 정의
        int[][] cases = {
                // dx1 dy1   dx2 dy2   dx3 dy3  (중심(x,y) 기준 3칸의 상대 좌표)
                {  0, -1,   -1,  0,    0,  1 },   // ㅗ
                { -1,  0,    0,  1,    1,  0 },   // ㅏ
                {  0, -1,    1,  0,    0,  1 },   // ㅜ
                { -1,  0,    0, -1,    1,  0 }    // ㅓ
        };

        int sum = 0;
        for(int[] c : cases){
            // 한 모양에 대한 양쪽 3개 날개
            int nx1 = x + c[0], ny1 = y + c[1];
            int nx2 = x + c[2], ny2 = y + c[3];
            int nx3 = x + c[4], ny3 = y + c[5];

            if(nx1 < 0 || nx1 >= n || ny1 < 0 || ny1 >= m) continue;
            if(nx2 < 0 || nx2 >= n || ny2 < 0 || ny2 >= m) continue;
            if(nx3 < 0 || nx3 >= n || ny3 < 0 || ny3 >= m) continue;

            sum = paper[x][y] + paper[nx1][ny1] + paper[nx2][ny2] + paper[nx3][ny3];
            maxSum = Math.max(maxSum, sum);
        }
    }
}


이로써 class 3 문제 해결 완료! 🤗


카테고리:

업데이트:

댓글남기기