문제 링크

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

문제 풀이

보물 지도가 주어질 때, 보물이 묻혀 있는 두 곳 간의 최단 거리로 이동하는 시간을 구하는 문제이다.

 

최단 거리에 대한 조건은 최단 거리로 이동하는데 있어 가장 긴 시간이 걸리는 육지 두 곳이라고 명시되어 있다.

이 말은 최단 거리들 중에서 가장 시간이 오래 걸리는 두 점을 찾으라는 말이며 위치로 본다면 최단 거리들 중에서 가장 먼 거리를 구하라는 말이다.

 

문제에서 최단 거리들 중에서 가장 시간이 오래 걸린다는 말은 두 점 간의 최단 거리는 여러 개가 존재할 수 있고 그 중에서 가장 거리가 먼 두 점을 찾으라는 말로 해석하면 된다.

 

예시

그림을 통해 지도 내 존재하는 어떤 한 점에서 다른 점까지의 최단 거리들의 존재를 확인할 수 있다.

 

파란색 점들은 시작 점에서 이동할 수 있는 지점들이며 그 중에 최대한 먼 거리가 또 다른 주황색 점이다.

코드

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 {
    private static int rowSize;
    private static int colSize;
    private static int[][] map;
    private static int[] dx = new int[]{0, 0, 1, -1};
    private static int[] dy = new int[]{1, -1, 0, 0};

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        rowSize = Integer.parseInt(st.nextToken());
        colSize = Integer.parseInt(st.nextToken());
        map = new int[rowSize][colSize];

        for (int i = 0; i < rowSize; i++) {
            String[] row = br.readLine().split("");
            for (int j = 0; j < colSize; j++) {
                String value = row[j];
                if (value.equals("L")) {
                    map[i][j] = 1;
                }
            }
        }
        int answer = 0;
        for (int i = 0; i < rowSize; i++) {
            for (int j = 0; j < colSize; j++) {
                if (map[i][j] == 1) {
                    boolean[][] visited = new boolean[rowSize][colSize];
                    int maxDistance = search(i, j, visited);
                    answer = Math.max(maxDistance, answer);
                }
            }
        }
        System.out.println(answer);
        br.close();
    }

    public static int search(int row, int col, boolean[][] visited) {
        Queue<Coordinate> queue = new LinkedList<>();
        queue.offer(new Coordinate(row, col,0));
        visited[row][col] = true;
        int maxDist = 0;
        while (!queue.isEmpty()) {
            Coordinate current = queue.poll();

            if(maxDist < current.getDist()){
                maxDist = current.getDist();
            }
            for (int i = 0; i < 4; i++) {
                int nRow = current.getRow() + dx[i];
                int nCol = current.getCol() + dy[i];

                if(isIn(nRow, nCol) && map[nRow][nCol]==1 && !visited[nRow][nCol] ){
                    visited[nRow][nCol] = true;
                    queue.offer(new Coordinate(nRow, nCol, current.getDist()+1));
                }
            }
        }
        return maxDist;
    }

    private static boolean isIn(int row, int col) {
        return (0 <= row && row < rowSize) && (0 <= col && col < colSize);
    }
}

class Coordinate {
    private final int row;
    private final int col;
    private final int dist;
    public Coordinate(int row, int col, int dist) {
        this.row = row;
        this.col = col;
        this.dist = dist;
    }

    public int getRow() {
        return row;
    }

    public int getCol() {
        return col;
    }

    public int getDist() {
        return dist;
    }

'코딩테스트 > boj' 카테고리의 다른 글

줄어들지 않아 - 백준 2688  (0) 2024.05.22
떡 먹는 호랑이 - 백준 2502 자바  (0) 2024.05.22
별 찍기 11 - 2448  (0) 2024.03.30
벽 부수고 이동하기 - 백준 2206  (0) 2024.03.30
램프 - 백준 1034  (0) 2024.03.07

문제 링크

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

 

문제 풀이

어떤 숫자가 줄어들지 않는다는 것은 그 숫자의 각 자리 수보다 그 왼쪽 자리 수가 작거나 같을 때라고 한다. 그렇다면 숫자를 구성할 때 특정 자리 k 이후의 숫자는 특정 자리 k의 수 이상을 만족하면 된다. 이때 숫자는 leading zero여도 만족하므로 0부터 9까지의 숫자 중에 줄어들지 않는 조건을 만족하는 순열을 구하면 되는 문제이다.

어떻게 되는지 확인해보자

1 자리 수일 때

1 자리 수인 경우 앞에 어떤 숫자도 올 수 없기 때문에 0,1,2,3,4,5,6,7,8,9 가 가능하며 10가지 수가 존재한다.

2 자리 수일 때

2 자리 수인 경우는 1 자리 수에서 확장해볼 수 있다.

