| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |
- DFS
- spring security 설정
- 자바의정석
- 동적계획법
- Comparator
- 분할정복
- Ubuntu서버
- 오블완
- 이분탐색
- 티스토리챌린지
- 그리디
- 자바
- BFS
- 자바의 정석
- 프로그래머스
- spring security
- 혼공얄코
- 리눅스
- greedy
- 코딩테스트
- 서버초기설정
- DP
- 스프링부트 배포
- java
- 완전탐색
- 백트래킹
- 둘만의 암호 자바
- 프로그래머스Lv2
- hackerrank
- 알고리즘
- Today
- Total
쉽게 쉽게
[프로그래머스] 지게차와 크레인 (Java) — BFS 본문
- BFS의 출발점은 외부 — 격자 안의 임의 칸이 아니라 창고 바깥에서 빈칸('.')을 타고 퍼진다. 도달한 빈칸 영역에 인접한 target 컨테이너가 지게차로 꺼낼 수 있는 컨테이너다.
- 판정과 반영의 분리 — 접근 가능 판정은 요청 시점의 상태 기준이다. BFS 도중 제거하면 같은 요청에서 연쇄 제거가 일어나므로, removeList에 기록만 하고 BFS 종료 후 한꺼번에 반영한다.
- 패딩 vs 무패딩 — 패딩은 외부에 좌표를 부여해 시작점을 하나로 만들고 가장자리 특수 케이스를 없앤다. 무패딩은 좌표 보정이 없는 대신 멀티 소스 BFS와 테두리 target 즉시 제거 처리가 필요하다.
1 문제 이해
n × m 창고에 알파벳 대문자로 구분되는 컨테이너가 놓여 있다. 요청이 "A"처럼 한 글자면 지게차로 접근 가능한 해당 종류 컨테이너를 모두 꺼내고, "BB"처럼 두 글자면 크레인으로 해당 종류를 전부 꺼낸다. 접근 가능하다는 것은 4면 중 적어도 1면이 창고 외부와 연결되어 있다는 뜻이다. 모든 요청 수행 후 남은 컨테이너 수를 구한다.
- 크레인 요청은 격자 전체를 순회하며 해당 종류를 모두 '.'로 바꾼다.
- 지게차 요청은 외부에서 출발하는 BFS로 외부와 연결된 빈칸 영역을 확정한다.
- 그 영역에 인접한 target 컨테이너를 기록해두고, BFS 종료 후 한꺼번에 제거한다.
- 모든 요청 후 '.'이 아닌 칸을 세어 반환한다.
크레인은 단순 전체 순회로 끝난다. 이 문제의 본체는 지게차의 "접근 가능" 판정이고, 이는 "이 빈칸이 창고 외부와 연결되어 있는가"라는 연결성 문제이므로 BFS가 정석이다.
2 접근 — BFS의 방향
처음에는 격자의 (0, 0)에서 BFS를 시작하는 뼈대를 잡았다. 하지만 (0, 0)은 컨테이너일 수도 있는 임의의 칸이고, 알고 싶은 것은 그 칸이 아니라 "외부에서 어디까지 들어올 수 있는가"이다. 따라서 출발점은 격자 안의 어떤 칸이 아니라 창고 바깥이어야 한다.
또 하나의 핵심은 동시성이다.
지게차 요청 하나에서 접근 가능 판정은 요청 시점의 상태 기준이다. 바깥쪽 컨테이너를 꺼냈다고 해서 그로 인해 뚫린 안쪽 컨테이너가 같은 요청에서 연달아 나가면 안 된다.
예제 1의 세 번째 요청이 정확히 이 케이스로, 2행 2열의 A만 나가고 2행 3열의 A는 남아야 한다.
3 풀이 1 — 패딩 BFS
패딩 트릭
"외부"는 격자 밖이라 좌표가 없다. 그래서 격자를 상하좌우 한 겹씩 '.'로 감싸 (N+2) × (M+2)로 만들었다.
외부가 실제 좌표를 가진 빈칸이 되므로 패딩의 (0, 0)에서 BFS를 시작하면 테두리를 따라 전부 연결되어 자연스럽게 외부 영역이 완성된다. 가장자리 컨테이너도 패딩 빈칸과 인접하게 되므로 특별 취급 없이 같은 로직으로 처리된다. 대신 원본 좌표가 전부 +1씩 밀리므로 크레인 루프와 카운트 루프의 범위는 1 ~ N, 1 ~ M이다.
import java.util.*;
class Solution {
static int[] dx = {-1, 1, 0, 0};
static int[] dy = {0, 0, -1, 1};
int N, M;
public int solution(String[] storage, String[] requests) {
N = storage.length;
M = storage[0].length();
// 패딩: 상하좌우 한 겹을 '.'로 감싼 (N+2) x (M+2) 맵
char[][] map = new char[N + 2][M + 2];
for (char[] row : map) Arrays.fill(row, '.');
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
map[i + 1][j + 1] = storage[i].charAt(j);
}
}
for (String s : requests) {
char target = s.charAt(0);
if (s.length() == 2) {
// 크레인: 전부 제거
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= M; j++) {
if (map[i][j] == target) map[i][j] = '.';
}
}
} else {
bfs(map, target);
}
}
// 남은 컨테이너 개수
int remain = 0;
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= M; j++) {
if (map[i][j] != '.') remain++;
}
}
return remain;
}
private void bfs(char[][] map, char target) {
Queue<int[]> queue = new LinkedList<>();
boolean[][] visited = new boolean[map.length][map[0].length];
List<int[]> removeList = new ArrayList<>();
queue.add(new int[]{0, 0}); // 패딩의 (0,0) = 외부
visited[0][0] = true;
while (!queue.isEmpty()) {
int[] cur = queue.poll();
for (int i = 0; i < 4; i++) {
int nextX = cur[0] + dx[i];
int nextY = cur[1] + dy[i];
// 경계 검사와 방문 검사는 독립된 두 줄로
if (nextX < 0 || nextY < 0 || nextX >= map.length || nextY >= map[0].length) continue;
if (visited[nextX][nextY]) continue;
if (map[nextX][nextY] == '.') {
visited[nextX][nextY] = true;
queue.add(new int[]{nextX, nextY});
} else if (map[nextX][nextY] == target) {
visited[nextX][nextY] = true; // 중복 등록 방지
removeList.add(new int[]{nextX, nextY});
}
// 다른 알파벳 컨테이너: 벽 취급
}
}
// 동시 제거: BFS가 끝난 뒤 한꺼번에
for (int[] p : removeList) {
map[p[0]][p[1]] = '.';
}
}
}
4 풀이 2 — 무패딩 멀티 소스 BFS
패딩 없이 N × M 배열 그대로 푸는 방법도 정리했다.
패딩이 공짜로 해결해주던 두 가지를 직접 처리하면 된다.
첫째, BFS 시작점이 하나가 아니라 여러 개가 된다(멀티 소스 BFS). 외부와 직접 닿아 있는 것은 격자의 테두리 칸들이므로, 테두리를 순회하며 '.'인 칸을 전부 큐에 넣고 시작한다. 테두리의 빈칸들은 중간에 컨테이너로 끊겨 서로 떨어져 있을 수 있으므로 하나만 넣으면 안 된다.
둘째, 테두리에 있는 target 컨테이너는 BFS와 무관하게 즉시 제거 대상이다. 테두리 칸은 그 자체로 한 면이 외부와 닿아 있어 빈칸을 경유할 필요가 없다. 패딩 버전에서는 패딩 '.'가 이 판정을 대신해줬지만, 무패딩에서는 테두리 순회 때 직접 체크해야 한다.
import java.util.*;
class Solution {
static int[] dx = {-1, 1, 0, 0};
static int[] dy = {0, 0, -1, 1};
int N, M;
public int solution(String[] storage, String[] requests) {
N = storage.length;
M = storage[0].length();
char[][] map = new char[N][M];
for (int i = 0; i < N; i++) {
map[i] = storage[i].toCharArray();
}
for (String s : requests) {
char target = s.charAt(0);
if (s.length() == 2) {
// 크레인: 전부 제거
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (map[i][j] == target) map[i][j] = '.';
}
}
} else {
bfs(map, target);
}
}
int remain = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (map[i][j] != '.') remain++;
}
}
return remain;
}
private void bfs(char[][] map, char target) {
Queue<int[]> queue = new LinkedList<>();
boolean[][] visited = new boolean[N][M];
List<int[]> removeList = new ArrayList<>();
// 테두리 순회: '.'는 BFS 출발점, target은 즉시 제거 대상
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (i != 0 && i != N - 1 && j != 0 && j != M - 1) continue; // 테두리만
if (visited[i][j]) continue;
if (map[i][j] == '.') {
visited[i][j] = true;
queue.add(new int[]{i, j});
} else if (map[i][j] == target) {
visited[i][j] = true;
removeList.add(new int[]{i, j}); // 한 면이 외부와 직접 연결
}
}
}
// 이후는 패딩 버전과 동일
while (!queue.isEmpty()) {
int[] cur = queue.poll();
for (int d = 0; d < 4; d++) {
int nx = cur[0] + dx[d];
int ny = cur[1] + dy[d];
if (nx < 0 || ny < 0 || nx >= N || ny >= M) continue;
if (visited[nx][ny]) continue;
if (map[nx][ny] == '.') {
visited[nx][ny] = true;
queue.add(new int[]{nx, ny});
} else if (map[nx][ny] == target) {
visited[nx][ny] = true;
removeList.add(new int[]{nx, ny});
}
}
}
for (int[] p : removeList) {
map[p[0]][p[1]] = '.';
}
}
}
solution 쪽은 패딩 관련 코드가 빠지므로 오히려 단순해진다. storage를 그대로 맵에 넣고, 크레인/카운트 루프도 0 ~ N-1 범위로 돈다. 복잡함이 bfs 앞단의 테두리 처리로 옮겨간 셈이다.
5 두 풀이 비교 및 정리
| 구분 | 풀이 1: 패딩 | 풀이 2: 무패딩 |
|---|---|---|
| 맵 크기 | (N+2) × (M+2) | N × M |
| BFS 시작점 | 하나 — 패딩의 (0, 0) | 여러 개 — 테두리의 모든 '.' |
| 가장자리 target | 자동 처리 (패딩 '.'와 인접) | 직접 처리 — 테두리 순회 시 즉시 제거 대상 |
| 좌표 보정 | 필요 — 원본 대비 +1 | 불필요 |
| 특수 케이스 수 | 0개 — 모든 판정이 하나의 규칙 | 1개 — 테두리 target 즉시 제거 |
| 성능/정답률 | 차이 없음 — 취향의 문제 | |
로직 총량은 비슷하고, 복잡함이 어디에 놓이는지가 다르다.
패딩 버전은 좌표 +1 보정이라는 비용을 내는 대신 BFS가 단순해지고, 무패딩 버전은 좌표가 깔끔한 대신 BFS 앞단에 테두리 특수 처리가 붙는다.
나는 모든 판정이 "외부 영역 빈칸과 인접한가" 하나로 통일되는 패딩 쪽이 실수 여지가 적다고 판단했다.
| 잘못된 내용이 있다면 지적부탁드립니다. 방문해주셔서 감사합니다. |

'알고리즘 & 코딩테스트 > BFS • DFS' 카테고리의 다른 글
| [프로그래머스 Lv.2] 후보키(Java) - DFS와 비트마스크 두 가지 풀이 (1) | 2026.07.28 |
|---|---|
| [프로그래머스 Lv.2] 석유 시추(Java) - BFS (0) | 2026.07.28 |
| [프로그래머스] 비밀 코드 해독 (Java) — DFS (0) | 2026.07.24 |
| [프로그래머스] 광물 캐기(Java) - DFS (0) | 2026.07.23 |
| [프로그래머스] 거리두기 확인하기(Java) — BFS (0) | 2026.07.18 |
