ballqs 님의 블로그

[Java] 프로그래머스 문제 - [PCCP 기출문제] 2번 / 석유 시추 본문

코딩 공부/Java

[Java] 프로그래머스 문제 - [PCCP 기출문제] 2번 / 석유 시추

ballqs 2025. 9. 3. 15:09

오늘 풀어본 문제는 좀 많이 어려웠다. 프로그래머스의 [PCCP 기출문제] 2번 / 석유 시추 라는 문제를 풀어보았다.


문제 설명

[본 문제는 정확성과 효율성 테스트 각각 점수가 있는 문제입니다.]

세로길이가 n 가로길이가 m인 격자 모양의 땅 속에서 석유가 발견되었습니다. 석유는 여러 덩어리로 나누어 묻혀있습니다. 당신이 시추관을 수직으로 단 하나만 뚫을 수 있을 때, 가장 많은 석유를 뽑을 수 있는 시추관의 위치를 찾으려고 합니다. 시추관은 열 하나를 관통하는 형태여야 하며, 열과 열 사이에 시추관을 뚫을 수 없습니다.

예를 들어 가로가 8, 세로가 5인 격자 모양의 땅 속에 위 그림처럼 석유가 발견되었다고 가정하겠습니다. 상, 하, 좌, 우로 연결된 석유는 하나의 덩어리이며, 석유 덩어리의 크기는 덩어리에 포함된 칸의 수입니다. 그림에서 석유 덩어리의 크기는 왼쪽부터 8, 7, 2입니다.

시추관은 위 그림처럼 설치한 위치 아래로 끝까지 뻗어나갑니다. 만약 시추관이 석유 덩어리의 일부를 지나면 해당 덩어리에 속한 모든 석유를 뽑을 수 있습니다. 시추관이 뽑을 수 있는 석유량은 시추관이 지나는 석유 덩어리들의 크기를 모두 합한 값입니다. 시추관을 설치한 위치에 따라 뽑을 수 있는 석유량은 다음과 같습니다.

시추관의 위치 획득한 덩어리 총 석유량
1 [8] 8
2 [8] 8
3 [8] 8
4 [7] 7
5 [7] 7
6 [7] 7
7 [7, 2] 9
8 [2] 2

오른쪽 그림처럼 7번 열에 시추관을 설치하면 크기가 7, 2인 덩어리의 석유를 얻어 뽑을 수 있는 석유량이 9로 가장 많습니다.

석유가 묻힌 땅과 석유 덩어리를 나타내는 2차원 정수 배열 land가 매개변수로 주어집니다. 이때 시추관 하나를 설치해 뽑을 수 있는 가장 많은 석유량을 return 하도록 solution 함수를 완성해 주세요.


제한 사항

  • 1 ≤ land의 길이 = 땅의 세로길이 = n ≤ 500
    • 1 ≤ land[i]의 길이 = 땅의 가로길이 = m ≤ 500
    • land[i][j]는 i+1행 j+1열 땅의 정보를 나타냅니다.
    • land[i][j]는 0 또는 1입니다.
    • land[i][j]가 0이면 빈 땅을, 1이면 석유가 있는 땅을 의미합니다.

정확성 테스트 케이스 제한사항

  • 1 ≤ land의 길이 = 땅의 세로길이 = n ≤ 100
    • 1 ≤ land[i]의 길이 = 땅의 가로길이 = m ≤ 100

효율성 테스트 케이스 제한사항

  • 주어진 조건 외 추가 제한사항 없습니다.

입출력 예

land result
[[0, 0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 1, 1, 0, 0], [1, 1, 0, 0, 0, 1, 1, 0], [1, 1, 1, 0, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 1, 1]] 9
[[1, 0, 1, 0, 1, 1], [1, 0, 1, 0, 0, 0], [1, 0, 1, 0, 0, 1], [1, 0, 0, 1, 0, 0], [1, 0, 0, 1, 0, 1], [1, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1]] 16

 


입출력 예 설명

입출력 예 #1

문제의 예시와 같습니다.

입출력 예 #2

시추관을 설치한 위치에 따라 뽑을 수 있는 석유는 다음과 같습니다.

시추관의 위치 획득한 덩어리 총 석유량
1 [12] 12
2 [12] 12
3 [3, 12] 15
4 [2, 12] 14
5 [2, 12] 14
6 [2, 1, 1, 12] 16

6번 열에 시추관을 설치하면 크기가 2, 1, 1, 12인 덩어리의 석유를 얻어 뽑을 수 있는 석유량이 16으로 가장 많습니다. 따라서 16을 return 해야 합니다.


