안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 SW 기출문제 치킨 배달 자바 풀이를 작성하고자 합니다.

 

문제 출처: https://www.acmicpc.net/problem/15686

 

15686번: 치킨 배달

크기가 N×N인 도시가 있다. 도시는 1×1크기의 칸으로 나누어져 있다. 도시의 각 칸은 빈 칸, 치킨집, 집 중 하나이다. 도시의 칸은 (r, c)와 같은 형태로 나타내고, r행 c열 또는 위에서부터 r번째 칸

www.acmicpc.net

 

 

1. 풀이 소스

 

import java.util.*;
import java.io.*;

import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = parseInt(st.nextToken());
        int m = parseInt(st.nextToken());
        ChickenDistance chickenDistance = new ChickenDistance(n, m);
        
        for (int row = 1; row <= n; row++) { // 1행 1열 이므로
            st = new StringTokenizer(br.readLine());
            for (int col = 1; col <= n; col++)
                chickenDistance.setMap(row, col, parseInt(st.nextToken()));
        }
        
        chickenDistance.start();
        
        System.out.println(chickenDistance.getMinDistance());
    }
}

class ChickenDistance {
    int n; // 정사각형 행렬
    int m; // m개의 선택된 치킨집
    boolean[] open; // 열려있는 집
    int minDistance = Integer.MAX_VALUE; // 최소 치킨 거리
    List<Point> chickens = new ArrayList<>(); // 치킨집 좌표
    List<Point> homes = new ArrayList<>(); // 집 좌표
    
    ChickenDistance(int n, int m) {
        this.n = n;
        this.m = m;
    }
    
    void setMap(int row, int col, int value) {
        if (value == 1) homes.add(new Point(row, col)); // 1이라면 집에 추가
        else if (value == 2) chickens.add(new Point(row, col)); // 2라면 치킨집 추가
    }
    
    void start() {
        open = new boolean[chickens.size()]; // 열려 있는집 초기화하기
        
        dfs(0, 0); // dfs
    }
    
    void dfs(int start, int cnt) {
        if (cnt == m) {            
            int minLocalDistance = 0; // 현재 dfs에서 지역변수 Distance 초기화  (모든 집이 거리 비교 끝나면 비교)

            for (Point home : homes) { // 집 리스트 순회
                int distance = Integer.MAX_VALUE; // 비교할 거리는 최대치로 초기화
                for (int i = 0; i < chickens.size(); i++) {
                    if (open[i]) { // 만약 열려 있다면
                        Point ch = chickens.get(i);
                        int nowD = Math.abs(home.row - ch.row) + Math.abs(home.col - ch.col);
                        distance = Math.min(nowD, distance); // 치킨집과 비교해서 거리 업데이트
                    }
                }
                minLocalDistance += distance;
            }
            
            minDistance = Math.min(minLocalDistance, minDistance);
            return;
        }
        
        for (int i = start; i < chickens.size(); i++) {
            open[i] = true; // 집 열려 있도록 설정
            dfs(i + 1, cnt + 1); // 다음 재귀로 조합 설정
            open[i] = false; // 백트래킹으로 다시 초기화함으로써, 다음 재귀에 영향 받지 않도록 설정
        }
    }
    
    int getMinDistance() {
        return minDistance;
    }
    
    static class Point {
        int row;
        int col;
        
        Point (int row, int col) {
            this.row = row;
            this.col = col;
        }
    }
}

 

 

2. 풀이 중점 사항

 

해당 문제는 백트래킹으로 해결하는 방법이 있습니다.

재귀를 수행할 때, m개의 치킨집을 선정한 후, 각 집에서 치킨집을 순회하며 거리를 측정해야 했습니다.

따라서, 먼저 백트래킹으로  치킨집을 여는 방법으로 m개의 조합이 설정될 수 있도록 하였습니다.

 

for (int i = start; i < chickens.size(); i++) {
    open[i] = true; // 집 열려 있도록 설정
    dfs(i + 1, cnt + 1); // 다음 재귀로 조합 설정
    open[i] = false; // 백트래킹으로 다시 초기화함으로써, 다음 재귀에 영향 받지 않도록 설정
}

 

만약 총 m개의 치킨집이 선택이 된다면 (open이 먼저 true가 되므로 총 m가 true)

int minLocalDistance = 0; // 현재 dfs에서 지역변수 Distance 초기화  (모든 집이 거리 비교 끝나면 비교)

for (Point home : homes) { // 집 리스트 순회
    int distance = Integer.MAX_VALUE; // 비교할 거리는 최대치로 초기화
    for (int i = 0; i < chickens.size(); i++) {
        if (open[i]) { // 만약 열려 있다면
            Point ch = chickens.get(i);
            int nowD = Math.abs(home.row - ch.row) + Math.abs(home.col - ch.col);
            distance = Math.min(nowD, distance); // 치킨집과 비교해서 거리 업데이트
        }
    }
    minLocalDistance += distance;
}

minDistance = Math.min(minLocalDistance, minDistance);
return;
}

 

minLocalDistance를 0으로 초기화한 후 집을 순회하며 가장 적은 거리의 치킨집을 업데이트합니다. 그리고, 하나의 집에 대한 치킨 거리가 업데이트되면, minLocalDistance에 최소 거리를 더하여 다음 집의 치킨 거리를 업데이트합니다.

 

이렇게 모든 집 업데이트가 끝나면, minDistance를 업데이트하고 return 합니다.

이어서 다음 재귀가 실행되고 cnt == m이되면 이와 같은 방법을 진행하되, 백트래킹으로 인해 다른 조합을 가진 오픈 치킨집이 설정되게 됩니다.

 

이상으로 치킨 거리 문제 풀이를 마치도록 하겠습니다.

감사합니다!!

 

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 SW 기출문제 드래곤 커브 자바 풀이를 진행하고자 합니다.

 

문제 출처:https://www.acmicpc.net/problem/15685

 

15685번: 드래곤 커브

첫째 줄에 드래곤 커브의 개수 N(1 ≤ N ≤ 20)이 주어진다. 둘째 줄부터 N개의 줄에는 드래곤 커브의 정보가 주어진다. 드래곤 커브의 정보는 네 정수 x, y, d, g로 이루어져 있다. x와 y는 드래곤 커

www.acmicpc.net

 

 

1. 풀이 소스:

 

import java.util.*;
import java.io.*;
import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = parseInt(br.readLine());
        
        DragonCurve curve = new DragonCurve();
        
        for (int i = 0; i < n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            curve.setMap(parseInt(st.nextToken()), parseInt(st.nextToken()),
                        parseInt(st.nextToken()), parseInt(st.nextToken()));
        }
        
        curve.build();
        System.out.println(curve.getSquare());
    }
}

class DragonCurve {
    boolean[][] map = new boolean[101][101]; // 0 ~ 100 까지 유효한 좌표로 드래곤 커브가 된 좌표
    List<Start> starts = new ArrayList<>(); // 드래곤 커브 시작점
    void setMap(int x, int y, int dir, int g) {
        starts.add(new Start(x, y, dir, g));
    }
    
    // 0: x++, 1: y--, 2: x--, 3: y++
    void build() {
        for (Start start : starts) {
            List<Point> nowPoints = new ArrayList<>(); // 드래곤 커브로 재귀 형태로 회전할 리스트
            nowPoints.add(new Point(start.x, start.y)); // 시작점 입력으로 추가
            map[start.x][start.y] = true; // 시작점 드래곤 커브에 true
            
            int referX = 0; // 90도 시계방향으로 회전시킬 기준점이 되는 x 좌표
            int referY = 0;
            
            switch (start.dir) {
                case 0:
                    referX = start.x + 1;
                    referY = start.y;
                    break;
                case 1:
                    referX = start.x;
                    referY = start.y - 1;
                    break;
                case 2:
                    referX = start.x - 1;
                    referY = start.y;
                    break;
                case 3:
                    referX = start.x;
                    referY = start.y + 1;
                    break;
            }
            map[referX][referY] = true;
            Point refer = new Point(referX, referY);
            nowPoints.add(refer);
            doCurve(1, start.g, nowPoints); // 재귀 실행
        }
    }
    
    // 특정 점을 기준으로 시계 방향 회전
    // x = a + (b - y)
    // y = b - (a - x)
    void doCurve(int k, int g, List<Point> nowPoints) { // 참조 #1
        if (k == g + 1) return; // 만약 드래곤 커브 세대가 끝이나면 종료 

        Point refer = nowPoints.get(nowPoints.size() - 1); // 마지막 입력은 모두 참조점이 됨 
        int prePointIdx = nowPoints.size() - 2; // 참조점을 제외하고 회전시킬 인덱스 (계속 리스트에 추가하므로 인트값을 설정 해야 함)
        for (int i = prePointIdx; i >= 0; i--) { // 내림차순 인덱스로 설정 참조 #2 
            Point p = nowPoints.get(i);
            int nextX = refer.x + (refer.y - p.y); // 공식 적용 
            int nextY = refer.y - (refer.x - p.x);
            map[nextX][nextY] = true;
            nowPoints.add(new Point(nextX, nextY)); // 다음 좌표 리스트에 추가 
        }

        doCurve(k + 1, g, nowPoints); // 재귀 실행 
    }
    
    int getSquare() {
        int result = 0;
        for (int row = 0; row < 100; row++) {
            for (int col = 0; col < 100; col++) {
                if (map[row][col] && map[row][col + 1] &&
                   map[row + 1][col] && map[row + 1][col + 1]) // 0 ~ 99까지 순회하며 4개의 좌표가 모두 true라면 정사각형 가능 
                    result++;
            }
        }
        return result;
    }
    
    static class Start {
        int x; // x 좌표 
        int y; // y 좌표 
        int dir; // 방향성 
        int g; // 세대 
        
        Start (int x, int y, int dir, int g) {
            this.x = x;
            this.y = y;
            this.dir = dir; 
            this.g = g;
        }
    } 
    
    static class Point {
        int x;
        int y;
        
        Point (int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
}

 

 

2. 풀이 중점 사항

 

참조#1

 

특정 좌표를 기준으로 90도 시계방향 회전은 행렬의 회전을 이용할 수 있습니다.

이전에 AI 공부할 때, 행렬 회전 공식을 암기했던 적이 있는데, 이번에 쓰이게 되어서 요긴하게 잘 쓸 수 있었던 것 같습니다.

 

출처: https://gaussian37.github.io/math-la-rotation_matrix/

예를 들어 (4, 2)가 회전해야 하는 점이고, 기준점이 (4, 1)이라면 다음의 공식으로 구할 수 있습니다.