  • 첫 번째 자리 수가 0인 경우
    • 두 번째 자리 수는 0~9까지 숫자가 가능하다.
  • 첫 번째 자리 수가 1인 경우
    • 두 번째 자리 수는 1~9까지 숫자가 가능하다.
  • 첫 번째 자리 수가 2인 경우
    • 두 번째 자리 수는 2~9까지 숫자가 가능하다.
  • 첫 번째 자리 수가 3인 경우
    • 두 번째 자리 수는 3~9까지 숫자가 가능하다.

이미 눈치채신 분들도 있겠지만 n번째 자리에 가능한 숫자의 갯수는 이전 자리 수부터 9까지 숫자가 올 수 있다는 것을 알 수 있다.

이전 값을 기반으로 같은 동작이 반복된다면 다이나믹 프로그래밍의 메모이제이션 기법으로 문제를 해결할 수 있다.

풀이 코드

파악한 규칙대로 작성한 코드

import java.util.Arrays;
import java.util.Scanner;

public class Main2 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int numberOfTestCases = scanner.nextInt();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < numberOfTestCases; i++) {
            int n = scanner.nextInt();
            long[][] dp = new long[n + 1][10];
            for (int j = 0; j < 10; j++) {
                dp[1][j] = 1;
            }
            for (int j = 2; j <=n; j++) {
                for (int k = 0; k < 10; k++) {
                    for (int l = k; l < 10; l++) {
                        dp[j][k] += dp[j-1][l];
                    }
                }
            }
            long answer = Arrays.stream(dp[n]).sum();
            sb.append(answer+"\\n");
        }
        System.out.println(sb);
        scanner.close();
    }

}

좀 더 dynamic programming 답게 수정한 코드

import java.util.Scanner;

public class Main3 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int numberOfTestCases = scanner.nextInt();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < numberOfTestCases; i++) {
            int n = scanner.nextInt();
            long[][] dp = new long[n + 1][11];
            
            // 0의 자리일 때 초기화
            for (int j = 0; j <= 10; j++) {
                dp[0][j] = 1;
            }
            // 1부터 n번째 자리 수까지 경우의 수 누적
            for (int j = 1; j <= n; j++) {
		            // j번째 자리 수일 때 숫자별로 가능한 경우의 수를 누적
                for (int k = 1; k <= 10; k++) {
                    dp[j][k] = dp[j][k - 1] + dp[j - 1][k];
                }
            }
            // 마지막 열에 j 번째 행의 값이 모두 누적되므로 
            // n번째 행의 10 번째 열의 값이 정답
            sb.append(dp[n][10]).append("\\n");
        }
        System.out.println(sb);
        scanner.close();
    }
}

좀 더 dp답게 푼 코드가 근소하게 시간이 덜 걸리는 것을 볼 수 있다.

'코딩테스트 > boj' 카테고리의 다른 글

보물섬 - 백준 2589  (0) 2024.05.22
떡 먹는 호랑이 - 백준 2502 자바  (0) 2024.05.22
별 찍기 11 - 2448  (0) 2024.03.30
벽 부수고 이동하기 - 백준 2206  (0) 2024.03.30
램프 - 백준 1034  (0) 2024.03.07

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

1. 문제 분석하기

호랑이는 할머니에게 n번째 날(n ≥ 3)에 n-1번째 날에 먹은 떡의 개수 + n-2 번째 날에 먹은 떡의 개수만큼 요구한다고 나와있다.

수식으로 나타내면 $a_n = a_{n-1} + a_{n-2}$으로 표현할 수 있고 굉장히 익숙한 형태인 피보나치 수열의 형태이다.

즉, n번째 날은 피보나치 수열의 n번째 항과 동일하다고 볼 수 있다.

문제에서 최종적으로 알아내야 하는 값은 할머니가 넘어온 날과 그날 호랑이에게 준 떡의 개수를 피보나치 수열의 n번째 항의 값으로 생각하고 n번째 항의 값을 만들 수 있는 첫 번째 항과 두 번째 항의 값을 찾는 것이다.

2. 문제 해결 방법 생각하기

할머니가 넘어온 날 표현하기