풀이 과정(오답)

이 문제는 아래와 같이 생각하여 진행하고자 했다.

  1. 열 기준으로 반복문을 돌리고 거기에서 행 기준으로 반복문을 돌리기 위한 설정
    ※ 열과 행 크기 설정
  2. BFS 알고리즘을 사용하여 석유 크기 계산
    ※ 방문 여부를 관리하는 변수를 통해 크기 계산에 이상 없이 하기 위함
    1. 첫번째 위치를 큐에 담기
    2. 반복문을 통해 큐에서 하나씩 꺼내기
    3. 4방면을 돌려가면서 석유가 있는지 그리고 방문하지 않은 위치인지 확인
      ※ 이상 없으면 큐에 담는 작업 및 방문 여부 설정 그리고 석유 크기 증가
  3. 계산된 크기를 합하여 결과 값과 비교 후 설정

이러한 생각으로 진행하여 코드를 작성하고자 했다.


코드(오답)

import java.util.*;
class Solution {
    // 방향 설정 : 동 , 서 , 남 , 북
    int[] x = {1 , -1 , 0 ,   0};
    int[] y = {0 ,  0 , 1 ,  -1};
    
    int rows , cols;
    
    public int solution(int[][] land) {
        int answer = 0;
        
        // 행 , 열 크기 설정
        rows = land.length;
        cols = land[0].length;
        
        for (int col = 0; col < cols; col++)
        {
            // 열 기준으로 방문 여부를 관리
            boolean[][] visit = new boolean[rows][cols];
            int hap = 0;
            
            for (int row = 0; row < rows; row++)
            {
                // 해당 지점(행 , 열)
                int target = land[row][col];
                
                // 해당 지점에 1이 담겨 있으면 석유로 판단! 그리고 방문되어 있지 않으면 통과
                if (target == 1 && !visit[row][col])
                {
                    hap += bfs(row , col , land , visit);
                }
            }
            
            // 합의 크기가 결과값보다 크면 설정
            if (hap > answer) answer = hap;
        }
        
        return answer;
    }
    
    // BFS 알고리즘
    public int bfs(int startRow , int startCol , int[][] land , boolean[][] visit)
    {
        // 들어온 시점에서 크기 1
        int result = 1;
        
        Queue<int[]> queue = new LinkedList<>();
        queue.add(new int[]{startRow , startCol});
        
        // 방문 여부 설정
        visit[startRow][startCol] = true;
        
        while (!queue.isEmpty())
        {
            // 큐에서 꺼내기
            int[] xy = queue.poll();
            
            // 4방향으로 반복문 돌기
            for (int i = 0; i < 4; i++)
            {
                int nextRow = xy[0] + x[i];
                int nextCol = xy[1] + y[i];
                
                if (nextRow > -1 && nextRow < rows &&
                   nextCol > -1 && nextCol < cols &&
                   !visit[nextRow][nextCol] && land[nextRow][nextCol] == 1)
                {
                    // 방문여부를 설정
                    visit[nextRow][nextCol] = true;
                    queue.add(new int[]{nextRow , nextCol});
                    result++;
                }
            }
        }
        
        return result;
    }
}

정확성은 전부 정답인데 효율성에서 다 틀려먹었다….

뭐가 문제인지 계속 생각한 끝에 아래와 같이 정리가 되었다.

  • 열마다 visit[][]을 초기화하고 BFS로 덩어리 크기 계산
  • 열마다 합산할 때 이미 계산된 덩어리를 재계산 → 시간 초과 발생
  • 문제점: 같은 덩어리를 여러 열에서 반복 계산, 효율성 떨어짐

그래서 다시 풀어보았다.


풀이 과정(정답)

재계산되지 않도록 해당 지점(행 , 열)에 크기를 알 수 있도록 하기 위해 변수를 만들어 관리하기로 했다.

  • sizes[][] 배열: 각 좌표에 해당 덩어리의 ID를 담아, 이미 계산된 덩어리를 다시 계산하지 않도록 관리
  • petroleumsizes (HashMap): 덩어리 ID별 크기를 저장하여, 같은 크기라도 다른 덩어리를 구분
  • 열별 석유 크기 합 계산 시, 중복 제거를 위해 HashSet 사용

