비밀 코드 해독 (Java)

박세건·2025년 2월 23일
0

알고리즘 문제 해결

목록 보기
37/50
post-thumbnail

📌 문제 설명

  • 2차원 문자열 배열인 storage와 문자열 배열 requests가 주어진다.
  • requests의 첫 글자는 제거해야 할 알파벳이며, 해당 알파벳이:
    • 단일 문자이면 외부와 연결된 해당 알파벳만 제거한다.
    • 문자열이면 해당 알파벳 전체를 제거한다.
  • 외부와 연결되었다는 것은 해당 칸이 가장자리와 연결된 공간과 접촉했음을 의미한다.
  • 모든 requests를 처리한 후 남은 문자 개수를 반환한다.

문제 링크 🔗


🔍 접근 방식

첫 번째 시도 (실패 - 시간 초과 및 메모리 초과)

  • 매 요청마다 BFS로 외부와 연결 여부를 확인했다.
  • 최대 2,500 x 100 연산이라 터지지 않을 거라 예상했지만,
    반복적인 BFS로 인해 시간 초과 및 메모리 초과 발생.

두 번째 시도 (실패 - 잘못된 로직으로 인한 오류 발생)

  • 탐색 중에 4방향 검사로 외부와 연결된지 확인하려 했으나,
    내부 컨테이너가 먼저 사라지고 주변 컨테이너가 나중에 사라지는 반례가 발생.

💡 반례:

  • 내부의 알파벳이 외부와 연결된 것처럼 판단되는 문제 발생

세 번째 시도 (성공 - 최적화 및 정확한 로직)

핵심 아이디어:

  • 외부와 연결 여부를 기록하는 배열(connOut)을 사용하여 중복 검사를 피함.
  • 특정 알파벳을 제거할 때:
    • 해당 알파벳이 외부와 연결된 경우:
      • 4방 탐색을 통해 연결된 삭제된 영역재귀적으로 외부와 연결됨으로 표시
    • 해당 알파벳이 외부와 연결되지 않은 경우:
      • 현재 좌표만 삭제하고 종료

이 방법으로 내부 영역이 외부와 연결된 것처럼 판단되는 반례를 해결함. ✅


💡 구현 방법

1. 전역 변수 초기화

  • N, M: 행과 열의 크기 저장
  • map: storage를 2차원 문자 배열로 변환
  • deleted: 해당 칸이 제거되었는지 여부를 저장
  • connOut: 해당 칸이 외부와 연결되었는지 여부를 저장
  • dir: 4방향 탐색을 위한 배열

2. 초기 설정

  • 가장자리(connOut)를 외부와 연결됨(true)으로 설정
  • map을 순회하며 각 알파벳의 좌표를 pointMap에 저장

3. 요청 처리

  • requests를 순회하며:
    • 단일 문자 요청 시:
      • 해당 알파벳의 좌표 중 외부와 연결된 좌표만 제거
    • 문자열 요청 시:
      • 해당 알파벳 전체를 제거

4. 삭제 메서드(delete)

  • 외부와 연결된 좌표라면:
    • 4방 탐색으로 주변의 삭제된 영역 중 외부와 연결되지 않은 곳재귀적으로 외부와 연결시킴
  • 좌표를 deleted 배열에 true로 표시하여 제거 처리

5. 최종 결과 반환

  • 최종적으로 deletedfalse인 칸의 개수를 세어 반환

🧩 코드 구현

import java.util.*;

class Solution {
    int N, M;                              // 행과 열의 크기
    char[][] map;                          // 2차원 문자 배열로 저장
    Map<Character, Set<String>> pointMap = new HashMap<>();
    boolean[][] deleted;                   // 해당 좌표가 제거되었는지 여부
    boolean[][] connOut;                   // 외부와 연결 여부를 저장
    int[][] dir = {{-1, 0, 1, 0}, {0, 1, 0, -1}}; // 상, 우, 하, 좌 탐색