피보나치 수열의 값들은 첫 번째 항(a)과 두 번째 항(b)으로부터 파생되므로 a,b의 선형 결합 $a_k = m * a + n * b$의 형태로 모든 항을 나타낼 수 있게 된다.

이 아이디어를 기반으로 다이나믹 프로그래밍 기법을 적용하여 할머니가 넘어온 날은 몇 개의 a와 b로 이루어졌는지 구할 수 있다.

첫 번째 날과 두 번째 날 떡의 개수 구하기

할머니가 넘어온 날은 몇 개의 a와 b로 이루어졌는지 구했다면 a와 b를 구할 차례다.

$a_k = m * a + n * b$는 일차 함수로 변환할 수 있기 때문에 a 또는 b 중에 하나를 왼쪽으로 넘겨준다.

b를 기준으로 일차 함수를 만들게 되면 $b = \frac{m}{n} * a - \frac{a_k}{n}$의 형태가 된다.

우리가 구해야 하는 a와 b는 정수이므로 a를 1부터 증가시키면서 $(m*a - a_k) \mod n = 0$을 만족할 때 정수인 b를 구할 수 있다.

3. 코드

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int numberOfDays = scanner.nextInt();
        int numberOfRiceCake = scanner.nextInt();
        Coefficient[] coefficients = new Coefficient[numberOfDays + 1];
        coefficients[1] = new Coefficient(1, 0);
        coefficients[2] = new Coefficient(0, 1);

        for (int i = 3; i <= numberOfDays; i++) {
            Coefficient dayBeforeYesterday = coefficients[i - 2];
            Coefficient yesterday = coefficients[i - 1];
            coefficients[i] = new Coefficient(
            dayBeforeYesterday.getA1() + yesterday.getA1(), 
            dayBeforeYesterday.getA2() + yesterday.getA2());
        }

        int a1 = coefficients[numberOfDays].getA1();
        int a2 = coefficients[numberOfDays].getA2();
        int quotient = numberOfRiceCake / a2;
        int candidate1 = 0;
        int candidate2 = 0;
        for (int i = 1; i < quotient; i++) {
            int res = numberOfRiceCake - (a2 * i);

            if (res % a1 == 0 
            && calculate(
            Math.min(i, res/ a1), 
            Math.max(i, res/ a1), 
            numberOfDays) == numberOfRiceCake) {
                candidate1 = i;
                candidate2 = res / a1;
            }
        }
        int a = Math.min(candidate1, candidate2);
        int b = Math.max(candidate1, candidate2);
        System.out.println(a);
        System.out.println(b);
        scanner.close();
    }

    public static int calculate(int a, int b, int days) {
        int c = 0;
        for (int i = 3; i <= days; i++) {
            c = b + a;
            a = b;
            b = c;
        }
        return c;
    }
}

class Coefficient {
    private int a1;
    private int a2;

    public Coefficient(int a1, int a2) {
        this.a1 = a1;
        this.a2 = a2;
    }

    public int getA1() {
        return a1;
    }

    public int getA2() {
        return a2;
    }
}

'코딩테스트 > boj' 카테고리의 다른 글

보물섬 - 백준 2589  (0) 2024.05.22
줄어들지 않아 - 백준 2688  (0) 2024.05.22
별 찍기 11 - 2448  (0) 2024.03.30
벽 부수고 이동하기 - 백준 2206  (0) 2024.03.30
램프 - 백준 1034  (0) 2024.03.07

문제 링크

2448번: 별 찍기 - 11

1. 풀이

고전적인 별 찍기 문제와 더불어 재귀를 통해 풀어야 한다.

재귀를 사용해야 함은 문제 예시를 통해 알 수 있다.

n이 24일 때가 예시로 나와 있다.

n은 $3*2^k$로 이루어져 있으므로 24는 k값이 3이 된다.

k의 값은 $0\le k \le 10$ 이므로 주어진 값은 1, 2, 4, 8 순으로 4번의 재귀로 만들어졌다고 가정하면

$k=0$일 때의 모양은 아래와 같다.

그렇다면 우리는 위의 별 찍기를 기준으로 k가 증가함에 따라 확장해나가야 함을 알 수 있다.

그럼 규칙을 살펴보기 위해 좀 더 예시를 확인해보자.

$k=1, n=6 \simeq 3 \times 2^1$

$k=2, n=12 \simeq 3 \times 2^2$

