중요하거나 어려웠던 문제에 대해서 작성합니다.
삼성 SW 역량 테스트 기출 문제
구현 / 그래프 이론 / 그래프 탐색 / 시뮬레이션 / BFS
구현 및 시뮬레이션류가 멀리 보면 재밌는데 실제 만난다 하면 아직까진 아찔하긴 하다. 연습 많이 해야겠다는 생각이 든다. 이미 했던 문제들도 지금 풀면 어려울 수 있으니 꺼진 불도 다시 보도록 하자. 아래처럼 좀 많이 틀리기는 했다...
단순히 보면 재밌는 문제인데, 구현이 많이 힘들었어서 선정하게 되었다. 공이 1개면 실버 상위 골드 하위였겠지만, 공이 두 개가 되니 난이도가 확 높아졌다. 문제는 다음 링크를 참고하기 바란다.
알려진대로 삼성 SW 역량 테스트 문제이다보니 구현이 힘든 편이긴 하다. 구현 문제를 풀면서 나름대로 정리해본 접근 방법은 다음과 같다.
- 우선 문제가 이해가 안되면 테스트 케이스를 통해서 직접 손으로 풀어본다.
- 문제에서의 조건들을 요약해본다. 이 때 그림을 적극 활용한다.
- 각 조건들을 메소드(함수)를 만들어서 구현해본다. 이 때, 어떤 알고리즘을 적용할지 생각해보고 가능하면 시간 복잡도도 함께 생각해보자.
- (특히 공부시에는) 디버깅을 적극적으로 활용한다.
- 반례들을 생각해보자. 반례는 최대한 극단적인 케이스로 생각하자.
사실 구현 문제 뿐만이 아닌 모든 문제에 해당하는 케이스라 생각한다. 그런데 특히 구현이 힘든 문제가 이렇게 생각해보는게 더 중요하다 생각한다.
다행히도 테스트 케이스가 꽤 많아서 문제 이해하는데 큰 도움을 주었을꺼라 생각한다. 예제 1, 2를 통해 어떤 상황인지 알아보자.
5 5
#####
#..B#
#.#.#
#RO.#
#####
오른쪽으로 기울이면 빨간 구슬(R)이 오른쪽으로 굴러가면서 1번만에 탈출 성공하게 된다.
#####
#..B#
#.#.#
#.O.#
#####
7 7
#######
#...RB#
#.#####
#.....#
#####.#
#O....#
#######
탈출까지 직접하면 5번이 걸리는데, 이 예제를 통해 한가지 사실을 알 수 있다. 일단 당연하겠지만 왼쪽으로 밖에 이동이 안되므로 왼쪽으로 기울여보면 아래와 같다.
#######
#RB...#
#.#####
#.....#
#####.#
#O....#
#######
코드로써 구현해야되는 부분을 확인할 수 있는게, 어떻게 기울이든 간에 빨간색 공 위치(이하 R)와 파란색 공 위치(이하 B)는 겹치치 않는다는 것이다. 이 문제의 핵심 구현 부분이라고 할 수 있다.
안되는 케이스 역시 살펴볼 필요가 있다.
3 10
##########
#.O....RB#
##########
직관적으로도 왼쪽으로 기울여야 되는 것을 알 수 있다. 하지만 그럴 경우 R과 B는 구멍 O를 지나가게 되어서 문제 조건을 만족하지 않고, 나머지 방향으로 못가므로 10번 이내에 문제를 해결할 수 없다. 즉 -1이 나오게 된다.
테스트 케이스를 통한 문제 조건은 다음과 같다.
- (입출력) #은 이동 불가능, .은 이동 가능, O는 이동 가능하지만 빠지는 구멍
- 보드를 기울이면 갈 수 있는 끝까지 움직인다.
- R이 탈출할 때 B가 탈출하면 안된다.
- R과 B의 위치는 겹치지 않는다.
- 시뮬레이션은 총 10번 진행된다.
아래 조건 구현에서 각각 하나씩 해보도록 하자.
입출력은 main()
메소드에서 진행한다.
우선 각 공의 위치가 필요하므로, 이를 Ball
로 만들어 놓자. cnt는 해당 볼이 움직이 횟수를 의미한다.
...
static class Ball {
int row;
int col;
int cnt;
public Ball(int row, int col, int cnt) {
this.row = row;
this.col = col;
this.cnt = cnt;
}
}
...
그리고 움직일 수 있는 케이스와 움직이지 못하는 케이스로 분리해서 해결해보자.
'.'
, 'O'
'#'
, R의 경우 B, B의 경우 R공의 종류에 따라 케이스가 나눠짐을 확인할 수 있다. 이럴 경우 일일이 조건문으로 처리도 가능하지만, 부분적으로 움직이는게 가능하므로 일단 '.'
으로 처리하고 나중에 위치를 처리하는 것이 좀 더 좋아보인다.
즉 조건이 많아지면 처리하기 힘들어진다. 이를 최대한 단순하게 '#'
(갈 수 없음) 과 '.'
(갈 수 있음) 으로 최대한 묶고, 특수한 케이스(구멍, 공)은 좌표를 따로 저장해두자.
아래에서는 구멍 O
는 holeRow, holeCol로 저장하였고, 공은 Ball 객체를 사용하였다.
...
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st;
st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
board = new char[n][m]; // static char[][] board;
isOut = new boolean[2]; // static boolean[];
Ball[] balls = new Ball[2]; // 인덱스 0 : R, 인덱스 1 : B
int ans = -1;
for(int i = 0; i < n; i++) {
String row = br.readLine();
for(int j = 0; j < m; j++) {
char c = row.charAt(j);
if(c == 'R') {
balls[0] = new Ball(i, j, 0); // 우선 Ball 객체 생성한다.
board[i][j] = '.'; // 갈 수 있는 케이스로 지정
} else if(c == 'B') {
balls[1] = new Ball(i, j, 0); // 우선 Ball 객체 생성한다.
board[i][j] = '.'; // 갈 수 있는 케이스로 지정
} else if(c == 'O') {
holeRow = i;
holeCol = j; // 구멍의 좌표 저장
board[i][j] = '.'; // 갈 수 있는 케이스로 지정
} else {
board[i][j] = c;
}
}
}
...
조건을 살펴보면, 움직일 수 있는 곳은 .
이므로 .
이 있는 곳은 다 움직일 수 있다. 다만 R의 경우 B를 만나면 일종의 벽이 되어서 진행할 수 없다. 그리고 B의 경우는 반대로 R을 만날 때 그렇다. 그러면 아래와 같은 방법으로 코드를 짜면 될 것이라 생각 가능하다.
...
static int[] dr = {-1, 1, 0, 0};
static int[] dc = {0, 0, -1, 1};
...
dr[i]
를 지속적으로 더해야 한다.우선 굴러가는 것을 구현해보자.
// rolling(방향, 공 인덱스(다른 공은 1 - rbIdx로 확인 가능), 공, 다른 공)
public static int[] rolling(int i, int rbIdx, int[] ballPos, int[] otherBallPos) {
int ballRow = ballPos[0], ballCol = ballPos[1];
int otherBallRow = otherBallPos[0], otherBallCol = otherBallPos[1];
while(board[ballRow + dr[i]][ballCol + dc[i]] == '.') { // 갈 수 있을 때
// 만일 다른 공이 나갔을 때 && 해당 공이 다른 공 좌표에 도달 할 때 -> 멈춤
if(!isOut[1- rbIdx] && ballRow + dr[i] == otherBallRow && ballCol + dc[i] == otherBallCol) break;
isNotMoved = false; // 안움직이면 구지 큐에 넣을 필요가 없다.
// 이상 없으면 계속 이동
ballRow += dr[i];
ballCol += dc[i];
// 이동 후 공이 나갔는지 확인
if(ballRow == holeRow && ballCol == holeCol) isOut[rbIdx] = true;
}
return new int[] {ballRow, ballCol};
}
조건문으로 간단하게 처리가 가능하다.
...
if(isOut[1]) { // B가 빠지면 볼 것도 없다. 그냥 무시하자.
continue;
} else if(isOut[0]) { // B는 안빠졌는데 A가 빠졌다면 그게 바로 우리가 찾는 케이스이다.
flag = true;
// int ans = -1로 bfs 시작 전에 초기화해준다.
ans = nextRed.cnt; // 만약 이것을 안거칠 경우 ans = -1이 되게 된다.
break;
}
...
추가적으로 if(isNotMoved) continue;
를 적어줘서 모두 움직이지 않는 케이스는 무시해서 메모리를 조금이라도 아껴보자.
이 문제의 핵심이다. 만일 3-1 코드를 진행할 경우 다음과 같은 상황을 만나게 된다. 우선 최대한 간단한 사례를 보자. (편의상 구멍은 제거하였다.)
######
#..BR#
######
아주 간단한 사례이다. 그런데 왼쪽으로 기울인 것을 구현하면 3-1 코드만 진행할 경우 다음과 같이 움직인다.
######
#B..R#
######
우리의 직관과는 달리 B만 움직인다. 그 이유는 우리가 R부터 움직이는 것을 구현하였는데, 처음 R의 경우 왼쪽에 B가 있어 더 이상 움직이지 못하기 때문이다. 그러면 어떻게 해야될까?
방법은 매우 간단하다. 한번 더 움직이면 된다. 공들 중 하나는 이미 움직임을 완료했기에 남은 하나에 대해서만 추가로 한번 움직이면 최종적인 위치가 나올 것이다.
그런데, 그 남은 하나가 뭔지 어떻게 알 수 있을까? 모르기 때문에 일단 둘 다 반복문을 똑같이 돌린다.
남은 과제는 해당 공에 대해서 언제 반복문을 돌려야 하는지 따져야 된다. 우선 반복문이 필요 없는 경우는 다음과 같다.
isOut[i]
board[nextPos[0] + dr[i]][nextPos[1] + dc[i]] != '.'
이 두 조건이 전부인 것을 알 수 있다. 이 두 조건이 아닌 경우는 반복문이 필요하다.
....
if(!isOut[0] && board[nextRedPos[0] + dr[i]][nextRedPos[1] + dc[i]] == '.') {
nextRedPos = rolling(i, 0, nextRedPos, nextBluePos);
}
if(!isOut[1] && board[nextBluePos[0] + dr[i]][nextBluePos[1] + dc[i]] == '.') {
nextBluePos = rolling(i, 1, nextBluePos, nextRedPos);
}
....
이건 간단하다. 빨간공.cnt
가 10일 때는 종료해준다. 물론 Queue에 넣을 때마다 cnt+1 해주는 것은 절대 잊지 말자.
import java.io.*;
import java.util.*;
public class Main {
static class Ball {
int row;
int col;
int cnt;
public Ball(int row, int col, int cnt) {
this.row = row;
this.col = col;
this.cnt = cnt;
}
}
static int holeRow, holeCol;
static int[] dr = {-1, 1, 0, 0};
static int[] dc = {0, 0, -1, 1};
static char[][] board;
static boolean isNotMoved;
static boolean[] isOut;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st;
st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
board = new char[n][m];
isOut = new boolean[2];
Ball[] balls = new Ball[2];
int ans = -1;
for(int i = 0; i < n; i++) {
String row = br.readLine();
for(int j = 0; j < m; j++) {
char c = row.charAt(j);
if(c == 'R') {
balls[0] = new Ball(i, j, 0);
board[i][j] = '.';
} else if(c == 'B') {
balls[1] = new Ball(i, j, 0);
board[i][j] = '.';
} else if(c == 'O') {
holeRow = i;
holeCol = j;
board[i][j] = '.';
} else {
board[i][j] = c;
}
}
}
Queue<Ball[]> q = new LinkedList<>();
q.add(balls);
while(!q.isEmpty()) {
Ball[] now = q.poll();
Ball nowRed = now[0];
Ball nowBlue = now[1];
boolean flag = false;
if(nowRed.cnt >= 10) break;
for(int i = 0; i < 4; i++) {
isOut[0] = false;
isOut[1] = false;
isNotMoved = true;
int[] nextRedPos = {nowRed.row, nowRed.col};
int[] nextBluePos = {nowBlue.row, nowBlue.col};
nextRedPos = rolling(i, 0, nextRedPos, nextBluePos);
nextBluePos = rolling(i, 1, nextBluePos, nextRedPos);
if(!isOut[0] && board[nextRedPos[0] + dr[i]][nextRedPos[1] + dc[i]] == '.') {
nextRedPos = rolling(i, 0, nextRedPos, nextBluePos);
}
if(!isOut[1] && board[nextBluePos[0] + dr[i]][nextBluePos[1] + dc[i]] == '.') {
nextBluePos = rolling(i, 1, nextBluePos, nextRedPos);
}
if(isNotMoved) continue;
Ball nextRed = new Ball(nextRedPos[0], nextRedPos[1], nowRed.cnt + 1);
Ball nextBlue = new Ball(nextBluePos[0], nextBluePos[1], nowBlue.cnt + 1);
if(isOut[1]) {
continue;
} else if(isOut[0]) {
flag = true;
ans = nextRed.cnt;
break;
}
q.add(new Ball[] {nextRed, nextBlue});
}
if(flag) break;
}
System.out.println(ans);
br.close();
}
public static int[] rolling(int i, int rbIdx, int[] ballPos, int[] otherBallPos) {
int ballRow = ballPos[0], ballCol = ballPos[1];
int otherBallRow = otherBallPos[0], otherBallCol = otherBallPos[1];
while(board[ballRow + dr[i]][ballCol + dc[i]] == '.') {
if(!isOut[1- rbIdx] && ballRow + dr[i] == otherBallRow && ballCol + dc[i] == otherBallCol) break;
isNotMoved = false;
ballRow += dr[i];
ballCol += dc[i];
if(ballRow == holeRow && ballCol == holeCol) isOut[rbIdx] = true;
}
return new int[] {ballRow, ballCol};
}
}