  • sin90은, 1, 코사인90은 0입니다.
  • Xnew = 0 * (4 - 4) + -1 * (2 - 1) = -1, Ynew = 1 * (4 - 4)  + 0 * (2 - 1) = 0 인 2 by 1 행렬을 구할 수 있습니다.
  • x' = -1 + 4 = 3, y' = 0 + 1 = 1     
  • 행렬 계산으로 인해 두 행렬을 계산하면 (3, 1)을 구할 수 있습니다.

따라서 이를 일반화 처리하면 

int nextX = -(y - refer.y) + refer.x;
int nextY = (x - refer.x) + refer.y;

--------------------------

int nextX = refer.x + (refer.y - y);
int nextX = refer.y - (refer.x - x);

로 공식을 구할 수 있습니다.

 

 

참조#2

 

만약 처리해야하는 값이 리스트에

0, 1가 있습니다.

 

1은 참조점이므로 0만 회전하여 나온 값이 2라면,

0 -> 2 로 리스트에 담기게 됩니다.

현재 리스트: 0, 1, 2 

 

참조점 2를 제외하고 내림차순으로 순회를 하면,

 

회전을 해서 나온 결과가

1  -> 3,

0  -> 4라고 하겠습니다,

 

현재 리스트: 0, 1, 2, 3, 4

 

참조점 4를 제외하고 다시 회전을 진행하면 0, 1, 2, 3, 4, 5, 6, 7, 8

즉, 최종적으로 가장 마지막에 끝나는 인덱스는 처음 리스트 0에 의해 회전한 값이 설정되게 됩니다.

따라서, 내림차순으로 순회하되, 하나씩 리스트에 추가하고 항상 마지막에 추가된 값을 다시 참조점으로 설정할 수 있습니다.

 

Point refer = nowPoints.get(nowPoints.size() - 1); // 마지막 입력은 모두 참조점이 됨 
int prePointIdx = nowPoints.size() - 2; // 참조점을 제외하고 회전시킬 인덱스 (계속 리스트에 추가하므로 인트값을 설정 해야 함)
for (int i = prePointIdx; i >= 0; i--) { // 내림차순 인덱스로 설정 참조 #2 
    Point p = nowPoints.get(i);
    int nextX = refer.x + (refer.y - p.y); // 공식 적용 
    int nextY = refer.y - (refer.x - p.x);
    map[nextX][nextY] = true;
    nowPoints.add(new Point(nextX, nextY)); // 다음 좌표 리스트에 추가 
}

 

따라서, 참조점을 설정한 후, 참조점보다 하나 적은 인덱스부터 내림차순 하여 회전한 값을 다시 리스트에 넣는 방법을 적용할 수 있습니다.

 

재귀로 인해 세대보다 하나가 커질 때까지 계속 드래곤 커브를 생성할 수 있습니다.

 

이상으로 드래곤 커브 자바 풀이를 마치도록 하겠습니다.

감사합니다.!!

 

 

회전 행렬 자료 출처: https://gaussian37.github.io/math-la-rotation_matrix/

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 백준 SW 기출문제 사다리 조작 문제를 해결하는 과정을 작성하고자 합니다.

사다리 조작 문제는 해결법을 찾지 못하여 다른 블로그 분의 코드를 참조하였습니다.

 

문제 출처: https://www.acmicpc.net/problem/15684

 

15684번: 사다리 조작

사다리 게임은 N개의 세로선과 M개의 가로선으로 이루어져 있다. 인접한 세로선 사이에는 가로선을 놓을 수 있는데, 각각의 세로선마다 가로선을 놓을 수 있는 위치의 개수는 H이고, 모든 세로선

www.acmicpc.net

 

 

1. 풀이 소스:

 

import java.io.*;
import java.util.*;

import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        int n = parseInt(st.nextToken());
        int m = parseInt(st.nextToken());
        int h = parseInt(st.nextToken());

        Ladder ladder = new Ladder(n, h);
        for (int i = 0; i < m; i++) {
            st = new StringTokenizer(br.readLine());
            ladder.setMap(parseInt(st.nextToken()), parseInt(st.nextToken()));
        }

        ladder.start();
        System.out.println(ladder.getMinAddEdge());
    }
}

class Ladder {
    int n; // 열 개수
    int h; // 행 개수
    int[][] map;
    int minAddEdgeCnt = 4; // 최소 추가 설치 사다리 개수 (만약 4가 바뀌지 않는다면 -1)

    Ladder (int n, int h) {
        this.n = n;
        this.h = h;
        map = new int[h + 2][n + 2]; // (1, 1)  ~ (h ~ )이므로 h + 2
    }

    void setMap(int row, int col) {
        map[row][col] = 1;
    }

    void start() {
        for (int i = 0; i <= 3; i++) dfs(0, i, 1); // dfs로 추가 사다리가 0인 것부터 0 ~ 3까지 탐색
    }

    void dfs(int cnt, int limit, int r) {
        if (cnt > limit || cnt > minAddEdgeCnt) return; // 만약 추가한 사다리가 한계치 (0 ~ 3)으로 설정한 값보다 크거나 최대치를 넘는다면 종료

        if (isPossible()) {
            minAddEdgeCnt = Math.min(minAddEdgeCnt, cnt);
            return;
        }

        for (int row = r; row <= h; row++) { // 각 몇번째 행에서부터
            for (int col = 1; col <= n; col++) { // 1번열 (1, 1) ~ n개까지 (총 n개의 열)
                if (map[row][col] == 0 && map[row][col - 1] == 0 && map[row][col + 1] == 0) { // 참조 #1
                    map[row][col] = 1; //  현재 열 선택 가능
                    dfs(cnt + 1, limit, row); // 사다리를 하나 추가하여 이어서 dfs 진행
                    map[row][col] = 0; // dfs 종료 후 다시 이전에 설정한 1을 초기화
                }
            }
        }
    }

    boolean isPossible() {
        for (int col = 1; col <= n; col++) {
            int nowC = col; // 현재 선택한 열 번호
            int row = 1; // 행의 시작은 1
            while (true) {
                if (row > h) break; // 만약 h의 길이보다 크다면
                if (map[row][nowC] == 1) nowC++; // 1이라면 오른쪽으로 연결
                else if (map[row][nowC - 1] == 1) nowC--;// 이전항이 1이라면 이전항에서 연결한 것임
                row++; // 한번 이동했다면 그 열에서는 연속적으로 다음 열로 이동할 수 없기에 바로 내려갈 수 있음
            }

            if (nowC != col) return false; // 만약 열을 이동하고 최종적으로 마지막 행을 지나 도착했을 때, 이전 열과 다르다면
        }
        return true;
    }


    int getMinAddEdge() {
        return minAddEdgeCnt == 4 ? -1 : minAddEdgeCnt;
    }
}

 

 

2. 풀이 중점 사항

 

참조#1

 

이 문제는 사다리를 조작할 때, 0 ~ 3가지의 사다리를 어떻게 놓을 것인가가 관건이었습니다. 같은 행의 하나의 열 B는 인접한 A와 C열에 동시에 사다리가 놓일 수 없습니다. 

 

즉    A      B      C

        |  ---  |  --- | 

 

이러한 식으로, 사다리를 놓을 수 없습니다. 

 

if (map[row][col] == 0 && map[row][col - 1] == 0 && map[row][col + 1] == 0) { // 참조 #1

 

이 소스가 의미하는 바는 만약 col이 B라고 가정하면, B보다 열 인덱스가 하나 작은 A열과 인덱스가 하나 큰 C 열의 값이 모두 0일 때만 map[row][col] = 1로 사다리를 놓을 수 있습니다.

map[row][col] = 1 이 의미하는 바는, col 열과 인접한 col + 1번 열에 사다리를 놓겠다는 의미입니다.

 

if map[row][col - 1] == 1이라면 col - 1와  인접한 인덱스가 하나 큰 col에 사다리를 놓았다는 것을 의미하므로

같은 행에 하나의 열은 두 개의 사다리를 놓을 수 없다라는 원칙에 위배되게 됩니다.

 

반대로 만약

if map[row][col + 1] == 1 이라면, 이 것은 col + 1 열과 인접한 col + 2에 사다리를 놓겠다는 것을 의미합니다. 만약 map[row][col] 이 사다리를 놓게 된다면 col + 1은 이 사다리를 공유하게 되는데, 이미 col + 1과 col + 2가 사다리를 하나 공유하고 있으므로
이 또한, 같은 행에 하나의 열은 두 개의 사다리를 놓을 수 없다는 원칙에 위배되게 됩니다.

 

따라서, 자신의 열과 인접한 두 열을 함께 비교해야 합니다.

(그러므로 h + 2 개로 초기화 map을 설정한 것과 일맥 상통합니다)

 

 

참조 #2

 

boolean isPossible() { // 참조#2
    for (int col = 1; col <= n; col++) {
        int nowC = col; // 현재 선택한 열 번호
        int row = 1; // 행의 시작은 1
        while (true) {
            if (row > h) break; // 만약 h의 길이보다 크다면
            if (map[row][nowC] == 1) nowC++; // 1이라면 오른쪽으로 연결
            else if (map[row][nowC - 1] == 1) nowC--;// 이전항이 1이라면 이전항에서 연결한 것임
            row++; // 한번 이동했다면 그 열에서는 연속적으로 다음 열로 이동할 수 없기에 바로 내려갈 수 있음
        }

        if (nowC != col) return false; // 만약 열을 이동하고 최종적으로 마지막 행을 지나 도착했을 때, 이전 열과 다르다면
    }
    return true;
}

 

이 코드와 비교하여 제가 이전에 실수했던 부분은, row++를 else에 넣었다는 점입니다.

문제는 nowC는 컬럼의 이동을 의미하는데, 같은 행의 열은 두 개의 사다리를 가질 수 없습니다.

즉 한 번 좌 혹은 우로 이동했다면 혹은 하지 않았다면 이는 곧바로 행을 한 칸 내려가야 한다는 것을 의미합니다.

 

즉      A      B      C

 1       |  ---  |       | 

 2       |       |        |

 3       |       |        |

 

만약 1행 B에서 출발하였다면 A와 사다리가 연결되어 있어서 A 열로 이동하고 1행 1열에서 2행 1열로 내려야 하는 것입니다.

만약 내리지 않는다면, 무한 루프에 빠지게 됩니다.

 

자세한 코드는 주석으로 처리하였습니다.

이상으로 백준 SW 기출문제 사다리 조작 문제 풀이를 마치도록 하겠습니다.

감사합니다.!!!

 

참고 자료: https://suhyeokeee.tistory.com/119

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 백준 SW 기출문제 - 감시 자바 풀이를 진행하고자 합니다.

 

문제 출처: https://www.acmicpc.net/problem/15683

 

15683번: 감시

스타트링크의 사무실은 1×1크기의 정사각형으로 나누어져 있는 N×M 크기의 직사각형으로 나타낼 수 있다. 사무실에는 총 K개의 CCTV가 설치되어져 있는데, CCTV는 5가지 종류가 있다. 각 CCTV가 감

www.acmicpc.net

 

 

1. 풀이 소스:

 

import java.util.*;
import java.io.*;

import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        int n = parseInt(st.nextToken());
        int m = parseInt(st.nextToken());
        Cctv cctv = new Cctv(n, m); // 인스턴스 생성
        
        for (int row = 0; row < n; row++) {
            st = new StringTokenizer(br.readLine());
            for (int col = 0; col < m; col++)
                cctv.setMap(row, col, parseInt(st.nextToken()));
        }
        
        cctv.build();
        
        System.out.println(cctv.getMinNonSafeRegion());
    }
}

