https://www.acmicpc.net/problem/21611

 

21611번: 마법사 상어와 블리자드

마법사 상어는 파이어볼, 토네이도, 파이어스톰, 물복사버그, 비바라기 마법을 할 수 있다. 오늘 새로 배운 마법은 블리자드이고, 크기가 N×N인 격자에서 연습하려고 한다. N은 항상 홀수이고, (

www.acmicpc.net

코드설명

구현(Implementation) + 시뮬레이션(Simulation) 를 활용하는 문제입니다.

 

문제를 한 문장으로 설명해보면, 주어진 판에서 상어가 블리자드 마법을 사용하여 시뮬레이션하고, 마법에 의해 일어나는 구슬의 변화를 처리하는 프로그램입니다.

 

코드의 큰 로직입니다. 자세한 사항은 모두 주석에 달아두었습니다. 또한 각 과정마다 모두 디버깅을 위해 출력과정을 담아두었습니다.

  1. 블리자드 마법 적용 (blizardMagic 함수):
    • 주어진 방향과 거리에 따라 블리자드 마법을 시뮬레이션하여 Map에서 해당 위치의 구슬을 제거합니다.
  2. 빈 구슬 칸 채우기 (makeNewMap 함수):
    • 구슬이 제거된 후 빈 공간을 제외한 나머지 구슬들을 상어와 가까운 칸에서 순서대로 떨어뜨려 채웁니다.
  3. 4개 이상 연속 구슬 폭파 (bombBallWhichIsLongerThan4 함수):
    • Map을 순회하면서 4개 이상의 연속된 구슬이 있는 경우 폭파시킵니다.
    • 각 그룹은 구슬의 개수와 번호로 큐에 저장되고, 나중에 처리됩니다.
  4. 구슬 변화 (mutateToNewBall 함수):
    • 구슬 폭파 후 남은 구슬을 그룹화하여 구슬의 개수와 번호로 새로운 얼음판을 만듭니다.
    • 얼음판을 중앙에서 시작하여 반시계방향으로 탐색하며 변화를 적용합니다.
  5. 결과 출력:
    • 마법을 여러 번 시뮬레이션한 후 각 색의 구슬이 폭발한 횟수를 계산하여 출력합니다.

 

유의해야할점입니다.

첫번째는, 구슬의 개수를 셀때, 마지막에도 세주어야합니다. 처음에 94%에서 틀려서 원인을 찾아보니 마지막에 칸에서 벗어날때도 개수를 세주어야합니다.

while(level <= N) {
//각 2번씩 같은숫자만큼 방향을 바꿔가며 이동한다.
for(int turn = 0; turn < 2; turn ++) {
dir = changeDir(dir);
//level만큼 이동한다. level, level, level + 1, level + 1 ... 의 규칙성이 존재한다.
for(int move = 0 ; move < level; move ++) {
r = r + dx[dir]; //이동
c = c + dy[dir];
moveCnt += 1;
if(r < 0 || r > N || c < 0 || c > N) continue;
if(map[r][c] != EMPTY_SPACE) {
//만약 연속적인 볼과 다르다면,
if(continuousBallNumber != map[r][c]) {
//만약 다른데 갯수가 4개 이상이라면, 해당 큐는 새로운 큐에 들어가지 못한다. 즉 폭파한다는 뜻이다.
if(continuousCnt >= 4) {
answer[continuousBallNumber] += continuousCnt;
findFlag = true;
continuousCheckQ.clear();
}
//만약 4개 이하라면, 이전 큐에 넣어줍니다.
else if(continuousCnt < 4) {
while(!continuousCheckQ.isEmpty()) {
q.offer(continuousCheckQ.poll());
}
}
continuousCnt = 1;
continuousBallNumber = map[r][c];
}
//연속적인 볼이라면 갯수를 증가시킨다.
else if(continuousBallNumber == map[r][c]) { //만약 이전 값과 같다면, 연속적인 볼이라면 하나씩 세어줍니다.
continuousCnt += 1;
}
continuousCheckQ.offer(map[r][c]); //먼저 넣는다.
}
}
}
level += 1;
}
//만약 다른데 갯수가 4개 이상이라면, 해당 큐는 새로운 큐에 들어가지 못한다.
if(continuousCnt >= 4) {
answer[continuousBallNumber] += continuousCnt;
findFlag = true;
continuousCheckQ.clear();
}

 

두번쨰는, 반시계방향으로 순회하는것에 대한 로직입니다.

이 사항은 유의해야할점이라기보다는  이 문제 구현에 있어서 가장 중요한 코드이기에 읽어보면 좋습니다.

//반시계방향으로 순회.
public static void traverseByAntiColor() {
int level = 1;
int dir = 0; //처음에는 왼쪽방향부터 시작합니다.
int r = (N/2); int c = (N/2);
int nr = r; int nc = c;
int moveCnt = 1;
while(level <= N) {
//각 2번씩 같은숫자만큼 방향을 바꿔가며 이동한다.
for(int turn = 0; turn < 2; turn ++) {
dir = changeDir(dir);
//level만큼 이동한다. level, level, level + 1, level + 1 ... 의 규칙성이 존재한다.
for(int move = 0 ; move < level; move ++) {
r = r + dx[dir]; //이동
c = c + dy[dir];
moveCnt += 1;
if(r < 0 || r > N || c < 0 || c > N) continue;
System.out.print(" "+map[r][c]);
}
System.out.println(" ");
}
level += 1;
}
}

 

세번째, 각 코드를 안에서부터 채워나갈떄 하나의 Map안에서 땡긴다기보다는 Queue를 활용해서 새로운 newMap을 반환시켜 한다면 편하게 구현할 수 있는 것 같습니다. 여러 풀이가 있지만, 저는 그렇습니다.

처음에는 DFS로 구현하는것도 생각해봤었는데 이렇게 For문으로 구현하는것이 훨씬 편한것 같습니다.

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;
public class Main {
public static int N, M;
public static int[][] map;
public static int[] answer = new int[4];
public static int[] dx = {-1, 1, 0, 0};
public static int[] dy = {0, 0, -1, 1};
public static int EMPTY_SPACE = 0;
public static boolean bombBallWhichIsLongerThan4Flag = false;
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());
map = new int[N][N];
for(int i=0; i<N;i++) {
st = new StringTokenizer(br.readLine());
for(int j=0;j<N;j++) {
map[i][j] = Integer.parseInt(st.nextToken());
// System.out.print(" "+map[i][j]);
}
// System.out.println();
}
// System.out.println();
//
for(int i=0; i<M;i++) {
st = new StringTokenizer(br.readLine());
int d = Integer.parseInt(st.nextToken());
int s = Integer.parseInt(st.nextToken());
//마법이 주어지고, 일련의 과정이 진행된다.
//이거 굳이 떙기지않고, 새로 만들어서 하나씩 채워가는것이 훨씬 편할것이다.
//1. 마법에 의해 얼음파편이 떨어져 구슬이 파괴된다.
blizardMagic(d - 1, s);
// System.out.println("blizard Magic ==================================");
// for(int k=0;k<N;k++) {
// for(int h=0;h<N;h++) {
// System.out.print(" "+map[k][h]);
// }
// System.out.println();
// }
// System.out.println();
//2. 빈 구슬칸을 채워준다.
// System.out.println("makeNew Map ==================================");
map = makeNewMap();
// for(int k=0;k<N;k++) {
// for(int h=0;h<N;h++) {
// System.out.print(" "+map[k][h]);
// }
// System.out.println();
// }
// System.out.println();
bombBallWhichIsLongerThan4Flag = false;
//3. 4개이상 연속하는 구슬이있을경우 구슬이 4개 이상의 구슬이 모두 폭파된다.
while(bombBallWhichIsLongerThan4Flag == false) {
map = bombBallWhichIsLongerThan4();
// System.out.println("bombBallWhichIsLongerThan4 Map ==================================");
// for(int k=0;k<N;k++) {
// for(int h=0;h<N;h++) {
// System.out.print(" "+map[k][h]);
// }
// System.out.println();
// }
// System.out.println();
}
//4. 더이상 폭발할 구슬이 없다면, 구슬이 변화하는 단계가 된다. 연속하는 구슬은 하나의 그룹이라고한다.
// 하나의 그룹은 두개의 구슬 A와 B로 변화한다. 구슬 A의 번호는 그룹에 들어있는 구슬의 개수이다. B는 그룹을 이루고 있는 구슬의 번호이다.
// 구슬은 다시 구슬의 순서대로 1번칸부터 차례대로 A, B의 순서로 칸에 들어간다.
map = mutateToNewBall();
// System.out.println("mutateToNewBall Map ==================================");
// for(int k=0;k<N;k++) {
// for(int h=0;h<N;h++) {
// System.out.print(" "+map[k][h]);
// }
// System.out.println();
// }
// System.out.println();
}
// System.out.println(answer[0]);
System.out.println(answer[1] * 1 + answer[2] * 2 + answer[3] * 3);
// traverseByAntiColor();
}
//4개 이상 연속 구슬이 있을시에 구슬이 폭발한다.
public static int[][] mutateToNewBall() {
Queue<Integer> q = new LinkedList<>();
Queue<Integer> continuousCheckQ = new LinkedList<>();
int level = 1;
int dir = 0; //처음에는 왼쪽방향부터 시작합니다.
int r = (N/2); int c = (N/2);
int moveCnt = 1;
//연속적인 볼의 개수의 변수명.
//먼저 기존 얼음판을 빈칸으 빼고 복사해옵니다.
int continuousCnt = 0;
int continuousBallNumber = EMPTY_SPACE; //어처피 이전에 블리자드로 구슬이 파괴된 후에 구슬이 다시 채워지니 빈칸은 걱정안해도 될듯하다.
while(level <= N) {
//각 2번씩 같은숫자만큼 방향을 바꿔가며 이동한다.
for(int turn = 0; turn < 2; turn ++) {
dir = changeDir(dir);
//level만큼 이동한다. level, level, level + 1, level + 1 ... 의 규칙성이 존재한다.
for(int move = 0 ; move < level; move ++) {
r = r + dx[dir]; //이동
c = c + dy[dir];
moveCnt += 1;
if(r < 0 || r > N || c < 0 || c > N) continue;
if(map[r][c] != EMPTY_SPACE) {
//만약 연속적인 볼과 다르다면,
if(continuousBallNumber != map[r][c]) {
//만약 다르다면, continuousQ에 (그룹의 개수, 구슬의 번호) 를 차례대로(순서지켜야한다) 넣는다.
if(continuousBallNumber != EMPTY_SPACE) {
continuousCheckQ.offer(continuousCnt);
continuousCheckQ.offer(continuousBallNumber);
//먼저 값을 넣어야한다. (맨 처음값같은경우는 어처피 값이 안들어가므로 상관없다.)
while(!continuousCheckQ.isEmpty()) {
q.offer(continuousCheckQ.poll());
}
}
continuousCnt = 1;
continuousBallNumber = map[r][c];
}
//연속적인 볼이라면 갯수를 증가시킨다.
else if(continuousBallNumber == map[r][c]) { //만약 이전 값과 같다면, 연속적인 볼이라면 하나씩 세어줍니다.
continuousCnt += 1;
}
}
}
}
level += 1;
}
continuousCheckQ.offer(continuousCnt);
continuousCheckQ.offer(continuousBallNumber);
//먼저 값을 넣어야한다. (맨 처음값같은경우는 어처피 값이 안들어가므로 상관없다.)
while(!continuousCheckQ.isEmpty()) {
q.offer(continuousCheckQ.poll());
}
int[][] newMap = new int[N][N];
level = 1;
dir = 0; //처음에는 왼쪽방향부터 시작합니다.
r = (N/2); c = (N/2);
moveCnt = 1;
while(level <= N) {
//각 2번씩 같은숫자만큼 방향을 바꿔가며 이동한다.
for(int turn = 0; turn < 2; turn ++) {
dir = changeDir(dir);
//level만큼 이동한다. level, level, level + 1, level + 1 ... 의 규칙성이 존재한다.
for(int move = 0 ; move < level; move ++) {
r = r + dx[dir]; //이동
c = c + dy[dir];
moveCnt += 1;
if(r < 0 || r > N || c < 0 || c > N) continue;
if(q.isEmpty()) return newMap;
newMap[r][c] = q.poll();
}
}
level += 1;
}
return newMap;
}
public static void blizardMagic(int d, int s) {
int r = (N/2), c = (N/2);
for(int i=0; i<s;i++) {
r = r + dx[d];
c = c + dy[d];
//문제어 주어진 S는 (N-1)/2 이기에 범위 걱정없다.
map[r][c] = EMPTY_SPACE;
}
}
public static int[][] makeNewMap(){
Queue<Integer> q = new LinkedList<>();
int level = 1;
int dir = 0; //처음에는 왼쪽방향부터 시작합니다.
int r = (N/2); int c = (N/2);
int moveCnt = 1;
//먼저 기존 얼음판을 빈칸으 빼고 복사해옵니다.
while(level <= N) {
//각 2번씩 같은숫자만큼 방향을 바꿔가며 이동한다.
for(int turn = 0; turn < 2; turn ++) {
dir = changeDir(dir);
//level만큼 이동한다. level, level, level + 1, level + 1 ... 의 규칙성이 존재한다.
for(int move = 0 ; move < level; move ++) {
r = r + dx[dir]; //이동
c = c + dy[dir];
moveCnt += 1;
if(r < 0 || r > N || c < 0 || c > N) continue;
if(map[r][c] != EMPTY_SPACE) q.offer(map[r][c]);
}
}
level += 1;
}
int[][] newMap = new int[N][N];
level = 1;
dir = 0; //처음에는 왼쪽방향부터 시작합니다.
r = (N/2); c = (N/2);
moveCnt = 1;
while(level <= N) {
//각 2번씩 같은숫자만큼 방향을 바꿔가며 이동한다.
for(int turn = 0; turn < 2; turn ++) {
dir = changeDir(dir);
//level만큼 이동한다. level, level, level + 1, level + 1 ... 의 규칙성이 존재한다.
for(int move = 0 ; move < level; move ++) {
r = r + dx[dir]; //이동
c = c + dy[dir];
moveCnt += 1;
if(r < 0 || r > N || c < 0 || c > N) continue;
if(q.isEmpty()) return newMap;
newMap[r][c] = q.poll();
}
}
level += 1;
}
return newMap;
}
//4개 이상 연속 구슬이 있을시에 구슬이 폭발한다.
public static int[][] bombBallWhichIsLongerThan4() {
Queue<Integer> q = new LinkedList<>();
Queue<Integer> continuousCheckQ = new LinkedList<>();
int level = 1;
int dir = 0; //처음에는 왼쪽방향부터 시작합니다.
int r = (N/2); int c = (N/2);
int moveCnt = 1;
//연속적인 볼의 개수의 변수명.
//먼저 기존 얼음판을 빈칸으 빼고 복사해옵니다.
int continuousCnt = 0;
int continuousBallNumber = EMPTY_SPACE; //어처피 이전에 블리자드로 구슬이 파괴된 후에 구슬이 다시 채워지니 빈칸은 걱정안해도 될듯하다.
boolean findFlag = false;
while(level <= N) {
//각 2번씩 같은숫자만큼 방향을 바꿔가며 이동한다.
for(int turn = 0; turn < 2; turn ++) {
dir = changeDir(dir);
//level만큼 이동한다. level, level, level + 1, level + 1 ... 의 규칙성이 존재한다.
for(int move = 0 ; move < level; move ++) {
r = r + dx[dir]; //이동
c = c + dy[dir];
moveCnt += 1;
if(r < 0 || r > N || c < 0 || c > N) continue;
if(map[r][c] != EMPTY_SPACE) {
//만약 연속적인 볼과 다르다면,
if(continuousBallNumber != map[r][c]) {
//만약 다른데 갯수가 4개 이상이라면, 해당 큐는 새로운 큐에 들어가지 못한다. 즉 폭파한다는 뜻이다.
if(continuousCnt >= 4) {
answer[continuousBallNumber] += continuousCnt;
findFlag = true;
continuousCheckQ.clear();
}
//만약 4개 이하라면, 이전 큐에 넣어줍니다.
else if(continuousCnt < 4) {
while(!continuousCheckQ.isEmpty()) {
q.offer(continuousCheckQ.poll());
}
}
continuousCnt = 1;
continuousBallNumber = map[r][c];
}
//연속적인 볼이라면 갯수를 증가시킨다.
else if(continuousBallNumber == map[r][c]) { //만약 이전 값과 같다면, 연속적인 볼이라면 하나씩 세어줍니다.
continuousCnt += 1;
}
continuousCheckQ.offer(map[r][c]); //먼저 넣는다.
}
}
}
level += 1;
}
//만약 다른데 갯수가 4개 이상이라면, 해당 큐는 새로운 큐에 들어가지 못한다.
if(continuousCnt >= 4) {
answer[continuousBallNumber] += continuousCnt;
findFlag = true;
continuousCheckQ.clear();
}
//만약 4개 이하라면, 이전 큐에 넣어줍니다.
else if(continuousCnt < 4) {
while(!continuousCheckQ.isEmpty()) {
q.offer(continuousCheckQ.poll());
}
}
if(findFlag == false) {
bombBallWhichIsLongerThan4Flag = true;
return map;
}
int[][] newMap = new int[N][N];
level = 1;
dir = 0; //처음에는 왼쪽방향부터 시작합니다.
r = (N/2); c = (N/2);
moveCnt = 1;
while(level <= N) {
//각 2번씩 같은숫자만큼 방향을 바꿔가며 이동한다.
for(int turn = 0; turn < 2; turn ++) {
dir = changeDir(dir);
//level만큼 이동한다. level, level, level + 1, level + 1 ... 의 규칙성이 존재한다.
for(int move = 0 ; move < level; move ++) {
r = r + dx[dir]; //이동
c = c + dy[dir];
moveCnt += 1;
if(r < 0 || r > N || c < 0 || c > N) continue;
if(q.isEmpty()) return newMap;
newMap[r][c] = q.poll();
}
}
level += 1;
}
return newMap;
}
//반시계방향으로 순회.
public static void traverseByAntiColor() {
int level = 1;
int dir = 0; //처음에는 왼쪽방향부터 시작합니다.
int r = (N/2); int c = (N/2);
int nr = r; int nc = c;
int moveCnt = 1;
while(level <= N) {
//각 2번씩 같은숫자만큼 방향을 바꿔가며 이동한다.
for(int turn = 0; turn < 2; turn ++) {
dir = changeDir(dir);
//level만큼 이동한다. level, level, level + 1, level + 1 ... 의 규칙성이 존재한다.
for(int move = 0 ; move < level; move ++) {
r = r + dx[dir]; //이동
c = c + dy[dir];
moveCnt += 1;
if(r < 0 || r > N || c < 0 || c > N) continue;
System.out.print(" "+map[r][c]);
}
System.out.println(" ");
}
level += 1;
}
}
public static int changeDir(int dir) {
if(dir == 0) {
return 2;
}else if(dir == 1) {
return 3;
}else if(dir == 2) {
return 1;
}else if(dir == 3) {
return 0;
}
//여기까지 올일은없음.
return -1;
}
}

+ Recent posts