계속해서 패턴을 확인해보면 이전 단계의 모양이 다음 단계의 기초 모양이 되는 것을 알 수 있다.

그렇다면 가장 작은 형태로 쪼개면 된다.

하지만 문자열로 처리하기에는 한 줄마다 형태를 찍어야 한다는 어려운 점이 존재하기 때문에 배열을 이용하는 것이 합당하다고 생각했다.

그럼 인덱스를 활용해야 한다는 의미가 되며 인덱스를 사용하기 위해서는 일단 n값에 따른 높이와 밑변의 길이를 알아야 한다.

확인해보면 높이는 n, 밑변의 길이는 2*n-1이 되는 것을 확인할 수 있다.

이제는 기준점을 잡아야 한다.

첫 번째 행의 별의 위치는 [0, n/2] 임을 알 수 있다.

두 번째 행의 별의 위치는 첫 번째 행의 별의 위치에서 좌로 한 칸, 우로 한 칸 이동한 모습이다.

세번째 행의 별의 위치는 첫번째 행의 별의 위치부터 좌로 두 칸, 우로 두 칸까지 모두 찍혀 있다.

이와 같이 첫 번째 행의 별의 위치가 기준점이 될 수 있음을 알 수 있다.

다음은 패턴마다 3개의 삼각형에 대해 기준점의 위치를 구하는 방법이다.

k=1인 경우를 살펴보면 행의 길이가 6, 열의 길이가 11이다.

가장 위쪽의 삼각형의 첫 번째 행의 별의 위치는 [0, 5],

가장 왼쪽의 삼각형의 첫 번째 행의 위치는 [3, 2(5-2)],

가장 오른쪽의 삼각형의 첫 번째 행의 위치는 [3, 7(5+2)] 라는 것을 알 수 있다.

여기서 행의 3이란 값은 행의 길이의 절반이라는 것을 알 수 있다.

또한, 열의 5란 값은 열의 길이인 11을 2로 나눈 것이고 5의 절반만큼 더하고 빼준 것을 확인할 수 있다.

찾아야 하는 규칙은 모두 찾았으므로 규칙을 이용하여 재귀 함수를 구성하면 된다.

주의하여야 할 점은 ‘출력 형식이 잘못되었습니다’ 라는 결과가 나온다면 혹시 별이 찍힌 행들의 인덱스가 2씩 증가하도록 배열을 구성했는지 확인해보는 것을 추천한다.

처음 문제를 풀 때 별이 찍힌 행 간에 공백이 존재하는 줄 알고 인덱스를 계산했다가 출력 형식이 잘되었다는 결과가 나와서 의아했었다…

또한, 표준 출력을 이용하면 StringBuilder나 BufferedWriter보다 현저하게 느려지므로 주의하면서 문제를 푸시길 권한다.

2. 소스 코드

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static char[][] starMap;

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int k = n / 3;
        starMap = new char[n][n * 2 - 1];
        for (int i = 0; i < n; i++) {
            Arrays.fill(starMap[i], ' ');
        }
        printStars(0, (n * 2 - 1) / 2, n, n);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < 2 * n - 1; j++) {
                sb.append(starMap[i][j]);
            }
            sb.append("\\n");
        }
        System.out.println(sb);

    }

    public static void printStars(int row, int col, int n, int height) {
        if (n == 3) {
            print(row, col);
            return;
        }
        printStars(row, col, n /2, height / 2);
        printStars(row + height / 2, col - (height / 2), n /2, height / 2);
        printStars(row + height / 2, col + (height / 2), n /2, height / 2);
    }

    public static void print(int row, int col) {
        eachPrint(row, col);

    }

    public static void eachPrint(int row, int col) {

        for (int i = 0; i < 3; i++) {
            for (int j = 0; j <= i; j++) {
                starMap[i + row][col - j] = '*';
                starMap[i + row][col + j] = '*';
            }
        }
        starMap[row + 1][col] = ' ';
    }
}

3. 느낀 점

처음 별 찍기 패턴은 빠르게 파악했지만 인덱스 계산에 어려움이 있었다. 애초에 문제를 잘못 이해한 탓도 있었다. 문제에 대해 모든 것을 알고 있어야 자명해지는 재귀는 어려우면서 재밌는 것 같다.

'코딩테스트 > boj' 카테고리의 다른 글

줄어들지 않아 - 백준 2688  (0) 2024.05.22
떡 먹는 호랑이 - 백준 2502 자바  (0) 2024.05.22
벽 부수고 이동하기 - 백준 2206  (0) 2024.03.30
램프 - 백준 1034  (0) 2024.03.07
숫자 할리갈리 게임 - 백준 20923  (2) 2024.02.29