class Cctv {
    
    static final int[] DR = {-1, 0, 1, 0, -1, 0}; // -1, 0, 1, 0 에 추가로 -1, 0 을 넣은 이유 참조 #1
    static final int[] DC = {0, -1, 0, 1, 0, -1};
    
    int n; // row 총 개수
    int m; // col 총 개수
    int safeCnt; // 벽과 cctv 개수를 포함한 개수
    int maxSafeCnt; // 최고 안전 지대 개수
    int[][] map;
    List<Point> cctvs = new ArrayList<>(); // cctv를 저장할 리스트
    
    Cctv (int n, int m) {
        this.n = n;
        this.m = m;
        map = new int[n][m];
    }
    
    void setMap(int row, int col, int value) {
        map[row][col] = value;
        
        if (value != 0) safeCnt++; // 만약 cctv 혹은 벽이라면 안전지대 개수 추가
        if (value != 0 && value != 6) cctvs.add(new Point(row, col)); // cctv 추가
    }
    
    void build() {
        if (cctvs.isEmpty()) { // cctv가 없을 수 있으므로 safeCnt 바로 maxSafeCnt로 할당하고 종료
            maxSafeCnt = safeCnt;
            return;
        }

        boolean[][] nowVisited = new boolean[n][m]; // 방문 위치 설정
        int nowSafeCnt = safeCnt; // nowSafeCnt로 지역변수 설정 후 재귀
        dfs(0, nowSafeCnt, nowVisited);
    }

    // idx: cctv 리스트를 순회할 인덱스, preSafeCnt: 이전 재귀에서 얻은 안전지대 개수, preVisited: 이전 재귀에서 방문한 곳
    void dfs(int idx, int preSafeCnt, boolean[][] preVisited) {
        if (idx == cctvs.size()) { // 만약 모든 cctv를 전부 탐색했다면
            maxSafeCnt = Math.max(preSafeCnt, maxSafeCnt);
            return; // 최대값 업데이트 후 종료
        }
        Point p = cctvs.get(idx); // 인덱스로 cctv 포인트 가져오기
        int row = p.row;
        int col = p.col;
        int value = map[row][col];

        if (value == 1) { // 1번 시시티비라면
            for (int k = 0; k < 4; k++) { // 상 하 좌 우 한 번만 이동 가능
                int nowSafeCnt = preSafeCnt; // nowSafeCnt로 지역 변수 할당 참조#2
                boolean[][] nowVisited = getNowVisited(preVisited); // preVisted를 깊은 복사한 nowVisited 생성
                nowSafeCnt = viewToDir(row, col, DR[k], DC[k], nowSafeCnt, nowVisited); // while문으로 벽이 나올 때까지 탐색 참조#3
                dfs(idx + 1, nowSafeCnt, nowVisited); // 재귀 실행
            }
        }

        else if (value == 2) {
            for (int k = 0; k < 2; k++) { // 짝 i = 0, i = 2 // i = 1, i = 3
                int nowSafeCnt = preSafeCnt;
                boolean[][] nowVisited = getNowVisited(preVisited);
                for (int d = 0; d < 2; d++) // 2번은 '좌우' 혹은 '상하'가 짝이므로 이중 포문으로 짝이 나올 수 있도록 2d + k 설정
                    nowSafeCnt = viewToDir(row, col, DR[k + 2 * d], DC[k + 2 * d], nowSafeCnt, nowVisited);
                dfs(idx + 1, nowSafeCnt, nowVisited);
            }
        }

        else if (value == 3) {
            for (int k = 0; k < 4; k++) {
                int nowSafeCnt = preSafeCnt;
                boolean[][] nowVisited = getNowVisited(preVisited);
                for (int d = 0; d < 2; d++) // 3번은 'ㄱ' 이 나와야 하므로 k + d로 '상좌', '좌하', '하우', '우상' 설정
                    nowSafeCnt = viewToDir(row, col, DR[k + d], DC[k + d], nowSafeCnt, nowVisited);
                dfs(idx + 1, nowSafeCnt, nowVisited);
            }
        }

        else if (value == 4) {
            for (int k = 0; k < 4; k++) {
                int nowSafeCnt = preSafeCnt;
                boolean[][] nowVisited = getNowVisited(preVisited);
                for (int d = 0; d < 3; d++) // 4번은 'ㅜ'가 나와야 하므로 k + d로  '상하좌', '상하우', '좌우상' '좌우하' 설정
                    nowSafeCnt = viewToDir(row, col, DR[k + d], DC[k + d], nowSafeCnt, nowVisited);
                dfs(idx + 1, nowSafeCnt, nowVisited);
            }
        }

        else {
            int nowSafeCnt = preSafeCnt;
            boolean[][] nowVisited = getNowVisited(preVisited);
            for (int k = 0; k < 4; k++) // 5번은 상하좌우 모두 탐색해야하므로 단일 포문으로 모두 탐색하도록 진행
                nowSafeCnt = viewToDir(row, col, DR[k], DC[k], nowSafeCnt, nowVisited);
            dfs(idx + 1, nowSafeCnt, nowVisited);
        }
    }

    // 유효한 인덱스에 row, col의 map 값이 벽이 아니여야함 (cctv는 통과 가능)
    boolean isValid(int row, int col) {
        return row >= 0 && row < n && col >= 0 && col < m && map[row][col] != 6;
    }

    int viewToDir(int row, int col, int dr, int dc, int nowSafeCnt, boolean[][] nowVisited) {
        while(true) {
            row += dr; // row에 값 업데이트
            col += dc; // col에 값 업데이트

            if (!isValid(row, col)) break; // 만약 벽이거나 유효한 인덱스가 아니라면 종료
            if (!nowVisited[row][col] && map[row][col] == 0) { // 방문하지 않은 경우에만
                nowVisited[row][col] = true;
                nowSafeCnt++;
            }
        }
        return nowSafeCnt;
    }

    boolean[][] getNowVisited(boolean[][] preVisited) { // 깊은 복사 진행
        boolean[][] nowVisited = new boolean[n][m];
        for (int r = 0; r < n; r++) nowVisited[r] = preVisited[r].clone();
        return nowVisited;
    }
    
    int getMinNonSafeRegion() {
        return n * m - maxSafeCnt;
    }
        
    static class Point {
        int row;
        int col;
        
        Point (int row, int col) {
            this.row = row;
            this.col = col;
        }
    }
}

 

 

2. 풀이 중점 사항

 

해당 문제는 정답률을 보았을 때, 쉬운 문제처럼 느껴질 수 있었지만 굉장히 복잡하고 실수를 유발하는 문제였습니다.

문제 자체는 이해가 빠르게 되었지만 이걸 어떻게 구현해야 할까라는 고민을 많이 하였습니다. 코드를 작성하는 과정에도 "잘못 풀고 있나?"라는 생각을 하면서 해결했던 것 같습니다.

 

먼저 이 문제는 5가지의 cctv를 다른 방법으로 탐색시켜야 했습니다. 따라서, DR, DC를 -1, 0, 1, 0 으로 설정하지 않고 다르게 설정하였습니다.

 

 

참조#1

 

static final int[] DR = {-1, 0, 1, 0, -1, 0}; // -1, 0, 1, 0 에 추가로 -1, 0 을 넣은 이유 참조 #1
static final int[] DC = {0, -1, 0, 1, 0, -1};

 

DR, DC를 다음과 같이 6개의 값으로 초기화 한 이유는  아래의 예시처럼 각각 다른 CCTV가 보이는 방향을 하나의 static final 배열의 DR, DC로 설정하기 위함입니다.

 

if (value == 1) { // 1번 시시티비라면
    for (int k = 0; k < 4; k++) { // 상 하 좌 우 한 번만 이동 가능
        int nowSafeCnt = preSafeCnt; // nowSafeCnt로 지역 변수 할당 참조#2
        boolean[][] nowVisited = getNowVisited(preVisited); // preVisted를 깊은 복사한 nowVisited 생성
        nowSafeCnt = viewToDir(row, col, DR[k], DC[k], nowSafeCnt, nowVisited); // while문으로 벽이 나올 때까지 탐색 참조#3
        dfs(idx + 1, nowSafeCnt, nowVisited); // 재귀 실행
    }
}

else if (value == 2) {
    for (int k = 0; k < 2; k++) { // 짝 i = 0, i = 2 // i = 1, i = 3
        int nowSafeCnt = preSafeCnt;
        boolean[][] nowVisited = getNowVisited(preVisited);
        for (int d = 0; d < 2; d++) // 2번은 '좌우' 혹은 '상하'가 짝이므로 이중 포문으로 짝이 나올 수 있도록 2d + k 설정
            nowSafeCnt = viewToDir(row, col, DR[k + 2 * d], DC[k + 2 * d], nowSafeCnt, nowVisited);
        dfs(idx + 1, nowSafeCnt, nowVisited);
    }
}

 

예를 들어 cctv 번호가 1번이라면, 상 하 좌 우 하나의 방향으로만 이동이 가능합니다. 따라서 for(int k = 0 ~) 단일 포문으로  viewToDir로 탐색을 진행하고 dfs를 적용할 수 있습니다.

 

하지만 cctv 번호가 2번이라면, '좌우', '상하'가 하나의 묶음입니다. 따라서, 이중 for문으로 

row가 위, 아래  // col이 좌, 우로 움직일 수 있도록 k + 2 * d로 인덱스를 설정하였습니다.

 

그 결과, 만약 k = 0라면,

if d == 0: k + 2 * d = 0         ----> DR[0] = -1, DC[0] = 0

if d == 1: k + 2 * d = 2         ----> DR[2] = 1,  DC[2] = 0 

 

좌우로 움직일 수 있는 공식을 만들 수 있었습니다.