위의 방법을 추가하여 아래와 같은 순서로 진행하고자 했다.

  1. 열 기준 반복문 수행
    • 각 열마다 행 기준으로 반복하며 덩어리를 탐색
    • 배열 크기를 기준으로 반복문 설정 (rows, cols)
  2. 덩어리 탐색 및 ID 부여
    • BFS를 통해 덩어리를 탐색
    • 각 덩어리에 고유 ID를 부여하고, sizes[][]에 기록
    • BFS 수행 후, 덩어리의 크기를 petroleumsizes에 저장
  3. 열별 덩어리 크기 합 계산
    • 각 열에서 등장하는 덩어리 ID를 HashSet으로 관리
    • 중복되지 않은 덩어리만 합산하여 열별 합 계산
    • 계산된 값 중 최대값을 결과로 설정
  4. 최종 결과 반환
    • 모든 열의 합산 값을 확인 후 최대값을 반환

코드(정답)

import java.util.*;
class Solution {
    // 방향 설정 : 동 , 서 , 남 , 북
    int[] x = {1 , -1 , 0 ,   0};
    int[] y = {0 ,  0 , 1 ,  -1};
    
    int rows , cols;
    
    public int solution(int[][] land) {
        int answer = 0;
        
        // 행 , 열 크기 설정
        rows = land.length;
        cols = land[0].length;
        
        // 덩어리별 ID를 부여하여 관리할 변수
        // ※ ID로 관리해야하는 이유! 열마다 덩어리 크기가 같을 수 있기에 고유 ID를 부여해야 함
        HashMap<Integer , Integer> petroleumsizes = new HashMap<>();
        int id = 1;
        
        // 덩어리의 ID를 담을 변수
        int[][] sizes = new int[rows][cols];
        
        // 열 기준으로 반복문
        for (int col = 0; col < cols; col++)
        {
            // 행 기준으로 반복문
            for (int row = 0; row < rows; row++)
            {
                // 해당 지점(행 , 열)
                int target = land[row][col];
                
                // 해당 지점에 1이 담겨 있으면 석유로 판단! 그리고 덩어리 ID 담을 변수가 초기값(0)일 경우
                if (target == 1 && sizes[row][col] == 0)
                {
                    petroleumsizes.put(id , bfs(row , col , land , sizes , id));
                    id++;
                }
            }
        }
        
        // 각 열마다 석유의 합계
        for (int col = 0; col < cols; col++) 
        {
            // 중복 제거를 위한 set 변수
            HashSet<Integer> petroleumids = new HashSet<>();
            int sum = 0;
            
            // 행 반복문
            for (int row = 0; row < rows; row++) 
            {
                // 해당 지점에 ID 고유값
                int getid = sizes[row][col];
                
                // ID가 0이 아닐 경우 덩어리가 있음! 그리고 set 변수에 담겨있지 않아야 함
                if (getid > 0 && !petroleumids.contains(getid))
                {
                    sum += petroleumsizes.get(getid);
                    petroleumids.add(getid);
                }
            }
            answer = Math.max(answer , sum);
        }
        
        return answer;
    }
    
    // BFS 알고리즘
    public int bfs(int startRow , int startCol , int[][] land , int[][] sizes , int id)
    {
        // 들어온 시점에서 크기 1
        int size = 1;
        
        // 석유의 행과 열을 담기 위한 변수
        List<int[]> cells = new ArrayList<>();
        
        Queue<int[]> queue = new LinkedList<>();
        queue.add(new int[]{startRow , startCol});
        
        // 시작 지점에 id 부여
        sizes[startRow][startCol] = id;
        
        while (!queue.isEmpty())
        {
            // 큐에서 꺼내기
            int[] xy = queue.poll();
            
            // 관련 있는 위치 담기
            cells.add(xy);
            
            // 4방향으로 반복문 돌기
            for (int i = 0; i < 4; i++)
            {
                int nextRow = xy[0] + x[i];
                int nextCol = xy[1] + y[i];
                
                if (nextRow > -1 && nextRow < rows &&
                   nextCol > -1 && nextCol < cols &&
                   sizes[nextRow][nextCol] == 0 && land[nextRow][nextCol] == 1)
                {
                    // id 설정
                    sizes[nextRow][nextCol] = id;
                    queue.add(new int[]{nextRow , nextCol});
                    size++;
                }
            }
        }
        
        // 관련 위치에 덩어리 ID 설정
        for (int[] cell : cells)
        {
            sizes[cell[0]][cell[1]] = id;
        }
        
        return size;
    }
}

효율성까지 검사하는 문제는 처음이였고 많은 공부가 된 문제였다.

접근 방법을 바꾸고 또 바꾸고 그렇게 풀어가면서 정답에 이르렀다. 시간은 많이 걸렸지만 ㅜ