문제 링크

2206번: 벽 부수고 이동하기

1. 풀이

벽을 부수고 이동할 수 있을 때의 최단 거리를 구하는 문제이다.

최단 거리는 너비 우선 탐색(Breadth First Search)을 이용하면 구할 수 있다.

그렇다면 벽을 부셨을 때의 상태 변화를 어떤 방식으로 체크를 해나가야 할까?

 

이 부분에서 엄청나게 헷갈려서 스터디를 진행할 때 제 시간에 해결을 못했다.

 

우리는 보통 프로그램이 무한 반복을 할 수 있는 재방문을 막기 위해 방문 배열을 이용하여 탐색을 진행한다.

방문 배열에서 상태 값은 방문 여부이다. 그럼 여기서 폭탄을 사용했음을 표시하려면 어떻게 해야 할까?

방문 배열의 차원을 한 차원 증가하여 상태에 따른 방문 여부를 확인할 수 있지 않을까?

 

예를 들면, 경로 탐색을 진행하다가 벽을 만난 경우 폭탄을 사용한 뒤에는 방문 배열 중 폭탄을 사용한 상태인 배열에 방문 처리를 하는 식이다.

 

눈치가 빠른 분들은 벌써 문제 해결의 키가 무엇인지 눈치채셨으리라 생각한다.

폭탄을 사용한 이후부터는 폭탄을 사용한 상태의 방문 배열을 이용하고 폭탄을 아직 사용하지 않았다면 폭탄 미사용 방문 배열을 사용하여 방문 체크를 진행하면 되는 것이다.

만약, 이렇게 하지 않고 폭탄을 사용한 위치에 대한 상태 값으로만 방문 배열을 사용한다면 계속 진행할 수 있음에도 나갈 수 없게 되어 정확한 답을 구할 수 없게 된다.

2. 소스 코드

package study.ct_study_58.problem3;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {
    private static final int USE_BOMB = 1;
    private static final int NOT_USE_BOMB = 0;
    private static final int WALL = 1;
    private static final int PATH = 0;
    private static int rowSize;
    private static int colSize;
    private static int[][] map;
    private static final int[][] MOVES = new int[][]{{0, 1}, {0, -1}, {1, 0}, {-1, 0}};

    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(bufferedReader.readLine());
        rowSize = Integer.parseInt(st.nextToken());
        colSize = Integer.parseInt(st.nextToken());
        map = new int[rowSize][colSize];
        for (int i = 0; i < rowSize; i++) {
            map[i] = Arrays.stream(bufferedReader.readLine().split("")).mapToInt(Integer::valueOf).toArray();
        }
        long res = bfs(0, 0);
        System.out.println(res);
        bufferedReader.close();
    }

    public static int bfs(int row, int col) {
        Queue<Block> queue = new LinkedList<>();
        queue.offer(new Block(row, col, 1, false));
        boolean[][][] visited = new boolean[rowSize][colSize][2];
        // 아직 폭탄을 사용하지 않은 상태이므로 폭탄을 사용하지 않은 방문 배열에 방문 체크
        visited[0][0][NOT_USE_BOMB] = true;
        while (!queue.isEmpty()) {
            Block current = queue.poll();
            if (current.getRow() == rowSize - 1 && current.getCol() == colSize - 1) {
                return current.getDistance();
            }
            for (int[] move : MOVES) {
                int nextRow = current.getRow() + move[0];
                int nextCol = current.getCol() + move[1];
                if (!isIn(nextRow, nextCol)) {
                    continue;
                }
								// 벽을 만났고 폭탄을 아직 사용하지 않았으며 폭탄을 사용한 BLOCK들이 현재 위치에 방문한 적이 없는 경우  
                if (map[nextRow][nextCol] == WALL && !current.isBroken() && !visited[nextRow][nextCol][USE_BOMB]){
                    visited[nextRow][nextCol][map[nextRow][nextCol]] = true;
                    queue.offer(new Block(nextRow, nextCol, current.getDistance() + 1, true));
                }
								// 경로를 만난 경우
                if (map[nextRow][nextCol] == PATH) {
                    // 폭탄은 이미 사용했고 폭탄을 이용한 이후의 방문 배열에서 방문한 적이 없는 경우
                    if (current.isBroken() && !visited[nextRow][nextCol][USE_BOMB]) {
                        visited[nextRow][nextCol][USE_BOMB] = true;
                        queue.offer(new Block(nextRow, nextCol, current.getDistance() + 1, true));
                        // 폭탄을 아직 사용하지 않았고 폭탄을 사용하지 않은 방문 배열에서 방문한 적이 없는 경우
                    } else if (!current.isBroken() && !visited[nextRow][nextCol][NOT_USE_BOMB]) {
                        visited[nextRow][nextCol][NOT_USE_BOMB] = true;
                        queue.offer(new Block(nextRow, nextCol, current.getDistance() + 1, false));
                    }
                }
            }
        }
				// BFS를 진행하는 동안 최단 거리가 리턴되지 않는다면 도착점에 도달할 수 없으므로 -1을 반환 
        return -1;
    }

    public static boolean isIn(int row, int col) {
        return (0 <= row && row < rowSize) && (0 <= col && col < colSize);
    }
}