이와 같은 방법으로 3, 4, 5 를 처리하면 DR, DC 로 모든 경우를 처리할 수 있습니다.

 

 

참조 #2

 

else if (value == 2) {
    for (int k = 0; k < 2; k++) { // 짝 i = 0, i = 2 // i = 1, i = 3
        int nowSafeCnt = preSafeCnt;
        boolean[][] nowVisited = getNowVisited(preVisited);
        for (int d = 0; d < 2; d++) // 2번은 '좌우' 혹은 '상하'가 짝이므로 이중 포문으로 짝이 나올 수 있도록 2d + k 설정
            nowSafeCnt = viewToDir(row, col, DR[k + 2 * d], DC[k + 2 * d], nowSafeCnt, nowVisited);
        dfs(idx + 1, nowSafeCnt, nowVisited);
    }
}

 

이 코드를 살펴보면, nowSafeCnt로 지역변수를 바깥 for문 내에서 적용하는 것을 볼 수 있습니다. 

안쪽의 for (int d = 0 ~ )는 cctv가 탐색할 수 있는 방향의 묶음이므로

nowSafeCnt, nowVisited에 한 묶음이 모두 영향을 받아야 합니다.

 

하지만, k가 바뀐다면, 이는 곧 아예 탐색하는 방법을 바꾸는 것을 의미합니다.

즉, k = 0 이라면 좌우를 의미하는데, k = 1이라면 상하를 의미합니다.

 

따라서, 회전은 다른 경우의 수로 보아야 하기 때문에 독립성을 보장받아야 합니다. 이전의 nowSafeCnt는 영향을 주면 안 되므로 preSafeCnt를 재할당하고, preVisited를 깊은 복사 하여 for문의 이전 인덱스에 변경된 값이 영향을 주지 않도록 해야 합니다.

 

 

참조 #3

 

int viewToDir(int row, int col, int dr, int dc, int nowSafeCnt, boolean[][] nowVisited) {
    while(true) {
        row += dr; // row에 값 업데이트
        col += dc; // col에 값 업데이트

        if (!isValid(row, col)) break; // 만약 벽이거나 유효한 인덱스가 아니라면 종료
        if (!nowVisited[row][col] && map[row][col] == 0) { // 방문하지 않은 경우에만 참조#4
            nowVisited[row][col] = true;
            nowSafeCnt++;
        }
    }
    return nowSafeCnt;
}

 

row, col은 행과 열을 의미하고, dr dc는 DR, DC로 이동한 값을 의미합니다. 

while문으로 row, col을 업데이트하되, 만약 유효한 인덱스가 아니거나, 벽을 만난다면 break 하여 더 이상 방문 처리가 되지 않도록 합니다. 만약 아직 방문하지 않았고, map 값이 0이라면 nowSafeCnt를 1 추가합니다. 

 

여기서, 중요한 점은 '방문'이라는 것입니다. 방문하지 않은 경우에만 nowSafeCnt를 증가시키는 이유는 

5와 2가 인접한 경우  ### 5 2 ###   처럼 5 와 2는 같은 방향을 공유할 수 있기 때문입니다. 

따라서, 중복을 방지하기 위해 방문하지 않은 경우만 ++할 수 있도록 하였습니다.

 

 

이상으로 SW 기출문제 감시 - 자바 풀이를 마치도록 하겠습니다.

감사합니다.!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 백준 SW 기출문제 톱니바퀴 자바 풀이를 진행하고자 합니다.!

 

문제 출처: https://www.acmicpc.net/problem/14891

 

14891번: 톱니바퀴

첫째 줄에 1번 톱니바퀴의 상태, 둘째 줄에 2번 톱니바퀴의 상태, 셋째 줄에 3번 톱니바퀴의 상태, 넷째 줄에 4번 톱니바퀴의 상태가 주어진다. 상태는 8개의 정수로 이루어져 있고, 12시방향부터

www.acmicpc.net

 

 

1. 풀이 소스:

 

import java.util.*;
import java.io.*;

import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        Gear gear = new Gear();
        for (int i = 0; i < 4; i++) {
            String gearStatus = br.readLine();
            for (int j = 0; j < 8; j++) {
                gear.addStatus(i, j, gearStatus.charAt(j));
            }
        }

        int k = parseInt(br.readLine());
        for (int i = 0; i < k; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            gear.setRotate(parseInt(st.nextToken()), parseInt(st.nextToken()));
        }

        gear.run();

        System.out.println(gear.getScore());
    }
}

class Gear {

    int[][] map = new int[4][8];
    Queue<Rotate> rotates = new ArrayDeque<>();
    Gear(){}

    void addStatus(int row, int col, char status) {
        map[row][col] = Character.getNumericValue(status);
    }

    void setRotate(int number, int dir) {
        rotates.add(new Rotate(number - 1, dir)); // 번호가 1부터인데 인덱스는 0부터 설정
    }

    void run() { // 극 N = 0, S = 1  dir:  시계 = 1, 반시계 = -1
        while (!rotates.isEmpty()) {
            Rotate rt = rotates.poll();
            int number = rt.number;
            int dir = rt.dir;

            int two = map[number][2];
            int six = map[number][6];

            rotate(number, dir);

            if (number == 0) dfs(number + 1, two, dir, false); // 오른쪽으로 이동하면서 회전
            else if (number == 3) dfs(number - 1, six, dir, true); // 왼쪽으로 이동
            else { // 1, 2는 양쪽 방향으로 진행
                dfs(number + 1, two, dir, false);
                dfs(number - 1, six, dir, true);
            }
        }
    }

    void rotate(int number, int dir) { // 극 N = 0, S = 1  dir:  시계 = 1, 반시계 = -1
        int[] tmp = map[number].clone(); // 복사 배열
        if (dir == 1) { // 시계
            // 이전항이 자신의 항
            System.arraycopy(tmp, 0, map[number], 1, 7);
            map[number][0] = tmp[7];
        } else { // 반시계
            // 다음 항이 자신의 항
            System.arraycopy(tmp, 1, map[number], 0, 7);
            map[number][7] = tmp[0];
        }
    }

    // 극이 달라야 회전
    void dfs(int nowNum, int prePole, int preDir, boolean toLeft) {
        if (nowNum == -1 || nowNum == 4) return;
        int nowDir = preDir == 1 ? -1 : 1; // 역방향

        int two = map[nowNum][2]; // 회전 시키기 전 고정 값
        int six = map[nowNum][6];

        if (toLeft) { // 영향 주는 곳은 6, 받는 곳은 2
            if (prePole == two) return; // 극이 같기에 더 이상 진행 x

            rotate(nowNum, nowDir); // 극이 다르다면 방향과 다르게 진행 회전
            dfs(nowNum - 1, six, nowDir, true); // 번호를 낮춰서, 다음 재귀

        } else { // 오른쪽으로 이동시 영향 주는 곳은 2, 받는 곳 6
            if (prePole == six) return; // 극이 같기에 더 이상 진행 x

            rotate(nowNum, nowDir);
            dfs(nowNum + 1, two, nowDir, false);
        }
    }

    int getScore() {  // 극 N = 0, S = 1  dir:  시계 = 1, 반시계 = -1
        int score = 0;

        score += map[0][0] == 0 ? 0 : 1;
        score += map[1][0] == 0 ? 0 : 2;
        score += map[2][0] == 0 ? 0 : 4;
        score += map[3][0] == 0 ? 0 : 8;

        return score;
    }

    class Rotate {
        int number;
        int dir;

        Rotate (int number, int dir) {
            this.number = number;
            this.dir = dir;
        }
    }
}

 

 

2.  풀이 중점 사항

 

해당 문제는 재귀를 활용하여 톱니바퀴 회전 가능 여부를 확인하고 회전하는 방법으로 문제를 해결할 수 있습니다.

0, 3의 인덱스를 가진 톱니바퀴(문제 1번,  4번 톱니바퀴)는 각 단방향으로만 영향을 줄 수 있습니다.

  •  인덱스 0번 톱니바퀴는 오른쪽 톱니바퀴에 영향
  •  인덱스 3번 톱니바퀴는 왼쪽 톱니바퀴에 영향
  •  인덱스 1번, 인덱스 2번 톱니바퀴는 왼쪽, 오른쪽 각각 모두 영향을 줍니다.

 

이 영향성을 가지고 문제 해결 전략을 세울 수 있습니다.

  • 먼저, 회전할 톱니바퀴 인덱스와 방향을 큐에 넣습니다.
  • 큐가 빌 때 까지 회전을 진행합니다.
  • 큐에서 나온 톱니바퀴는 무조건 회전을 1회 진행합니다.
  • 큐에서 나온 번호를 토대로 위에서 정의한 방법으로 재귀를 진행합니다.
  • 중요한 점은 톱니바퀴에 영향을 주는 극은 회전 이전의 극입니다. 즉, 회전 이전에 영향을 주는 2번 인덱스와 6번 인덱스의 번호를 가진 톱니바퀴의 칸을 미리 변수로 설정한 후, 회전 이후의 값이 영향을 주지 않도록 합니다.
  • 만약 왼쪽으로 톱니바퀴에 영향을 주는 재귀라면 이전 톱니바퀴의 6번 칸이 현재 톱니바퀴의 2번과 같은지 여부를 판단하고 회전을 시행합니다.
  • 만약 오른쪽 톱니바퀴에 영향을 주는 재귀라면 이전 톱니바퀴의 2번 칸이 현재 톱니바퀴의 6번과 같은지 여부를 판단하고 회전을 시행합니다.
  • 만약 인덱스가 -1 혹은 인덱스 4라면 재귀를 멈추고, 이전 톱니바퀴의 칸의 극과 현재 톱니바퀴의 칸 극이 같다면 재귀를 종료합니다.

 

 

rotate 메서드

 

void rotate(int number, int dir) { // 극 N = 0, S = 1  dir:  시계 = 1, 반시계 = -1
    int[] tmp = map[number].clone(); // 복사 배열
    if (dir == 1) { // 시계
        // 이전항이 자신의 항
        System.arraycopy(tmp, 0, map[number], 1, 7);
        map[number][0] = tmp[7];
    } else { // 반시계
        // 다음 항이 자신의 항
        System.arraycopy(tmp, 1, map[number], 0, 7);
        map[number][7] = tmp[0];
    }
}

 

회전할 때 중요한 점은 시계와 반시계 방향으로 회전할 때 이전 항 혹은 다음 항을 기억할 tmp 배열을 생성해야 합니다.

자바에서는 System.arraycopy() 메서드로 해결할 수 있습니다.

  

System.arraycopy(복사할 배열, 복사할 배열의 시작점, 복사될 배열, 복사될 배열의 시작점, 총 복사할 개수);

 

