이번에는 "빡구현"이라고 불리는 시뮬레이션, 구현에 대해서 공부했다. 시뮬레이션은 정형화된 방식의 풀이 방법이 있지 않은 문제들을 말한다. 그래서 지금까지 공부했던 알고리즘이나 자료구조들과는 달리 필요한 추가적인 배경지식이랄 것이 없다고 할 수 있다. 시뮬레이션 문제들에 대한 감을 잡고, 그들의 특징에는 어떤 것이 있을 지 연습 문제들을 풀면서 알아보자.
처음으로 접한 시뮬레이션 문제였는데, 끝나질 않는 스크롤을 보면서 이것이 시뮬레이션인가 하는 선입견을 갖게 됐다.
문제를 요약해보자면 이렇다.
위 정보를 바탕으로 최소 사각지대의 크기를 구하는 것이다.
처음에는 cctv 방향의 모든 경우의 수를 확인하지 않는 방법을 생각하려 했는데, 사무실의 최대크기도 8*8이었고, cctv도 최대 7개이므로 네 방향으로 모두 돌릴 수 있는 1, 3, 4번 카메라로만 구성되어있다고 해도 4^7, 약 8000번의 검사밖에는 필요하지 않았다. 내가 해결한 방식은 다음과 같다.
package simulation;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;
//진법 활용해서 독립적인 변수들의 경우의수 구하는 방법 기억하기!!!
class Cctv {
int x;
int y;
public Cctv(int x, int y) {
this.x = x;
this.y = y;
}
}
public class BOJ15683 {
static int[][] board;
static List<Cctv> cctvs = new ArrayList<>();
static int[] dx = {1, 0, -1, 0};
static int[] dy = {0, 1, 0, -1};
static boolean[][] monitored;
static int n;
static int m;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
n = Integer.parseInt(st.nextToken());
m = Integer.parseInt(st.nextToken());
int wallsN = 0;
board = new int[n][m];
for (int i = 0; i < n; i++) {
st = new StringTokenizer(br.readLine());
for (int j = 0; j < m; j++) {
int tmp = Integer.parseInt(st.nextToken());
if (tmp == 6) wallsN++;
if (tmp != 0 && tmp != 6) cctvs.add(new Cctv(i, j));
board[i][j] = tmp;
}
}
int[] directions = new int[cctvs.size()];
int caseNum = (int) Math.pow(4, cctvs.size());
int max = 0;
for (int i = 0; i < caseNum; i++) {
fourJinsu(directions, i);
int tmpSum = 0;
monitored = new boolean[n][m];
for (int j = 0; j < directions.length; j++) {
Cctv tmp = cctvs.get(j);
int tmpDir = directions[j];
switch (board[tmp.x][tmp.y]) {
case 1:
tmpSum += monitor(tmp.x, tmp.y, tmpDir);
continue;
case 2:
if (tmpDir % 2 == 0) {
tmpSum += monitor(tmp.x, tmp.y, 0);
tmpSum += monitor(tmp.x, tmp.y, 2);
} else {
tmpSum += monitor(tmp.x, tmp.y, 1);
tmpSum += monitor(tmp.x, tmp.y, 3);
}
continue;
case 3:
tmpSum += monitor(tmp.x, tmp.y, tmpDir);
tmpSum += monitor(tmp.x, tmp.y, (tmpDir+1)%4);
continue;
case 4:
tmpSum += monitor(tmp.x, tmp.y, (tmpDir+1)%4);
tmpSum += monitor(tmp.x, tmp.y, (tmpDir+2)%4);
tmpSum += monitor(tmp.x, tmp.y, (tmpDir+3)%4);
continue;
case 5:
tmpSum += monitor(tmp.x, tmp.y, 0);
tmpSum += monitor(tmp.x, tmp.y, 1);
tmpSum += monitor(tmp.x, tmp.y, 2);
tmpSum += monitor(tmp.x, tmp.y, 3);
continue;
}
}
if (max < tmpSum) max = tmpSum;
}
System.out.println(n*m - max - cctvs.size() - wallsN);
}
private static int monitor(int x, int y, int direction) {
int cnt = 0;
while (true) {
x = x + dx[direction];
y = y + dy[direction];
if (y < 0 || x < 0 || x > n-1 || y > m-1 || board[x][y] == 6) break;
if (board[x][y] != 0 || monitored[x][y]) continue;
monitored[x][y] = true;
cnt++;
}
return cnt;
}
private static void fourJinsu(int[] directions, int caseNum) {
for (int i = 0; i < directions.length; i++) {
directions[i] = caseNum % 4;
caseNum = caseNum / 4;
}
}
}
또 느낀 구현 문제의 특징은 코드가 엄청 지저분해지기 쉽다는 것이었다. 많은 연습이 필요할 것 같다.
monitor 함수는 cctv의 위치와 방향을 인자로 받아 해당 cctv가 cover하는 감시 범위 크기를 반환해주는 함수다. 이것을 각각 경우의 수 별로 모두 더해서, 다른 경우의 수와 계속해서 비교하는 과정을 통해 최댓값을 구할 수 있다.
처음에는 감시하는 방향에 따라 함수를 따로 구현했었는데 dx, dy 배열을 선언해서 dir을 인덱스로 방향을 지정할 수 있도록 해서 코드의 중복을 줄였다. 코드가 간결해졌다.
또, 구현 문제에서는 경우의 수를 구해야하는 상황이 많은데, n진수를 활용해서 경우의 수를 구하는 방법이 있다. 모든 변수들이 독립적인 경우에는, 모든 경우의 수 (이 문제에서는 4^5)에 대해 4진법으로 변환해서, 각 자릿수의 0,1,2,3을 cctv의 방향으로써 사용하면 된다. 써먹을 데가 많은 스킬인 것 같다.
1번 문제처럼 board를 첫번째 칸부터 확인해가며 스티커 방향의 모든 경우에 대해서 검사하면 되는 문제였다.
package simulation;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;
public class BOJ18808 {
static boolean[][] tmpSticker;
static boolean[][] laptop;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
laptop = new boolean[n][m];
int nSticker = Integer.parseInt(st.nextToken());
int cnt = 0;
for (int i = 0; i < nSticker; i++) {
//스티커 초기 설정
st = new StringTokenizer(br.readLine());
int x = Integer.parseInt(st.nextToken());
int y = Integer.parseInt(st.nextToken());
tmpSticker = new boolean[x][y];
for (int j = 0; j < x; j++) {
st = new StringTokenizer(br.readLine());
for (int k = 0; k < y; k++) {
if (st.nextToken().equals("1")) tmpSticker[j][k] = true;
}
}
//스티커 붙일 수 있는 지 검사 후 붙이기
a : for (int l = 0; l < 4; l++){
for (int j = 0; j < n; j++) {
for (int k = 0; k < m; k++) {
if (isAttachable(j, k)) {
cnt += attach(j, k);
break a;
}
}
}
rotate(x, y);
int tmp = x;
x = y;
y = tmp;
}
}
System.out.println(cnt);
}
private static boolean isAttachable(int row, int col) {
for (int i = 0; i < tmpSticker.length; i++) {
for (int j = 0; j < tmpSticker[0].length; j++) {
if (row+i >= laptop.length || col+j >= laptop[0].length) return false;
if (laptop[row+i][col+j] && tmpSticker[i][j]) return false;
}
}
return true;
}
private static int attach(int row, int col) {
int answer = 0;
for (int i = 0; i < tmpSticker.length; i++) {
for (int j = 0; j < tmpSticker[0].length; j++) {
if (tmpSticker[i][j]) {
laptop[row+i][col+j] = true;
answer++;
}
}
}
return answer;
}
private static void rotate(int x, int y) {
boolean[][] newThang = new boolean[y][x];
int cnt = 0;
for (int i = 0; i < y; i++) { //열에 대한 검사
for (int j = x-1; j >= 0; j--) { //행에 대한 검사
newThang[cnt / x][cnt % x] = tmpSticker[j][i];
cnt++;
}
}
tmpSticker = newThang;
int tmp = x;
x = y;
y = tmp;
}
private static void showBoard() {
for (int i = 0; i < tmpSticker.length; i++) {
System.out.println(Arrays.toString(tmpSticker[i]));
}
}
}
isAttachable 함수는 이미 붙어있는 스티커와 현재 스티커를 확인해가며 스티커를 해당 위치에 붙일 수 있는 지 확인하고, attach함수는 laptop에 스티커를 추가하고, 붙인 스티커의 크기를 반환한다.
앞의 두 문제보다 구현의 아이디어를 생각해내기 어려운 문제였다. 앞의 문제들처럼 모든 이동의 경우에 수에 대해 검사하는 것은 똑같았다. 처음 생각했던 방법은 각 이동에 대해서 행/열 별로 검사해가면서 인덱스를 활용해 인접한 숫자들이 같은 수가 있는 지를 확인하고 이동시키는 방법이었다.
static void up(int[][] tmpBoard) {
boolean prevChecked = false;
for (int j = 0; j < n; j++) { //col
int prev = 0; //이전 숫자의 idx 저장
for (int i = 1; i < n; i++) { // row
if (tmpBoard[i][j] == 0){
prevChecked = false;
continue;
}
if (tmpBoard[i][j] == tmpBoard[prev][j] && !prevChecked) {
prevChecked = true;
tmpBoard[prev][j] = 2*tmpBoard[prev][j];
tmpBoard[i][j] = 0;
}
//다른 숫자를 만났을 때
else {
prevChecked = false;
if (tmpBoard[prev][j] == 0) {
int tmp = tmpBoard[i][j];
tmpBoard[i][j] = 0;
tmpBoard[prev][j] = tmp;
} else{
int tmp = tmpBoard[i][j];
tmpBoard[i][j] = 0;
tmpBoard[prev+1][j] = tmp;
prev = prev+1;
}
}
}
}
}
위로 이동시키는 함수를 이렇게 구현했다. 이전에 숫자가 있는 인덱스를 저장해서 현재 검사하는 숫자와 비교하는 방식이었는데, 이 방법은 한 번의 이동에서 합쳐진 숫자는 다시 합쳐질 수 없다는 조건이나, 행/열의 마지막이 0인 경우 등 예외적인 경우를 처리하기가 너무 어려웠다.
그래서 그 다음으로 내가 생각한 방법은 deque를 이용해서 그 줄의 요소들을 저장한 뒤에 그 줄에 순서대로 다시 저장하는 것이었다. 스택을 활용한 괄호 문제 풀이처럼 말이다.
package simulation;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.StringTokenizer;
public class BOJ12100_2 {
static int[][] board;
static int[][] tmpBoard;
static int n;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
n = Integer.parseInt(br.readLine());
board = new int[n][n];
for (int i = 0; i < n; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
for (int j = 0; j < n; j++) {
board[i][j] = Integer.parseInt(st.nextToken());
}
}
int[] direction = new int[5];
int answer = 0;
for (int i = 0; i < 1024; i++) {
tmpBoard = new int[n][n];
for (int j = 0; j < n; j++) {
tmpBoard[j] = Arrays.copyOf(board[j], n);
}
fourJinsu(direction, i);
// System.out.println(Arrays.toString(direction));
for (int x : direction) {
switch (x) {
case 0 : up2(tmpBoard);
break;
case 1 : right2(tmpBoard);
break;
case 2 : down2(tmpBoard);
break;
case 3 : left2(tmpBoard); break;
}
}
answer = maxNum(tmpBoard) > answer ? maxNum(tmpBoard) : answer;
}
System.out.println(answer);
}
private static void showBoard() {
for (int i = 0; i < n; i++) {
System.out.println(Arrays.toString(tmpBoard[i]));
}
}
private static void fourJinsu(int[] direction, int n) {
for (int i = 0; i < 5; i++) {
direction[i] = n % 4;
n = n / 4;
}
}
private static int maxNum(int[][] tmpBoard) {
int answer = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (tmpBoard[i][j] > answer) answer = tmpBoard[i][j];
}
}
return answer;
}
//왼쪽으로 쌔리는경우
static void left2(int[][] tmpBoard) {
for (int i = 0; i < n; i++) {
LinkedList<Integer> deque = new LinkedList<>();
for (int j = 0; j < n; j++) {
if (tmpBoard[i][j] == 0) continue;
if (!deque.isEmpty() && deque.peekLast() == tmpBoard[i][j]) {
deque.offer(deque.pollLast()*2);
deque.offer(-1);
} else {
deque.offer(tmpBoard[i][j]);
}
tmpBoard[i][j] = 0;
}
int cnt = 0;
while (!deque.isEmpty()) {
int tmp = deque.poll();
if (tmp != -1) tmpBoard[i][cnt++] = tmp;
}
}
}
static void right2(int[][] tmpBoard) {
LinkedList<Integer> deque = new LinkedList<>();
for (int i = 0; i < n; i++) {
deque.clear();
for (int j = n-1; j >= 0; j--) {
if (tmpBoard[i][j] == 0) continue;
if (!deque.isEmpty() && deque.peekLast() == tmpBoard[i][j]) {
deque.offer(deque.pollLast()*2);
deque.offer(-1);
} else {
deque.offer(tmpBoard[i][j]);
}
tmpBoard[i][j] = 0;
}
int cnt = n-1;
while (!deque.isEmpty()) {
int tmp = deque.poll();
if (tmp != -1) tmpBoard[i][cnt--] = tmp;
}
}
}
static void up2(int[][] tmpBoard) {
LinkedList<Integer> deque = new LinkedList<>();
for (int j = 0; j < n; j++) {
for (int i = 0; i < n; i++) {
if (tmpBoard[i][j] == 0) continue;
if (!deque.isEmpty() && deque.peekLast() == tmpBoard[i][j]) {
deque.offer(deque.pollLast()*2);
deque.offer(-1);
} else {
deque.offer(tmpBoard[i][j]);
}
tmpBoard[i][j] = 0;
}
int cnt = 0;
while (!deque.isEmpty()) {
int tmp = deque.poll();
if (tmp != -1) tmpBoard[cnt++][j] = tmp;
}
}
}
static void down2(int[][] tmpBoard) {
LinkedList<Integer> deque = new LinkedList<>();
for (int j = 0; j < n; j++) {
for (int i = n-1; i >= 0; i--) {
if (tmpBoard[i][j] == 0) continue;
if (!deque.isEmpty() && deque.peekLast() == tmpBoard[i][j]) {
deque.offer(deque.pollLast()*2);
deque.offer(-1);
} else {
deque.offer(tmpBoard[i][j]);
}
tmpBoard[i][j] = 0;
}
int cnt = n-1;
while (!deque.isEmpty()) {
int tmp = deque.poll();
if (tmp != -1) tmpBoard[cnt--][j] = tmp;
}
}
}
}
이동시키는 함수들을 보면, deque에 스택처럼 저장해가며 같은 숫자가 나오면, 추가하지 않고 두 배의 값으로 저장해준 뒤에, -1을 추가해서 두 번 연속으로 합쳐지지 않도록 했다. 그리고 나서 덱에 있는 -1을 제외한 요소들을 이동하려는 방향에서부터 순서대로 저장해줬다.
그 외의 코드는 어려운 부분은 없었다.
모든 치킨집의 개수 중 M개를 선택하는 경우 (cCm)에 대해서, 각각의 집의 치킨거리를 모두 더해서 도시의 치킨 거리를 구하고, 이 중 최솟값을 구하면 되겠다.
package simulation;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
class Coordinate {
int x;
int y;
public Coordinate(int x, int y) {
this.x = x;
this.y = y;
}
}
public class BOJ15686 {
static List<Coordinate> houses = new ArrayList<>();
static List<Coordinate> chickens = new ArrayList<>();
static boolean[] survived;
static int[][] board;
static int n;
static int m;
static int answer = Integer.MAX_VALUE;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
n = Integer.parseInt(st.nextToken());
m = Integer.parseInt(st.nextToken());
board = new int[n][n];
for (int i = 0; i < n; i++) {
st = new StringTokenizer(br.readLine());
for (int j = 0; j < n; j++) {
int tmp = Integer.parseInt(st.nextToken());
if (tmp == 1) houses.add(new Coordinate(i, j));
else if (tmp == 2) chickens.add(new Coordinate(i, j));
board[i][j] = tmp;
}
}
survived = new boolean[chickens.size()];
chickenComb(0,0);
System.out.println(answer);
}
private static void chickenComb(int depth, int start) {
if (depth == m) {
int tmpMin = chickenDistance();
answer = answer < tmpMin ? answer : tmpMin;
// System.out.println(Arrays.toString(survived));
}
for (int i = start; i < survived.length; i++) {
survived[i] = true;
chickenComb(depth+1, i+1);
survived[i] = false;
}
}
private static int chickenDistance() {
int distance = 0;
for (Coordinate house : houses) {
int minDist = Integer.MAX_VALUE;
for (int i = 0; i < chickens.size(); i++) {
if (!survived[i]) continue;
int tmpDist = Math.abs(house.x - chickens.get(i).x) + Math.abs(house.y - chickens.get(i).y);
minDist = minDist < tmpDist ? minDist : tmpDist;
}
distance += minDist;
}
return distance;
}
}
전체 치킨집의 개수에서 m개를 뽑는 것은 위 문제들처럼 독립적인 경우가 아니기 때문에 백트래킹을 이용했다. chickenComb 함수로 모든 경우의 수를 구하고, 바로 치킨거리를 구하는 chickenDistance()를 호출하도록 했다.
chickenDistance()함수는 해당하는 경우에서, 각 집에서 가장 가까운 치킨집까지의 거리를 모두 더해 그 경우의 전체 치킨거리의 합을 반환한다. 그러면 chickenComb()에서 static 변수인 answer에 최솟값을 자동적으로 구할 수 있게 된다.
이렇게 해서 시뮬레이션 연습 문제들을 네 개 풀어봤다. 시뮬레이션 문제를 처음 접하면서 내가 느낀 점은 다음과 같다.
시뮬레이션 문제들은 특히나 많이 풀어봄으로써 구현력을 기를 수 있을 것 같다. 다음 포스팅에서는 시뮬레이션 문제들을 몇 개 더 풀어볼 것이다.
연습 문제 출처 : encrypted-def github