class Block {
    private int row;
    private int col;

    private int distance;
    private boolean isBroken;

    public Block(int row, int col, int distance, boolean isBroken) {
        this.row = row;
        this.distance = distance;
        this.col = col;
        this.isBroken = isBroken;
    }

    public int getRow() {
        return row;
    }

    public int getCol() {
        return col;
    }

    public boolean isBroken() {
        return isBroken;
    }

    public int getDistance() {
        return distance;
    }
}

3. 느낀 점

인자 값이 정확히 무엇을 의미하는지 알고 풀어야 함을 다시 한번 느낄 수 있었던 문제였다.

방문 배열의 상태 값이 폭탄을 사용했을 때와 하지 않았을 때로 분리하는 것까지는 좋았는데 상태와 함께 방문한다는 것을 생각하지 못했다.

부트캠프에서 알고리즘 강의를 들으며 강사님께 DP는 어떻게 해야 잘 풀 수 있을까 질문을 했던 적이 있었다.

그 때 강사님께서 말씀하시길 DP 문제에서 각 변수들이 의미하는 바가 무엇인지 이해해보세요라는 말씀을 하셨는데 이번 문제를 시간 안에 해결하지 못한 이유와 동일한 것 같다.

문제 풀이 시 조금 더 의미를 이해한 뒤 코드를 작성해야겠다.

'코딩테스트 > boj' 카테고리의 다른 글

줄어들지 않아 - 백준 2688  (0) 2024.05.22
떡 먹는 호랑이 - 백준 2502 자바  (0) 2024.05.22
별 찍기 11 - 2448  (0) 2024.03.30
램프 - 백준 1034  (0) 2024.03.07
숫자 할리갈리 게임 - 백준 20923  (2) 2024.02.29

문제 링크

1034번: 램프

문제 해석

알고리즘 및 자료구조 유형

  • 애드 혹
  • 브루트포스

조건 분석

스위치는 어떤 열에 존재하는 램프의 상태를 1이면 0으로 0이면 1로 변경한다.

그렇다면 행을 기준으로 보았을 때 어떤 열의 스위치를 누른다면 각 행의 상태가 일치하는 것끼리 같이 상태가 변경될 것을 유추해볼 수 있다.

 

그렇다면 어떻게 행의 모든 열이 1이 될 수 있는지 판별할 수 있을까?

어떤 행의 0인 상태의 개수가 홀수 또는 짝수 여부와 k의 홀수 짝수 여부를 확인해보면 된다.

 

만약 어떤 행의 0인 상태의 개수가 2개이고 k가 6라고 가정해보자.

2개의 0인 열의 스위치는 한 번씩 누르면 행의 모든 열이 1이 된다.

남은 4번은 어느 열이든지 눌러도 행은 모두 1인 상태를 유지할 수 있게된다.

이는 스위치를 짝수 번만큼 누르면 원래 상태로 되돌아오는 특성 때문이다.

어떤 행의 0인 상태의 개수가 홀수 개이고 k도 홀수인 경우도 마찬가지다.

 

하지만 여기서 고려해야 할 조건이 하나 더 존재한다.

만약 0인 상태의 개수가 k보다 크다면 어떻게 될까?

0인 램프를 모두 1로 만들지 못하는 경우가 발생한다.

 

따라서 발견한 조건들을 종합해보면 어떤 행의 0인 상태 개수의 홀수 짝수 여부와 k의 홀수 짝수 여부는 동일해야 하며 0인 상태의 개수는 k보다 작거나 같아야 함을 알 수 있다.

 