시계 방향의 경우 -> 방향이기 때문에 복사할 배열의 시작점은 0이 됩니다.

복사될 배열인 map[number]는 시작점이 1입니다.

즉, 0번의 인덱스 값이 1에 복사된다는 의미이므로 시계 방향 회전을 수행할 수 있고 복사할 개수가 7이므로

map[number][0]은 tmp[7]로 원형 시계를 설정할 수 있습니다.

 

 

dfs 메서드

 

 void dfs(int nowNum, int prePole, int preDir, boolean toLeft) {
    if (nowNum == -1 || nowNum == 4) return;
    int nowDir = preDir == 1 ? -1 : 1; // 역방향

    int two = map[nowNum][2]; // 회전 시키기 전 고정 값
    int six = map[nowNum][6];

    if (toLeft) { // 영향 주는 곳은 6, 받는 곳은 2
        if (prePole == two) return; // 극이 같기에 더 이상 진행 x

        rotate(nowNum, nowDir); // 극이 다르다면 방향과 다르게 진행 회전
        dfs(nowNum - 1, six, nowDir, true); // 번호를 낮춰서, 다음 재귀

    } else { // 오른쪽으로 이동시 영향 주는 곳은 2, 받는 곳 6
        if (prePole == six) return; // 극이 같기에 더 이상 진행 x

        rotate(nowNum, nowDir);
        dfs(nowNum + 1, two, nowDir, false);
    }
}

 

각 톱니바퀴는 회전 이전의 값을 반드시 가지고 있어야 합니다. 즉, 영향을 줄 수 있는 톱니바퀴가 회전한 결과를 넘겨준다면 문제에 위배되게 됩니다.

 

이를 구현하기 위해 two, six로 회전 이전의 값을 저장했습니다.

만약, toLeft == true로 왼쪽 방향으로 이동하는 것이라고 가정한다면,

 

먼저 prePole == two의 극을 비교합니다. prePole은 맞대는 이전 톱니바퀴의 6번 칸의 극을 의미합니다 (왼쪽으로 이동하므로 현재 톱니바퀴의 이전 톱니바퀴는 오른쪽에 위치합니다.)

 

두 극이 다르다면, 회전을 수행합니다.

 

왼쪽 방향성을 그대로 유지하면서, 톱니바퀴 번호는 하나 낮추고, 회전 이전의 값인 map[number][6], 현재 회전한 방향을 파라미터로 설정하여 재귀를 진행합니다.

 

이렇게 함으로써 다음 재귀가 실행될 때는, prePole을 받았을 때, 회전 이전의 값을 받을 수 있습니다.

 

 

이상으로 톱니바퀴 자바 풀이를 마치도록 하겠습니다. 감사합니다.!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 백준 경사로 자바 풀이를 진행하고자 합니다.

 

문제 출처: https://www.acmicpc.net/problem/14890

 

14890번: 경사로

첫째 줄에 N (2 ≤ N ≤ 100)과 L (1 ≤ L ≤ N)이 주어진다. 둘째 줄부터 N개의 줄에 지도가 주어진다. 각 칸의 높이는 10보다 작거나 같은 자연수이다.

www.acmicpc.net

 

 

1. 풀이 소스

 

import java.util.*;
import java.io.*;
import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        int N = parseInt(st.nextToken());
        int L = parseInt(st.nextToken());

        PathMap path = new PathMap(N, L);

        for (int row = 0; row < N; row++) {
            st = new StringTokenizer(br.readLine());
            for (int col = 0; col < N; col++) {
                path.setMap(row, col, parseInt(st.nextToken()));
            }
        }

        System.out.println(path.calculatePossiblePath());
    }
}

class PathMap {
    int n; // n개
    int len; // 경사로 길이
    int[][] mapR; // 행 * 열
    int[][] mapC; // 열 * 행

    PathMap (int n, int len) {
        this.n = n;
        this.len = len;
        mapR = new int[n][n];
        mapC = new int[n][n];
    }

    void setMap(int row, int col, int value) {
        mapR[row][col] = value;
        mapC[col][row] = value;
    }

    int calculatePossiblePath() {
        int possiblePath = 0; // 최종 답

        for (int row = 0; row < n; row++) {
            if (isPossiblePath(mapR[row])) possiblePath++;
            if (isPossiblePath(mapC[row])) possiblePath++;
        }

        return possiblePath;
    }

    boolean isPossiblePath(int[] map) {
        int preH = map[0]; // 기준이 되는 높이
        int cnt = 1; // 경사로를 놓을 수 있는 길이 (초기화 1)

        for (int col = 1; col < n; col++) {
            if (preH == map[col]) cnt++; // 높이가 같다면 cnt 증가 (혹시 더 높은 높이가 나타나면 cnt로 판별)

            else if (preH + 1 == map[col]) { // 만약 이전보다 크다면
                if (cnt < len) return false; // 경사로를 놓을 수 있는 길이보다 짧다면
                preH++; // 기준점 업데이트 (높이 한칸 더 높게)
                cnt = 1; // 길이 초기화 (기준점은 아직 사다리를 안놓았으므로)
            }

            else if (preH - 1 == map[col]){ // 이전 높이가 더 크다면

                int nowCnt = 1;
                int nowH = map[col]; // 현재 높이를 기준으로 len개까지 같은 높이가 나와야 함

                for (int k = col + 1; k < n; k++) { // 다음 행 부터 개수 세기
                    if (nowH != map[k]) break; // 현재 높이와 같지 않은 경우
                    nowCnt++;
                    if (nowCnt >= len) break; // 경사로를 놓을 수 있는 길이 이상이 되면
                }
                if (nowCnt < len) return false; // 이보다 적은 경우 종료

                col += (len - 1); // col을 포함한 열부터 경사로를 놓으므로
                cnt = 0; // 새로 시작하는 지점은 이미 경사로가 놓여있으므로
                preH = nowH;
            }

            else return false;
        }
        return true;
    }
}

 

 

2. 풀이 중점 사항

 

처음에 문제를 접근할 때, 경사로를 놓게 되면 계속 유지되는 것이라고 판단하여 많은 시간을 소모하였습니다.

이후 문제를 다시 읽고 가능한 예시를 자세하게 보니 가능한 경우의 수 이므로 한 행 혹은 한 열 단위로 경사로를 놓을 수 있고,

놓은 사다리는 다른 행, 열에 영향을 주지 않는 것이었습니다. 따라서, visited와 같은 체크 변수는 필요하지 않았습니다. 

 

중요한 사항은 경사로를 설정하는 과정에서 이전 높이가 더 큰지 혹은 더 작은지, 같은지에 따라 다르게 분기를 처리해야 했습니다.

 

가능한 경우는 

  • 이전 높이와 같음
  • 이전 높이보다 1 큼
  • 이전 높이보다 1 작음
  • 그 외 모두 불가

 

이전 높이와 같음

이전 높이와 같다면, cnt++를 하여 혹여 이전 높이보다 1 크게 되는 상황에 대비해야 합니다.

 

이전 높이보다 1 큼

예를 들어 len = 3이고 높이가 3 3 3 4 라면

이전 높이가 같은 것이 3개가 있고 (cnt = 3) len과 같으므로 경사로를 놓을 수 있음을 의미합니다. 

 

preH++; cnt = 1; 를 설정하는 것은 현재 높이가 4이므로 이전 높이가 3인 것보다 1 크므로 업데이트를 하는 과정입니다.

cnt = 1은 경사로가 놓인 위치는 3 3 3입니다. 한 칸 높은 곳은 경사로가 놓이지 않으므로  해당 위치는 경사로를 놓을 수 있는 유효한 칸이기 때문에 cnt++를 합니다.

 

이전 높이보다 1 작음

이전 높이보다 1이 작은 경우는, 현재 위치한 높이를 nowH로 설정하여

for 문으로 col을 nowH와 다를 때까지 이동하여야 합니다.

 

else if (preH - 1 == map[col]){ // 이전 높이가 더 크다면

    int nowCnt = 1;
    int nowH = map[col]; // 현재 높이를 기준으로 len개까지 같은 높이가 나와야 함

    for (int k = col + 1; k < n; k++) { // 다음 행 부터 개수 세기
        if (nowH != map[k]) break; // 현재 높이와 같지 않은 경우
        nowCnt++;
        if (nowCnt >= len) break; // 경사로를 놓을 수 있는 길이 이상이 되면
    }
    if (nowCnt < len) return false; // 이보다 적은 경우 종료

    col += (len - 1); // col을 포함한 열부터 경사로를 놓으므로
    cnt = 0; // 새로 시작하는 지점은 이미 경사로가 놓여있으므로
    preH = nowH;
}

 

여기서 nowCnt가 len 이상이 되면 break를 하여 더 이상 탐색되지 않도록 합니다.

col += (len - 1)을 하는 이유는 다음과 같습니다.

 

만약 len = 3이고, 인덱스는 0 1 2 3 이고, 해당 높이는 4 3 3 3입니다.

인덱스 1이 인덱스 0인 높이 4보다 1 작으므로 인덱스 3까지 탐색하였고

nowCnt == len 이여서 경사로를 놓았다고 가정하겠습니다.

 

이 경우 4 경 경 경 이 됩니다.

col은 기존에 인덱스 1이었고, 인덱스 3까지 경사로를 놓게 됩니다. 

이때, 기존 높이는 preH가 nowH로 업데이트하고 인덱스 3까지 경사로를 놓았으므로  인덱스 3은 더 이상 경사로를 

놓을 수 없으므로 0으로 cnt를 초기화하게 되는 것입니다.!

 

나머지 자세한 풀이는 주석화 하였습니다.

이상으로 경사로 자바 풀이를 마치도록 하겠습니다. 감사합니다.!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 백준 SW 기출문제 - 로봇 청소기 자바 풀이를 진행하고자 합니다.

 

문제 출처: https://www.acmicpc.net/problem/14503

 

14503번: 로봇 청소기

첫째 줄에 방의 크기 $N$과 $M$이 입력된다. $(3 \le N, M \le 50)$  둘째 줄에 처음에 로봇 청소기가 있는 칸의 좌표 $(r, c)$와 처음에 로봇 청소기가 바라보는 방향 $d$가 입력된다. $d$가 $0$인 경우 북쪽

www.acmicpc.net

 

 

1. 풀이 소스

 

import java.util.*;
import java.io.*;

