https://school.programmers.co.kr/learn/courses/30/lessons/17679
프로그래머스
SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프
programmers.co.kr
문제
입력
입력으로 판의 높이 m, 폭 n과 판의 배치 정보 board가 들어온다.
board는 길이 n인 문자열 m개의 배열로 주어진다.
블록을 나타내는 문자는 대문자 A에서 Z가 사용된다.
출력
입력으로 주어진 판 정보를 가지고 몇 개의 블록이 지워질지 출력하라.
제한사항
2 ≦ n, m ≦ 30
풀이
1. 시뮬레이션 문제의 황금률: "상태 변화의 분리 (Phase Separation)"
- 보드의 상태가 변하는 문제(퍼즐, 테트리스, 애니팡 류)를 풀 때는 '탐색'과 '상태 변경'을 동시에 하면 절대 안 된다. (기존에 하다가 실패했다. 단일책임원칙... 지키자. 일단 다 분리하고 나중에 합치자.)
- 찾으면서 동시에 지우거나, 지우면서 동시에 내리면 기존 보드가 망가져서 다음 탐색에 악영향을 미친다.
- 해결책: 좌표 수집(Find) ➔ 한 번에 지우기(Erase) ➔ 한 번에 중력 적용(Gravity)처럼 각 단계를 완전히 독립적인 루프(또는 함수)로 쪼개야 한다.
2. 중력(Gravity) 알고리즘의 O(N) 최적화 테크닉
- 블록을 밑으로 떨어뜨리는 로직을 짤 때, 지워진 빈칸의 모양(행-2 등)에 의존하거나 버블 정렬처럼 한 칸씩 스왑(Swap)하며 내릴 필요가 없다는 점.
- 기존에는 그냥 행-2부터 첫번째 행까지 while문 돌리면 되는 거 아니야? - A()B()C는 어떻게 해결할 건데.
- 버블정렬로 빈칸을 가장 앞으로 하면 되는 거 아니야? - 시간복잡도 O(n^2)
- 해결책 (주워 담기 기법):
- 각 열(Column)마다 맨 아래부터 위로 훑으면서 살아남은 블록만 queue(또는 vector)에 순서대로 쏙쏙 주워 담는다.
- 해당 열을 전부 빈칸('0')으로 초기화한다.
- 다시 맨 아래부터 큐에 담아둔 블록을 차곡차곡 꺼내어 채워 넣는다.
3. 중복되는 영역의 카운트 처리 방법 (2x3, 3x3 등)
- 2x2 블록 여러 개가 겹쳐서 지워질 때 중복 카운트를 어떻게 방지할까.
- 해결책: 4칸이 묶여있다고 해서 4칸 전체를 한 번에 검사하려고 복잡하게 짜지 말 것.
- 각 칸을 지울 때마다 "이 칸이 이미 지워진 빈칸('0')이 아닐 때만 정답(answer)을 1 증가시키고 빈칸으로 바꾼다"라고 아주 단순하게 개별 처리하면 중복 문제가 완벽히 해결된다.
4. 면접관이 좋아하는 클린 코드 (리팩토링 요 요소)
- 매직 넘버 제거: 코드에 의미 없는 숫자나 문자(예: '0')를 하드코딩하지 않고, const char EMPTY = '0';처럼 상수로 빼내어 가독성과 유지보수성을 높였다.
- 방향 배열 최적화: 2x2를 탐색할 때, 기준점 (0,0)까지 포함하여 dr[4], dc[4]로 묶어버리면 불필요한 예외 처리(if문) 없이 for문 하나로 4칸을 깔끔하게 처리할 수 있다.
코드
리팩토링 전 코드
#include <string>
#include <vector>
#include <queue>
using namespace std;
// 그냥 구현 같은데요. 그냥 해보겠습니다.
/*
1. board의 행과 열을 각각 n-1, r-1까지 순회하면서
[행][열], [행][열+1], [행+1][열], [행+1][열+1]이 모두 같다면('0'이 아니라면) vector 배열에 {행, 열}을 넣어.
2. 해당 배열에 원소가 있다면
해당 배열을 순회하면서 해당 board[행][열], [행][열+1], [행+1][열], [행+1][열+1]을 모두 '0' 표시로 바꿔. (지우기)
3. 밀기
같은 열에서 가장 밑행부터 확인하면서 '0'이 없다면 해당 값을 저장하고 마지막에 가장 밑 행부터 차례대로 밀어 넣어.
4. 1~3번을 해당 배열이 비었을 때까지 실행해.
*/
int solution(int m, int n, vector<string> board) {
int answer = 0;
int dr[3] = {0, 1, 1};
int dc[3] = {1, 0, 1};
while (true) {
vector<pair<int, int>> arr;
// 2*2 찾아서 해당 배열 저장
for (int r = 0; r < m - 1; ++r) {
for (int c = 0; c < n - 1; ++c) {
if (board[r][c] == '0') continue;
bool is_erase = true;
for (int i = 0; i < 3; ++i) {
int nr = r + dr[i];
int nc = c + dc[i];
if (board[r][c] != board[nr][nc] || board[nr][nc] == '0') {
is_erase = false;
break;
}
}
if (is_erase) {
arr.push_back({r, c});
}
}
}
if (arr.empty()) break;
// 지우기
for (auto& pos : arr) {
int r = pos.first;
int c = pos.second;
if (board[r][c] != '0') {
board[r][c] = '0';
answer++;
}
for (int i = 0; i < 3; ++i) {
int nr = r + dr[i];
int nc = c + dc[i];
if (board[nr][nc] != '0') {
board[nr][nc] = '0';
answer++;
}
}
}
// 밀어 넣기
for (int c = 0; c < n; ++c) {
queue<char> q;
// '0'이 아닌 값을 저장
for (int r = m - 1; r >= 0; --r) {
if (board[r][c] != '0') {
q.push(board[r][c]);
}
board[r][c] = '0';
}
// 해당 열 갱신
for (int r = m - 1; r >= 0; --r) {
if (q.empty()) break;
board[r][c] = q.front();
q.pop();
}
}
}
return answer;
}
리팩토링 후 코드 (함수로 나누고 매직넘버, 변수 정리)
#include <string>
#include <vector>
#include <queue>
using namespace std;
// 그냥 구현 같은데요. 그냥 해보겠습니다.
/*
1. board의 행과 열을 각각 n-1, r-1까지 순회하면서
[행][열], [행][열+1], [행+1][열], [행+1][열+1]이 모두 같다면('0'이 아니라면) vector 배열에 {행, 열}을 넣어.
2. 해당 배열에 원소가 있다면
해당 배열을 순회하면서 해당 board[행][열], [행][열+1], [행+1][열], [행+1][열+1]을 모두 '0' 표시로 바꿔. (지우기)
3. 밀기
같은 열에서 가장 밑행부터 확인하면서 '0'이 없다면 해당 값을 저장하고 마지막에 가장 밑 행부터 차례대로 밀어 넣어.
4. 1~3번을 해당 배열이 비었을 때까지 실행해.
*/
const char EMPTY = '0';
using Position = pair<int, int>;
const int dr[4] = {0, 0, 1, 1};
const int dc[4] = {0, 1, 0, 1};
vector<Position> findMatchedBlocks(int m, int n, const vector<string>& board) {
vector<Position> matched;
for (int r = 0; r < m - 1; ++r) {
for (int c = 0; c < n - 1; ++c) {
char current = board[r][c];
if (current == EMPTY) continue;
bool is_match = true;
for (int i = 1; i < 4; ++i) {
int nr = r + dr[i];
int nc = c + dc[i];
if (board[nr][nc] != current) {
is_match = false;
break;
}
}
if (is_match) {
matched.push_back({r, c});
}
}
}
return matched;
}
int eraseBlocks(const vector<Position>& matched, vector<string>& board) {
int erased_count = 0;
for (const auto& pos : matched) {
int r = pos.first;
int c = pos.second;
for (int i = 0; i < 4; ++i) {
int nr = r + dr[i];
int nc = c + dc[i];
if (board[nr][nc] != EMPTY) {
board[nr][nc] = EMPTY;
erased_count++;
}
}
}
return erased_count;
}
void applyGravity(int m, int n, vector<string>& board) {
for (int c = 0; c < n; ++c) {
queue<char> surviving_blocks;
for (int r = m - 1; r >= 0; --r) {
if (board[r][c] != EMPTY) {
surviving_blocks.push(board[r][c]);
board[r][c] = EMPTY;
}
}
for (int r = m - 1; r >= 0 && !surviving_blocks.empty(); --r) {
board[r][c] = surviving_blocks.front();
surviving_blocks.pop();
}
}
}
int solution(int m, int n, vector<string> board) {
int total_erased = 0;
while (true) {
vector<Position> matched = findMatchedBlocks(m, n, board);
if (matched.empty()) {
break;
}
total_erased += eraseBlocks(matched, board);
applyGravity(m, n, board);
}
return total_erased;
}728x90
'Computer > 알고리즘' 카테고리의 다른 글
| [JS] 코딩테스트 치팅 시트 (0) | 2026.07.13 |
|---|---|
| [JS] 백준 16928: 뱀과 사다리 게임 (0) | 2026.04.08 |
| [JS] 백준 5430: AC (0) | 2026.04.06 |
| [JS] 프로그래머스 가장 큰 수 (1) | 2025.01.27 |
| [JS] 프로그래머스 K번째수 (2) | 2025.01.26 |