그렇다면 행의 패턴이 발견한 조건을 만족하며 갯수가 가장 많은 패턴을 찾으면 문제를 해결할 수 있다.

풀이

1. 주어진 램프의 상태를 입력받는다.

2. 램프를 누르는 횟수를 입력받는다.

3. 램프 누르는 횟수가 홀수인지 짝수인지 판별한다.

4. 각 행의 램프의 상태를 키 값으로 동일한 패턴의 행의 수를 파악한다.

5. 파악한 패턴들마다 순회하며 패턴 내 존재하는 0의 개수를 세어 그 합이 홀수인지 짝수인지 판별한다.

6. 만약 패턴 내 존재하는 0의 개수와 램프 누르는 횟수의 홀수 짝수 여부가 일치 여부를 판별한다.

7. 만약 일치한다면 0의 갯수가 램프를 누르는 횟수보다 작거나 같을 경우 정답을 갱신한다.

코드

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int rowSize = scanner.nextInt();
        int columnSize = scanner.nextInt();
        String[] table = new String[rowSize];
        for (int i = 0; i < rowSize; i++) {
            table[i] = scanner.next();
        }
        int numberOfClick = scanner.nextInt();
        HashMap<String, Integer> map = new HashMap<>();
        for (int i = 0; i < rowSize; i++) {
            String row = table[i];
            map.put(row, map.getOrDefault(row, 0) + 1);
        }
        List<String> keys = new ArrayList<>(map.keySet());
        int answer = 0;
        int evenOrOdd = numberOfClick % 2;
        for (int i = 0; i < keys.size(); i++) {
            String[] key = keys.get(i).split("");
            int numberOfOff = 0;
            for (int j = 0; j < key.length; j++) {
                if (key[j].equals("0")) {
                    numberOfOff++;
                }
            }
            if (numberOfOff % 2 != evenOrOdd) {
                continue;
            }
            if (numberOfOff <= numberOfClick) {
                answer = Math.max(answer, map.get(keys.get(i)));
            }
        }
        System.out.println(answer);
        scanner.close();
    }
}

회고

첫 풀이 당시 모든 스위치에 대해 눌러본 후 최선의 선택을 구하기 위해 램프 배열의 상태 변경을 위한 재귀를 사용했었다. 풀이 후 생각해보니 최대 2의 1000승이라는 가짓수가 발생할 수 있었고 시간 초과가 발생할 수 밖에 없는 풀이라는 것을 간과했음을 알았다.

자료구조와 알고리즘을 사용하는 문제는 익숙해진 것 같지만 패턴을 찾아내는 문제는 아직 미숙하다는 것을 다시금 느끼게 해주는 문제였다.

'코딩테스트 > boj' 카테고리의 다른 글

줄어들지 않아 - 백준 2688  (0) 2024.05.22
떡 먹는 호랑이 - 백준 2502 자바  (0) 2024.05.22
별 찍기 11 - 2448  (0) 2024.03.30
벽 부수고 이동하기 - 백준 2206  (0) 2024.03.30
숫자 할리갈리 게임 - 백준 20923  (2) 2024.02.29

문제 해석

알고리즘 및 자료구조 유형

할리갈리 게임을 시뮬레이션하고 카드 덱과 카드를 내는 그라운드와 관련된 연산을 deque 자료구조를 이용하면 풀 수 있는 문제다.

 

스택과 큐를 사용할 수도 있지만 앞뒤로 삽입과 삭제 연산이 가능한 deque를 쓰는 것이 더 유리하기 때문에 deque 자료구조를 이용했다.

조건 분석

기존의 할리갈리 게임과 달리 숫자로 이루어져 있는 카드를 사용하기 때문에 종을 치는 조건에 유의해야 한다.
또한, 게임 시행 횟수에 대한 조건을 잘 파악해야 한다.

 

문제에 주어진 두 명의 플레이어 도도와 수연이가 있고 종을 치는 행위는 게임 진행에 영향을 주지 않으므로 각자가 그라운드에 카드를 내는 행위가 게임 1회 시행으로 간주해야 한다.

 

종을 칠 수 있는 조건은 플레이어마다 다르다.

  • 도도
    • 양쪽 그라운드 위에 숫자 5가 쓰여진 카드가 있는 경우
  • 수연
    • 양쪽 그라운드 위의 카드 숫자의 합이 5가 되는 경우
    • 이 때, 어느 쪽의 그라운드도 비어 있으면 안된다.