import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        StringTokenizer st = new StringTokenizer(br.readLine());

        int n = parseInt(st.nextToken());
        int m = parseInt(st.nextToken());

        st = new StringTokenizer(br.readLine());

        int r = parseInt(st.nextToken());
        int c = parseInt(st.nextToken());
        int dir = parseInt(st.nextToken());

        RobotCleaner robot = new RobotCleaner(n, m);
        robot.setPoint(r, c, dir); // 처음 위치 및 방향 초기화

        for (int row = 0; row < n; row++) {
            st = new StringTokenizer(br.readLine());
            for (int col = 0; col < m; col++) {
                robot.setMap(row, col, parseInt(st.nextToken()));
            }
        }

        robot.run(); // 작동 시작

        System.out.println(robot.getCleanRoom());

    }
}

class RobotCleaner {

    static final int[] DR = {-1, 0, 1, 0};
    static final int[] DC = {0, -1, 0, 1};

    int n; // 행 개수
    int m; // 열 개수
    int[][] map; // 벽 혹은 이동 가능 영역 표시
    Point point; // 현재 위치
    int nowDir; // 북: 0, 동: 1, 남: 2,  서: 3
    int cleanRoom; // 총 청소한 구역
    boolean[][] visited; // 청소한 지역 (방문한다면 청소 해야 함)

    RobotCleaner (int n, int m) {
        this.n = n;
        this.m = m;
        map = new int[n][m];
        visited = new boolean[n][m];
    }

    void setPoint(int row, int col, int dir) {
        point = new Point(row, col);
        nowDir = dir;
    }

    void setMap(int row, int col, int value) {
        map[row][col] = value;
    }

    void run() {
        while(true) {

            int row = point.row; // point가 변경될 수 있으므로 임시 row
            int col = point.col; // 임시 col

            // 움직일 수 있고, 방문을 안했다면(청소가 가능하다면) <1>
            if (map[row][col] == 0 && !visited[row][col]) {
                cleanRoom++;
                visited[row][col] = true; // 방문 처리
            }

            boolean isNotCleanRoom = false; // 방문 가능한 곳 체크
            for (int i = 0; i < 4; i++) {
                int possibleR = row + DR[i];
                int possibleC = col + DC[i];

                if (!isValid(possibleR, possibleC)) continue; // 불가능한 곳

                isNotCleanRoom = true; // 청소할 곳 있음 <2> // isNotCleanRoom
                break;
            }

            if (isNotCleanRoom) {
                rotate(); // 회전하기

                //북: 0, 동: 1, 남: 2,  서: 3 이동하기
                if (nowDir == 0) row--; // 북 방향에서 한 칸 앞은 행 기준 --
                else if (nowDir == 1) col++; // 동 방향(오른쪽) 한 칸 앞
                else if (nowDir == 2) row++; // 남 기준 앞 row++
                else  col--; // 서 방향 기준 앞은 왼쪽

                if (!isValid(row, col)) continue;
                point.row = row;// 가능한 경우 업데이트
                point.col = col;
            }

            else { // 현재 칸의 주변 중 청소되지 않은 빈 칸이 없는 경우 <2>

                //북: 0, 동: 1, 남: 2,  서: 3
                if (nowDir == 0) row++; // 북 방향에서 한 칸 뒤는 남쪽으로
                else if (nowDir == 1) col--; // 동 방향(오른쪽) 한 칸 뒤 왼쪽
                else if (nowDir == 2) row--;
                else  col++;

                if (!isPossibleBackMove(row, col)) return; // 후진 불가능
                point.row = row; // 후진 가능하다면 위치 업데이트 후 while문 돌아가기
                point.col = col;
            }
        }
    }

    boolean isValid(int row, int col) { // 유효한 범위 && 이동 가능하고 && 청소하지 않음
        return row >= 0 && row < n && col >= 0 && col < m &&
                map[row][col] == 0 && !visited[row][col];
    }

    boolean isPossibleBackMove(int row, int col) { // 유효한 범위 && 후진하는 곳은 벽이 아니여야 함
        return row >= 0 && row < n && col >= 0 && col < m &&
                map[row][col] == 0;
    }

    void rotate() {
        switch (nowDir) { // 북: 0, 동: 1, 남: 2,  서: 3 
            case 0:
                nowDir = 3; // 북 -> 서
                break;
            case 1:
                nowDir = 0; // 동 -> 북
                break;
            case 2:
                nowDir = 1; // 남 -> 동
                break;
            case 3:
                nowDir = 2; // 서 -> 남
                break;
        }
    }

    int getCleanRoom() {
        return cleanRoom;
    }

    class Point {
        int row;
        int col;

        Point (int row, int col) {
            this.row = row;
            this.col = col;
        }
    }
}

 

 

2. 풀이 중점 사항

 

작동 사항 면밀하게 분석하기

 

해당 문제는 어떻게 코드를 작성해야 하는지 의사 코드를 명확하게 제시하고 있습니다. 따라서, 1 -> 2 or 3으로 반복적인 작업이 수행될 수 있도록 while 문 내부에서 적절한 분기 처리를 진행해야 합니다.

 

저는 2번 혹은 3번으로 분기가 처리되도록 하기 위해 isNotCleanRoom이라는 변수를 설정하였습니다.

총 4개의 상, 하, 좌, 우 에서 isValid() 메서드를 진행한 후, 만약 청소 가능한 구역이 하나라도 있다면 for문에서 빠져나와 isNotCleanRoom == true인 if 문으로 들어갑니다. 이후 회전을 진행하고 전진 여부를 판단하고 while문으로 돌아갑니다.

만약, 청소 가능한 구역이 없다면 isNotCleanRoom == false가 되어 후진 여부를 판단하여 while을 진행하거나 혹은 while을 종료합니다. 

 

그 외 부분은 자세한 주석으로 처리하였습니다.!

 

이상으로 로봇 청소기 자바 풀이를 마치도록 하겠습니다. 감사합니다.!!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 백준 SW 기출문제 - 연구소 자바 풀이를 진행하도록 하고자 합니다.

 

문제 출처: https://www.acmicpc.net/problem/14502

 

14502번: 연구소

인체에 치명적인 바이러스를 연구하던 연구소에서 바이러스가 유출되었다. 다행히 바이러스는 아직 퍼지지 않았고, 바이러스의 확산을 막기 위해서 연구소에 벽을 세우려고 한다. 연구소는 크

www.acmicpc.net

 

 

1. 풀이 소스 :

 

import java.util.*;
import java.io.*;

import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        int n = parseInt(st.nextToken());
        int m = parseInt(st.nextToken());
        
        Lab lab = new Lab(n, m);
        
        for (int row = 0; row < n; row++) {
            st = new StringTokenizer(br.readLine());
            for (int col = 0; col < m; col++)
                lab.setMap(row, col, parseInt(st.nextToken()));
        }
        
        lab.buildWall();
        System.out.println(lab.getSafeRegion());
    }
}

class Lab {
    
    static final int[] DR = {-1, 0, 1, 0};
    static final int[] DC = {0, -1, 0, 1};    
    
    int n; // row의 총 개수
    int m; // col의 총개수
    int minVirusCnt = Integer.MAX_VALUE; // 최소 바이러스 확진
    int virusCnt; // dfs로 실행된 각 조합에 바이러스 확진
    int[][] map; // 맵
    int wallsCnt = 3; // 벽의 개수 (무조건 3개를 세울 수 있으므로 초기화 3)
    
    boolean[][] visited; // 방문 여부
    List<Point> virus = new ArrayList<>(); // 바이러스 저장 리스트
    Combination combi; // 벽이 생성될 수 있는 조합
    
    Lab (int n, int m) {
        this.n = n;
        this.m = m;
        map = new int[n][m];
        visited = new boolean[n][m];
        combi = new Combination();
    }
    
    void setMap(int row, int col, int value) {
        map[row][col] = value;
        if (value == 0) combi.setInputs(new Point(row, col)); // 벽을 세울 수 있는 곳이라면 조합에 input 추가
        else if (value == 1) wallsCnt++; // 벽 개수 추가
        else virus.add(new Point(row, col)); // 바이러스 point 추가
    }
    
    void buildWall() {
        List<List<Point>> possibleCombi = combi.getCombination(); // 벽을 세울 수 있는 조합 생성
        
        for (List<Point> combis : possibleCombi) {
            for (Point point : combis) { // 각 조합에 있는 3가지 벽의 위치 하나씩
                map[point.row][point.col] = 1; // 벽 세우기
            }
            
            virusCnt = 0; // 각 조합 당 몇 개의 바이러스가 확진 되는지
            for (Point v : virus) {
                dfs(v.row, v.col); // dfs
            }
            
            minVirusCnt = Math.min(minVirusCnt, virusCnt); // 최소 바이러스 설정

            for (int row = 0; row < n; row++) {
                for (int col = 0; col < m; col++)
                    visited[row][col] = false; // 방문여부 다시 초기화
            }
            
            for (Point point : combis) {
                map[point.row][point.col] = 0; // 다시 벽 해제
            }
        }
    }
    
    void dfs(int row, int col) {
        
        visited[row][col] = true; // 방문 설정
        virusCnt++; // 바이러스 개수 증가
        
        for (int i = 0; i < 4; i++) {
            int nextR = row + DR[i]; // 다음 행
            int nextC = col + DC[i]; // 다음 열
            
            if (!isValid(nextR, nextC)) continue; // 이미 방문 혹은 인덱스 벗어났거나, 확진 가능한 곳이 아니라면
            dfs(nextR, nextC); // dfs
        }
    }
    
    int getSafeRegion() {
        return (n * m) - wallsCnt - minVirusCnt; // 총 안전 지대 = 총 장소 - 벽 - 확진자
    }
    
    boolean isValid(int row, int col) {
        return row >= 0 && row < n && col >= 0 && col < m && !visited[row][col] &&
            map[row][col] == 0; // 유효한 인덱스 && 방문 X && 감염 안된 사람들
    }
    
    static class Point {
        int row;
        int col;
        Point (int row, int col) {
            this.row = row;
            this.col = col;
        }
    }
    
    static class Combination { // 조합 생성
        List<Point> inputs = new ArrayList<>(); // 조합에 사용될 인풋
        List<List<Point>> combi = new ArrayList<>(); // 최종 조합

        Combination () {}

        void setInputs(Point point) {
            inputs.add(point); // 리스트에 추가
        }

        List<List<Point>> getCombination() {
            getCombinationHelper(3, 0, new ArrayList<>()); // 조합 재귀 실행
            return combi;
        }