    public int solution(String[] storage, String[] requests) {
        // ✅ 전역 변수 초기화
        N = storage.length;
        M = storage[0].length();
        deleted = new boolean[N][M];
        connOut = new boolean[N][M];
        map = new char[N][M];

        // ✅ 외부와 연결된 가장자리 초기화
        for (int i = 0; i < N; i++) {
            connOut[i][0] = true;
            connOut[i][M - 1] = true;
        }
        for (int i = 0; i < M; i++) {
            connOut[0][i] = true;
            connOut[N - 1][i] = true;
        }

        // ✅ Map에 알파벳 좌표 저장
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                map[i][j] = storage[i].charAt(j);
                pointMap.computeIfAbsent(map[i][j], k -> new HashSet<>()).add(i + "," + j);
            }
        }

        // ✅ requests 탐색
        for (String request : requests) {
            char alpha = request.charAt(0);
            if (!pointMap.containsKey(alpha)) continue; // 알파벳이 없으면 건너뜀
            Set<String> set = pointMap.get(alpha);

            if (request.length() == 1) { // ✅ 단일 문자 요청 (외부와 연결된 것만 제거)
                List<int[]> deletedList = new ArrayList<>();
                for (String str : set) {
                    String[] splits = str.split(",");
                    int x = Integer.parseInt(splits[0]);
                    int y = Integer.parseInt(splits[1]);
                    if (connOut[x][y]) deletedList.add(new int[]{x, y});
                }
                for (int[] cur : deletedList) {
                    delete(cur[0], cur[1]); // ✅ 삭제 및 외부 연결 처리
                    set.remove(cur[0] + "," + cur[1]);
                }
            } else { // ✅ 문자열 요청 (해당 알파벳 모두 제거)
                for (String str : set) {
                    String[] splits = str.split(",");
                    int x = Integer.parseInt(splits[0]);
                    int y = Integer.parseInt(splits[1]);
                    delete(x, y); // ✅ 해당 좌표 제거
                }
                set.clear(); // ✅ 전체 제거했으므로 Set 비움
            }
        }

        // ✅ 남은 문자 개수 세기
        int answer = 0;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                if (!deleted[i][j]) answer++;
            }
        }
        return answer;
    }

    // ✅ 삭제 메서드
    private void delete(int x, int y) {
        if (connOut[x][y]) { // 외부와 연결된 경우
            for (int i = 0; i < 4; i++) {
                int dx = x + dir[0][i];
                int dy = y + dir[1][i];
                if (dx < 0 || dx >= N || dy < 0 || dy >= M) continue;
                if (deleted[dx][dy] && !connOut[dx][dy]) { // ✅ 삭제되었지만 외부와 연결되지 않은 경우
                    connOut[dx][dy] = true; // ✅ 외부와 연결 표시
                    delete(dx, dy);        // ✅ 재귀적으로 외부와 연결됨 처리
                } else {
                    connOut[dx][dy] = true; // ✅ 바로 외부와 연결됨 표시
                }
            }
        }
        deleted[x][y] = true; // ✅ 현재 좌표 제거 처리
    }
}

알고리즘 설명

  1. 외부와 연결 여부(connOut)

    • 배열의 가장자리는 기본적으로 외부와 연결됨으로 설정.
    • 특정 좌표가 외부와 연결된 경우에는 4방 탐색으로 연쇄적으로 외부와 연결됨으로 표시.
  2. 알파벳 요청 처리

    • 단일 문자일 경우 외부와 연결된 좌표만 제거.
    • 문자열일 경우 해당 알파벳 전체 제거.
  3. 삭제 메서드(delete)

    • 외부와 연결된 좌표인 경우:
      • 4방 탐색을 통해 연결된 삭제된 좌표재귀적으로 외부와 연결됨으로 표시
    • 현재 좌표deleted로 표시하여 제거 처리.

📊 성능 분석

  • 시간복잡도:

    • requests가 최대 2,500개이고
    • delete()각 좌표당 최대 한 번만 호출되므로
    • 시간복잡도는 O(N*M)으로 충분히 빠르다.
  • 메모리 사용:

    • deletedconnOut2차원 배열고정 크기 사용
    • 메모리 사용량이 안정적이며 추가적인 메모리 낭비 없음

🧪 테스트 케이스

기본 테스트

String[] storage = {"AAAB", "ABBB", "ABAB", "BBBB"};
String[] requests = {"A", "B"};
int result = solution(storage, requests);
System.out.println(result); // 0 (모두 제거됨)

외부와 연결되지 않은 경우

String[] storage = {"AAA", "ABA", "AAA"};
String[] requests = {"A"};
int result = solution(storage, requests);
System.out.println(result); // 중앙 'A'는 남아있음

연쇄적으로 외부와 연결되는 경우

String[] storage = {"AAA", "ABB", "ABB"};
String[] requests = {"A"};
int result = solution(storage, requests);
System.out.println(result); // 외곽 'A'가 제거되며 내부도 외부와 연결되어 제거됨

최대 입력 테스트 (성능 확인)

  • N = 50, M = 50, requests = 2,500
  • 시간 내에 통과 확인 완료

💡 문제 해결 과정 요약

  1. 첫 시도는 BFS 사용으로 시간초과메모리 초과 발생
  2. 두 번째 시도는 4방 탐색으로 외부 연결 여부 확인 → 반례 발생
  3. 세 번째 시도에서는:
    • 외부와 연결 여부(connOut)를 기록하여 중복 검사 제거
    • 연쇄적으로 외부와 연결되는 경우를 재귀적으로 처리하여 문제 해결 ✅

➡ 최종적으로 효율적이고 정확하게 문제 해결! ✅🚀


🚀 최종 결론

  1. 외부와 연결 여부(connOut)를 사용하여 중복 검사 제거
  2. 재귀적으로 연쇄적으로 외부와 연결되는 경우까지 정확히 처리
  3. 시간복잡도: O(N*M)빠르고 안정적
  4. 모든 테스트 케이스 통과 및 성능 최적화 완료! ✅🚀
profile
멋있는 사람 - 일단 하자

0개의 댓글