종을 쳤다면 상대방 플레이어 그라운드 위의 카드를 뒤집어서 자신의 카드 덱의 밑에 추가하고 자신의 그라운드 위 카드도 뒤집어서 자신의 카드 덱에 추가한다.

풀이

게임은 도도부터 시작한다.
도도는 그라운드에 카드를 내고 플레이어 각자는 종을 칠 수 있는 조건을 확인한다.
종을 쳤다면 그라운드 위 카드를 카드 덱에 추가한다.
한 턴이 종료된다.

 

수연이도 그라운드에 카드를 내고 플레이어 각자는 종을 칠 수 있는 조건을 확인한다.
종을 쳤다면 그라운드 위 카드를 카드 덱에 추가한다.
한 턴이 종료된다.

 

게임 시행 중 어느 플레이어의 카드 덱이 모두 소진되었다면 게임을 종료한다.

게임이 끝난 이후 두 플레이어의 카드 수를 확인하고 카드 수가 더 많은 사람이 승리한다.
만약 카드 수가 동일하다면 비기게 된다.

소스 코드

package ct_study_46.problem1;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.StringTokenizer;

public class Main {
    private static final String DODO_WIN = "do";
    private static final String SUYEON_WIN = "su";
    private static final String DRAW = "dosu";

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int numberOfCard = Integer.parseInt(st.nextToken());
        int numberOfStage = Integer.parseInt(st.nextToken());
        Deque<Integer> deckOfDodo = new LinkedList<>();
        Deque<Integer> deckOfSuYeon = new LinkedList<>();
        for (int i = 0; i < numberOfCard; i++) {
            st = new StringTokenizer(br.readLine());
            int cardOfDodo = Integer.parseInt(st.nextToken());
            int cardOfSuyeon = Integer.parseInt(st.nextToken());
            deckOfDodo.offerFirst(cardOfDodo);
            deckOfSuYeon.offerFirst(cardOfSuyeon);
        }
        Deque<Integer> groundOfDodo = new LinkedList<>();
        Deque<Integer> groundOfSuyeon = new LinkedList<>();
        int turn = 0;
        while (numberOfStage > turn) {
            if (turn % 2 == 0) {
                groundOfDodo.offerLast(deckOfDodo.pollFirst());

            } else {
                groundOfSuyeon.offerLast(deckOfSuYeon.pollFirst());

            }
            if (deckOfDodo.isEmpty()) {
                break;
            }
            if (deckOfSuYeon.isEmpty()) {
                break;
            }
            if ((!groundOfDodo.isEmpty() && groundOfDodo.peekLast() == 5) || (!groundOfSuyeon.isEmpty() && groundOfSuyeon.peekLast() == 5)) {
                deckOfDodo.addAll(groundOfSuyeon);
                groundOfSuyeon.clear();
                deckOfDodo.addAll(groundOfDodo);
                groundOfDodo.clear();
            }

            if ((!groundOfDodo.isEmpty() && !groundOfSuyeon.isEmpty())
                    && groundOfDodo.peekLast() + groundOfSuyeon.peekLast() == 5) {
                deckOfSuYeon.addAll(groundOfDodo);
                groundOfDodo.clear();
                deckOfSuYeon.addAll(groundOfSuyeon);
                groundOfSuyeon.clear();
            }
            turn++;
        }
        String answer = judge(deckOfDodo, deckOfSuYeon);
        System.out.println(answer);

    }

    public static String judge(Deque<Integer> deckOfDodo, Deque<Integer> deckOfSuyeon) {
        if (deckOfDodo.size() > deckOfSuyeon.size()) {
            return DODO_WIN;
        }
        if (deckOfDodo.size() < deckOfSuyeon.size()) {
            return SUYEON_WIN;
        }
        return DRAW;
    }

}

 

종을 칠 수 있는 조건은 매 턴마다 검사해야하므로 공통으로 넣고 현재 시행 횟수의 홀수 또는 짝수 여부를 통해 그라운드에 카드를 내는 순서를 정한다.

'코딩테스트 > boj' 카테고리의 다른 글

줄어들지 않아 - 백준 2688  (0) 2024.05.22
떡 먹는 호랑이 - 백준 2502 자바  (0) 2024.05.22
별 찍기 11 - 2448  (0) 2024.03.30
벽 부수고 이동하기 - 백준 2206  (0) 2024.03.30
램프 - 백준 1034  (0) 2024.03.07

+ Recent posts