        void getCombinationHelper(int k, int start, List<Point> current) {
            if (k == 0) {
                combi.add(new ArrayList<>(current)); // 깊은 복사로 combi에 추가
                return;
            }
            
            for (int i = start; i < inputs.size(); i++) {
                current.add(inputs.get(i)); // input에 있는 값 조합에 추가
                getCombinationHelper(k - 1, i + 1, current); // 가능한 개수 줄이고, 다음 번 조합은 i를 증가 시켜서 실행
                current.remove(current.size() - 1); // [1, 2]이 조합에 있는 경우 다음 조합에 영향을 주지 않기 위해 [1]로 설정하여 다음 조합 실행
            }
        }
    }
}

 

 

2. 풀이 중점 사항

 

해당 문제는 안전 지대를 최대한 늘릴 수 있는 벽 3개를 짓고, 최대 안전지대 개수를 출력하는 문제입니다.

따라서, 벽을 지을 수 있는 곳 3가지의 조합을 생성한 후, 하나씩 dfs를 진행하여 최대 안전지대 개수를 구하는 방법으로 진행하였습니다.

 

조합

 

조합은 재귀로 생성할 수 있습니다. 3가지의 벽을 세워야 하는데, 이미 벽이 있거나 확진자가 존재하는 곳이 있기 때문에,
벽을 지을 수 있는 Point를 인풋으로 설정하여 List에 넣은 후, 각 리스트에 있는 원소를 재귀로 탐색하여 3가지의 배열을 설정하는 Combination 클래스를 static으로 선언하였습니다.

 

 

static class Combination { // 조합 생성 
    List<Point> inputs = new ArrayList<>(); // 조합에 사용될 인풋 
    List<List<Point>> combi = new ArrayList<>(); // 최종 조합 

    Combination () {}

    void setInputs(Point point) {
        inputs.add(point); // 리스트에 추가 
    }

    List<List<Point>> getCombination() {
        getCombinationHelper(3, 0, new ArrayList<>()); // 조합 재귀 실행 
        return combi;
    }

    void getCombinationHelper(int k, int start, List<Point> current) {
        if (k == 0) {
            combi.add(new ArrayList<>(current)); // 깊은 복사로 combi에 추가 
            return;
        }

        for (int i = start; i < inputs.size(); i++) {
            current.add(inputs.get(i)); // input에 있는 값 조합에 추가 
            getCombinationHelper(k - 1, i + 1, current); // 가능한 개수 줄이고, 다음 번 조합은 i를 증가 시켜서 실행 
            current.remove(current.size() - 1); // [1, 2]이 조합에 있는 경우 다음 조합에 영향을 주지 않기 위해 [1]로 설정하여 다음 조합 실행 
        }
    }
}

 

중점 사항은, 이전 블록 퍼즐 문제와 비슷한 방법으로 재귀로 하나의 원소를 더한 후, 재귀 이후 다시 원소를 하나 제거해야 하는 것입니다.

이 코드는 다음의 순서로 진행됩니다.

 

[1, 2] -> [1, 2, 3] , [1, 2, 4](return) ...... -> getCombinationHelper() 종료 -> current.remove(current.size() - 1)

==> [1] 

 

다음 i++로 인해 inputs.get(i) = 3

[1, 3]

 

따라서, 각 원소에 대한 모든 조합을 구할 수 있습니다.

 

DFS 빌드 업

 

void buildWall() {
    List<List<Point>> possibleCombi = combi.getCombination(); // 벽을 세울 수 있는 조합 생성

    for (List<Point> combis : possibleCombi) {
        for (Point point : combis) { // 각 조합에 있는 3가지 벽의 위치 하나씩 
            map[point.row][point.col] = 1; // 벽 세우기
        }

        virusCnt = 0; // 각 조합 당 몇 개의 바이러스가 확진 되는지 
        for (Point v : virus) {
            dfs(v.row, v.col); // dfs
        }

        minVirusCnt = Math.min(minVirusCnt, virusCnt); // 최소 바이러스 설정 

        for (int row = 0; row < n; row++) {
            for (int col = 0; col < m; col++)
                visited[row][col] = false; // 방문여부 다시 초기화 
        }

        for (Point point : combis) {
            map[point.row][point.col] = 0; // 다시 벽 해제
        }
    }
}

 

각 DFS 결과는 독립적으로 실행되어야 합니다. 예를 들어, 3가지의 조합 결과가 다음 3가지의 조합 결과에 영향을 주면 안 됩니다. 따라서, 모든 방문을 끝낸 이후, 최소 바이러스 확진 수를 구한 후, 방문 처리를 초기화하였습니다.

그리고 다시 벽을 해제하여, 다음 벽이 세워질 때 영향을 주지 않도록 하였습니다.

 

DFS

 

void dfs(int row, int col) {

    visited[row][col] = true; // 방문 설정 
    virusCnt++; // 바이러스 개수 증가 

    for (int i = 0; i < 4; i++) {
        int nextR = row + DR[i]; // 다음 행 
        int nextC = col + DC[i]; // 다음 열 

        if (!isValid(nextR, nextC)) continue; // 이미 방문 혹은 인덱스 벗어났거나, 확진 가능한 곳이 아니라면 
        dfs(nextR, nextC); // dfs 
    }
}

virusCnt는 인스턴스 변수로 설정되어 있기 때문에 각 dfs가 진행되더라도 전역적으로 공유하여 사용될 수 있습니다.

유효한 범위이고 방문하지 않았으며 확진 가능한 곳 (map[r][c] = 0)인 곳만 다음 dfs로 탐색을 진행할 수 있습니다.

나머지 자세한 부분은 주석화 하였습니다. 

 

이상으로 SW 기출문제 연구소 풀이를 마치도록 하겠습니다. 감사합니다.

 

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 백준 SW 기출문제 테트로미노 자바 풀이를 작성하고자 합니다.

 

문제 출처: https://www.acmicpc.net/problem/14500

 

14500번: 테트로미노

폴리오미노란 크기가 1×1인 정사각형을 여러 개 이어서 붙인 도형이며, 다음과 같은 조건을 만족해야 한다. 정사각형은 서로 겹치면 안 된다. 도형은 모두 연결되어 있어야 한다. 정사각형의 변

www.acmicpc.net

 

 

1. 풀이 소스

 

import java.util.*;
import java.io.*;

import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = parseInt(st.nextToken());
        int m = parseInt(st.nextToken());
        
        Board board = new Board(n, m);
        
        for (int row = 0; row < n; row++) {
            st = new StringTokenizer(br.readLine());
            for (int col = 0; col < m; col++) {
                board.setMap(row, col, parseInt(st.nextToken()));
            }
        }
        
        board.run();
        
        System.out.println(board.getMaxSum());
    }
}

class Board{
    
    static final int[] DR = {-1, 0, 1, 0};
    static final int[] DC = {0, -1, 0, 1};
    
    int n; // 행의 개수
    int m; // 열의 개수
    int maxSum; // 최대 값
    int[][] map; // 각 행과 열에 해당하는 값을 저장할 2차원 배열
    boolean[][] visited; // 방문 여부
    
    Board(int n, int m) {
        this.n = n;
        this.m = m;
        map = new int[n][m];
        visited = new boolean[n][m];
    }

    void setMap(int row, int col, int value) {
        map[row][col] = value;
    }

    void run() {
        
        for (int row = 0; row < n; row++) {
            for (int col = 0; col < m; col++) {
                int count = 1; // 시작은 1로 설정
                int sum = map[row][col]; // 합 초기화

                visited[row][col] = true; // 맨 처음 선택하는 좌상단 좌표는 방문처리
                dfs(row, col, sum, count); // dfs 시작
                visited[row][col] = false; // dfs 종료 후 맨 처음 선택한 시작점 방문 해제
            }
        }
    }
    
    void dfs(int row, int col, int sum, int count) {
        
        if (count == 4) {
            maxSum = Math.max(maxSum, sum); // 최대 값 구하기
            return; // 재귀 종료
        }

        for (int i = 0; i < 4; i++) {
            int nextR = row + DR[i]; // 이동할 row  
            int nextC = col + DC[i]; // 이동할 col

            if (!isValid(nextR, nextC)) continue; // 유효한 범위가 아니거나 이미 방문한 경우

            if (count == 2) { // 만약 2라면 이전 행과 열에서 한 번 더 움직일 수 있도록 row, col로 연결 
                visited[nextR][nextC] = true;
                dfs(row, col, sum + map[nextR][nextC], count + 1);
                visited[nextR][nextC] = false;
            }

            visited[nextR][nextC] = true; // 방문 여부 표시
            dfs(nextR, nextC, sum + map[nextR][nextC], count + 1); // dfs 재귀
            visited[nextR][nextC] = false; // 현재 방문한 곳 false 표시
        }
    }
    
    boolean isValid(int row, int col) { // 유효한 범위에 아직 방문하지 않은 경우 
        return row >= 0 && row < n && col >= 0 && col < m && !visited[row][col];
    }
    
    int getMaxSum() {
        return this.maxSum;
    }
    
}

 

 

2. 풀이 중점 사항

 

해당 문제는 DFS로 완전탐색을 진행하되 블록의 규칙을 찾을 수 있어야 했습니다.

 

***  *     *  ***   **  **   *    * 
*    *** ***    *    *  *    *    *
                     *  *    **  **

*    *  **    **  
**  **   **  **   
 *  *       

*    *   *  ***
**  **  ***  *
*    *


****

*
*
*
* 

**
**

 

해당 블록을 자세히 보면, 특정 점을 시작으로 마치 방문하지 않은 노드를 방문하는 것처럼 이뤄진 것을 확인할 수 있습니다.

예를 들어 아래의 블록들이 있다면 해당 블록은 x x x에서 파생된 것이라고 볼 수 있습니다.

 

x  x  x   |    x   x   x   x   |    x        

        x   |                       |    x  x  x   

 

이는 방문하지 않은 곳으로 노드를 옮겨가며 총 4번만 움직이면 ㅜ, ㅗ, ㅏ, ㅓ 를 제외한 블록은 모두 이동할 수 있음을 의미합니다.

 

ㅏ, ㅗ, ㅓ, ㅜ 는 다음의 방법으로 이동할 수 있습니다.

두번째 이동하는 장소에서  다음으로 이동할 때, nextR, nextC에 대한 map의 합을 구한 후, 경로를 row, col로 설정하는 것입니다.

   

 

 

 

 

    for (int i = 0; i < 4; i++) {
        int nextR = row + DR[i]; // 이동할 row  
        int nextC = col + DC[i]; // 이동할 col

        if (!isValid(nextR, nextC)) continue; // 유효한 범위가 아니거나 이미 방문한 경우

        if (count == 2) { // 만약 2라면 이전 행과 열에서 한 번 더 움직일 수 있도록 row, col로 연결 
            visited[nextR][nextC] = true;
            dfs(row, col, sum + map[nextR][nextC], count + 1);
            visited[nextR][nextC] = false;
        }

        visited[nextR][nextC] = true; // 방문 여부 표시
        dfs(nextR, nextC, sum + map[nextR][nextC], count + 1); // dfs 재귀
        visited[nextR][nextC] = false; // 현재 방문한 곳 false 표시
    }

 

이 그림과 함께 해당 코드를 살펴보면, row, col을 다시 dfs의 재귀 위치로 설정함으로써, 4개를  방문한 후 maxSum 처리를 할 수 있습니다.

 

    visited[nextR][nextC] = true; // 방문 여부 표시
    dfs(nextR, nextC, sum + map[nextR][nextC], count + 1); // dfs 재귀
    visited[nextR][nextC] = false; // 현재 방문한 곳 false 표시

 

다음과 같이 방문 처리 후 다시 방문을 푸는 재귀를 위해 이동한 4개의 움직임이 visited[row][col] = true로 계속 유지된다면
하나의 경로가 끝난 후 실행되는 다음 dfs에 영향을 줄 수 있습니다.

따라서,  count == 4가 된다면 maxSum을 업데이트하기 때문에 현재 방문 여부는 return; 이후 visited[row][col] = false로 처리함으로써 다시 리셋할 수 있게 됩니다.

 

 

이상으로 백준 테트로미노 자바 풀이를 마치도록 하겠습니다. 감사합니다.!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 백준 주사위 굴리기 자바 풀이를 작성하도록 하겠습니다.

 

문제 출처: https://www.acmicpc.net/problem/14499

 

14499번: 주사위 굴리기

첫째 줄에 지도의 세로 크기 N, 가로 크기 M (1 ≤ N, M ≤ 20), 주사위를 놓은 곳의 좌표 x, y(0 ≤ x ≤ N-1, 0 ≤ y ≤ M-1), 그리고 명령의 개수 K (1 ≤ K ≤ 1,000)가 주어진다. 둘째 줄부터 N개의 줄에 지

www.acmicpc.net

 

 

1, 풀이 소스

 

import java.util.*;
import java.io.*;

import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        int n = parseInt(st.nextToken());
        int m = parseInt(st.nextToken());
        int r = parseInt(st.nextToken());
        int c = parseInt(st.nextToken());
        int k = parseInt(st.nextToken());

        Board board = new Board(n, m);
        board.setPoint(r, c);

        for (int row = 0; row < n; row++) {
            st = new StringTokenizer(br.readLine());
            for (int col = 0; col < m; col++) {
                board.setMap(row, col, parseInt(st.nextToken()));
            }
        }

        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < k; i++) {
            board.addCommand(parseInt(st.nextToken()));
        }

        board.run();
        System.out.print(board.printUpNumber());
    }
}

class Board {
    int n;
    int m;
    Point point;
    int[][] map;
    int[] dice;
    Queue<Character> cmds = new ArrayDeque<>();
    List<Integer> upNumbers = new ArrayList<>();

    Board (int n, int m) {
        this.n = n;
        this.m = m;
        map = new int[n][m]; // (0, 0) 부터 시작
        dice = new int[6]; // front, bottom, back, up, left, right
    }

    void setPoint(int row, int col) {
        point = new Point(row, col);
    }

    void setMap(int row, int col, int value) {
        map[row][col] = value;
    }

    void addCommand(int cmd) {
        switch (cmd) {
            case 1:
                cmds.add('E');
                break;
            case 2:
                cmds.add('W');
                break;
            case 3:
                cmds.add('N');
                break;
            case 4:
                cmds.add('S');
                break;
        }
    }

    void run() {

        while(!cmds.isEmpty()) {
            char command = cmds.poll();
            int row = 0;
            int col = 0;

            switch (command) {
                case 'E': // 동쪽은 열++
                    col++;
                    break;
                case 'W': // 서쪽은 열--
                    col--;
                    break;
                case 'N': // 북쪽은 행 -- (위로 올리므로)
                    row--;
                    break;
                case 'S': // 남쪽은 행 ++ (아래로 내려가므로)
                    row++;
                    break;
            }

            int nextR = point.row + row;
            int nextC = point.col + col;

            if (!isValid(nextR, nextC)) continue; // 유효하지 않은 명령어 패스

            rollDice(command); // 주사위 굴리기

            point.row = nextR; // 주사위 위치 row 이동시키기
            point.col = nextC; // 주사위 위치 col 이동시키기

            if (map[point.row][point.col] == 0) {
                map[point.row][point.col] = dice[1]; // 바닥에 주사위 값 복사
            } else {
                dice[1] = map[point.row][point.col]; // 바닥에 있는 값을 주사위애 복사
                map[point.row][point.col] = 0; // 칸 값 0
            }

            upNumbers.add(dice[3]); // 상단에 있는 주사위 값 upNumbers에 추가
        }
    }

    boolean isValid(int row, int col) {
        return row >= 0 && row < n && col >= 0 && col < m;
    }

    void rollDice(char command) {
        // front(0), bottom(1), back(2), up(3), left(4), right(5) 회전 마침
        int up = dice[3];
        int bottom = dice[1];
        int left = dice[4];
        int right = dice[5];
        int front = dice[0];
        int back = dice[2];

        if (command == 'E') { // 왼쪽으로 회전
            dice[4] = up; // 왼 <- 위
            dice[1] = left; // 바 <- 왼
            dice[5] = bottom; // 오 <- 바
            dice[3] = right; // 위 <- 오
        }

        else if (command == 'W') { // 오른쪽으로 회전
            dice[5] = up; // 오 <- 위
            dice[1] = right; // 바 <- 오
            dice[4] = bottom; // 왼 <- 바
            dice[3] = left; // 위 <- 왼
        }

        else if (command == 'N') { // 북쪽으로
            dice[0] = bottom; // 앞 <- 바
            dice[3] = front; // 위 <- 앞
            dice[2] = up; // 뒤 <- 위
            dice[1] = back; // 바 <- 뒤
        }

        else if (command == 'S') {
            dice[0] = up; // 앞 <- 위
            dice[1] = front; // 바 <- 앞
            dice[2] = bottom; // 뒤 <- 바
            dice[3] = back; // 위 <- 뒤
        }
    }

    String printUpNumber() {
        StringBuilder sb = new StringBuilder();
        for (int num : upNumbers) sb.append(num).append("\n");
        return sb.toString();
    }

    class Point {
        int row;
        int col;

        Point (int row, int col) {
            this.row = row;
            this.col = col;
        }
    }
}

 

 

2. 풀이 중점 사항

 

해당 문제는 회전 문제로, 주사위를 어떻게 회전시켜야 하는지가 중요한 과제입니다. 

먼저 방위표는 고정입니다. 즉 이전에 서쪽으로 이동하였더라도 방위표의 기준은 변하지 않습니다. 즉 서쪽 (컬럼 기준으로는 왼쪽으로 이동) 방향은 바뀌지 않습니다.

 

현재 위치한 지점으로 부터 동서남북 각 방향으로 이동한다고 했을 때, 명령어에 따라 주사위를 회전시켜야 합니다.

이후, 회전시킨 주사위를 이동시켜서 칸에 번호가 있는지 없는지 판단하고 바닥을 업데이트해야 합니다.

 

문제 풀이 의사코드는 다음과 같습니다.

 

  • 각 방위에 대한 숫자형 데이터를 입력받고, 선호도에 따라 인트형 혹은 char형 등으로 이동 방향을 선입선출로 처리할 큐에 넣습니다.
  • 큐가 빌 때까지 (방향) 루프를 진행합니다.
  • 각 방위는 고정이므로 E, W, N, S에 따라 다음 이동할 nextR, nextCol을 조정합니다.
  • 이동 불가능한 (인덱스를 벗어난) nextR, nextCol은 continue 하여 명령어를 넘깁니다.
  • 이동 가능한 경우 rollDice()를 통해 주사위를 굴려서 각 주사위의 값을 회전시킵니다.
  • 회전 시, 동 서 방향은 위, 바닥, 왼쪽, 오른쪽 면만 영향을 받습니다.
  • 회전 시, 남 북 방향은 앞, 뒤, 위, 바닥 면만 영향을 받습니다.
  • 회전을 마친 후, 구한 nextR, nextC을 현재 있는 point에 업데이트하여 이동을 시작합니다.
  • 이동한 칸이 0이라면, 현재 주사위 상태의 바닥에 있는 값을 칸에 복사합니다.
  • 이동한 칸이 0이 아니라면, 현재 주사위 상태의 바닥에 칸의 값을 복사하고, 칸은 0으로 만듭니다.
  • 이동을 마쳤으므로 현재 주사위 상태의 윗면의 값을 upNumbers에 추가합니다.
  • 큐가 비면, 윗면 리스트를 담은 upNumbers를 출력합니다.

 

방향성 

 

switch (command) {
    case 'E': // 동쪽은 열++
        col++;
        break;
    case 'W': // 서쪽은 열--
        col--;
        break;
    case 'N': // 북쪽은 행 -- (위로 올리므로)
        row--;
        break;
    case 'S': // 남쪽은 행 ++ (아래로 내려가므로)
        row++;
        break;
}

 

저는 이전에 이러한 이동 문제를 풀 때, x, y로 하려다 보니 항상 실수했던 경험이 있습니다. 따라서, row, col로 명확하게 명시하고 문제를 해결하다 보니 DB를 다룰 때처럼 자연스럽게 up 명령어는 row--를 실행하고 down 명령어는 row++를 처리하여 도중에 실수를 방지할 수 있었습니다.

 

 

주사위 회전

 

if (command == 'E') { // 왼쪽으로 회전
    dice[4] = up; // 왼 <- 위
    dice[1] = left; // 바 <- 왼
    dice[5] = bottom; // 오 <- 바
    dice[3] = right; // 위 <- 오
}

 

주사위 회전 시, 저는 이 부분이 항상 헷갈렸습니다. 따라서 저는 command == 'E'라는 식으로 명확하게 동쪽으로 회전하겠다는 것을 명시하여 혼돈을 줄이도록 작성을 했습니다.

왼쪽으로 주사위를 돌리면, 위쪽에 있던 면은 왼쪽으로 가므로 이러한 방식으로 회전을 처리하였습니다.

 

작성하다 보니 코드가 길어졌지만, 문제 자체가 난해하고 어렵기 때문에,

케이스를 명확하게 나누는 연습을 하였고, 쓰다가 제가 헷갈리는 것을 방지하기 위해 이왕이면 코드가 의미하는 바가 무엇인지 최대한 신경 써서 작성하려고 하였습니다.

 

이상으로 주사위 굴리기 문제를 마치도록 하겠습니다. 감사합니다.! 

+ Recent posts