14502번: 연구소 (acmicpc.net)

 

0 빈칸, 1 벽, 2 바이러스의 위치

바이러스가 빈칸을 통해 퍼져나간다. 바이러스가 퍼질 수 없는 영역을 안전 영역이라고 한다.

이때 벽을 3개 추가로 세워 안전 영역 크기의 최댓값을 구하는 프로그램을 작성하라.

 

현재 map 상태에서 바이러스를 퍼트린 다음 0의 개수를 세면 된다.

바이러스는 DFS나 BFS를 사용해서 퍼트리면 될 것이다.

문제는 벽을 3개 추가해야 한다는 것이다.

N, M 의 최대 값이 8이기 때문에 완전탐색을 사용해도 될 것 같다.

  1. 0인 곳을 3곳 선택해 벽을 세운다.
  2. 바이러스를 퍼트린다.
  3. 0인 지점의 수를 센다.

이 과정이 3중 루프 동안 반복이 되는 것이다.

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {

    static FastReader scan = new FastReader();

    static int N, M;
    static int[][] map;
    static int[][] DIRECTION = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    static List<int[]> VIRUS;
    static List<int[]> ZEROS;
    static int safeZone = Integer.MIN_VALUE;

    static void input() {
        N = scan.nextInt();
        M = scan.nextInt();
        map = new int[N][M];
        ZEROS = new ArrayList<>();
        VIRUS = new ArrayList<>();

        for (int row = 0; row < N; row++) {
            for (int col = 0; col < M; col++) {
                map[row][col] = scan.nextInt();
                if (map[row][col] == 0) {
                    ZEROS.add(new int[]{row, col});
                }
                else if (map[row][col] == 2) {
                    int[] virus = new int[]{row, col};
                    VIRUS.add(virus);
                }
            }
        }
    }

    static void bfs( int[][] map) {
        Queue<int[]> q = new LinkedList<>();
        for (int[] virus : VIRUS) {
            q.add(virus);
        }

        while (!q.isEmpty()) {
            int[] now = q.poll();

            for (int d = 0; d < 4; d++) {
                int nr = now[0] + DIRECTION[d][0];
                int nc = now[1] + DIRECTION[d][1];

                if (nr < 0 || nr >= N || nc < 0 || nc >= M) continue;
                if (map[nr][nc] != 0) continue;

                map[nr][nc] = 2;
                q.add(new int[]{nr, nc});
            }
        }
    }

    static void renewSafeZone(int[][] map) {
        int count = (int)Arrays.stream(map).flatMapToInt(Arrays::stream)
                .filter(num -> num == 0)
                .count();
        safeZone = Math.max(safeZone, count);
    }

    static void buildWall(int i, int j, int k, int[][] map) {
        map[ZEROS.get(i)[0]][ZEROS.get(i)[1]] = 1;
        map[ZEROS.get(j)[0]][ZEROS.get(j)[1]] = 1;
        map[ZEROS.get(k)[0]][ZEROS.get(k)[1]] = 1;
    }

    static int[][] deepCopy(int[][] map) {
        return Arrays.stream(map)
                .map(int[]::clone)
                .toArray(int[][]::new);
    }

    public static void main(String[] args) {
        input();
        for (int i = 0; i < ZEROS.size() - 2; i++) {
            for (int j = i + 1; j < ZEROS.size() - 1; j++) {
                for (int k = j + 1; k < ZEROS.size(); k++) {
                    int[][] copyMap = deepCopy(map);
                    buildWall(i, j, k, copyMap);
                    bfs(copyMap);
                    renewSafeZone(copyMap);
                }
            }
        }

        System.out.println(safeZone);
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }

        public FastReader(String s) throws FileNotFoundException{
            br = new BufferedReader(new FileReader(s));
        }

        String next() {
            while (st == null || !st.hasMoreElements()) {
                try {
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return st.nextToken();
        }

        int nextInt() {
            return Integer.parseInt(next());
        }
    }
}

'Algorithm > BOJ' 카테고리의 다른 글

BOJ 14888 - 연산자 끼워넣기  (1) 2024.10.10
BOJ 14503 - 로봇 청소기  (2) 2024.10.10
BOJ 14501 - 퇴사  (1) 2024.10.09
BOJ 14500 - 테트로미노  (1) 2024.10.09
BOJ 14499 - 주사위 굴리기  (1) 2024.10.09

14501번: 퇴사 (acmicpc.net)

 

N일 동안 상담을 진행한다.

각 날 별로 걸리는 기간(T)과 상담을 했을 때 받을 수 있는 금액(P)으로 이루어져 있다

예를 들어 1일에 T = 3이고 P = 10이라고 해보자.

1일에 잡혀있는 상담은 총 3일이 걸리며 상담 시 받을 수 있는 금액은 10이다.

T가 1보다 클 수 있기 때문에 모든 상담을 할 수는 없다. 예를 들어 1일 상담을 하기로 결정하면 2, 3일은 상담을 할 수 없다.

또한 N + 1일에는 회사에 없기 때문에 그것도 고려해서 상담을 결정해야 한다.

상담 일정을 적절히 골라 최대 수익을 구하라.

 

일단 확실한 건 마지막 N일의 상담은 T가 1이 아니면 선택할 수 없다.

해야 할 일은 그날 상담을 할지 말지를 결정해야 한다.

우선은 완전탐색을 고려할 수 있다. 그런데 이 경우 중복된 계산이 빈번하게 등장한다.

이 중복 된 계산은 저장해 두면 속도 향상이 될 것이다.

결국에는 DP 문제이다. 최대 수익을 저장하면 될 것이다.

그런데 이전의 선택이 다음의 선택에 영향을 주고 있다.

그렇다면 뒤에서부터 접근하면 날짜 결정 이후의 결과에 영향을 주지 않을 것이다.

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

    static FastReader scan = new FastReader();
    static int N;
    static Consult[] consults;

    static class Consult {
        int T;
        int P;

        public Consult(int day, int price) {
            this.T = day;
            this.P = price;
        }
    }

    static void input() {
        N = scan.nextInt();
        consults = new Consult[N + 1];
        for (int i = 1; i <= N; i++) {
            int day = scan.nextInt();
            int price = scan.nextInt();

            consults[i] = new Consult(day, price);
        }
    }

    public static void main(String[] args) {
        input();

        int[] DP = new int[N + 2];

        for (int day = N; day >= 1; day--) {
            if (day + consults[day].T <= N + 1) {
                // day의 상담이 가능한 경우 -> 현재 상담 + 최대 수익 vs 현재 상담 x
                DP[day] = Math.max(consults[day].P + DP[day + consults[day].T], DP[day + 1]);
            } else {
                DP[day] = DP[day + 1];
            }
        }

        System.out.println(DP[1]);
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }

        public FastReader(String s) throws FileNotFoundException{
            br = new BufferedReader(new FileReader(s));
        }

        String next() {
            while (st == null || !st.hasMoreElements()) {
                try {
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return st.nextToken();
        }

        int nextInt() {
            return Integer.parseInt(next());
        }
    }
}

'Algorithm > BOJ' 카테고리의 다른 글

BOJ 14503 - 로봇 청소기  (2) 2024.10.10
BOJ 14502 - 연구소  (0) 2024.10.10
BOJ 14500 - 테트로미노  (1) 2024.10.09
BOJ 14499 - 주사위 굴리기  (1) 2024.10.09
BOJ 13458 - 시험 감독  (1) 2024.10.09

14500번: 테트로미노 (acmicpc.net)

 

폴리오미노란 크기가 1x1인 정사각형을 여러 개 이어서 붙인 도형이며 다음을 만족해야 한다.

  • 정사각형은 서로 겹치면 안 된다.
  • 도형은 모두 연결되어 있어야 한다.
  • 정사각형의 변끼리 연결되어 있어야 한다. 즉 꼭짓점과 꼭짓점만 맞닿아 있으면 안 된다.

NxM인 종이 위에 4개의 정사각형으로 이루어진 폴리오미노인 테트로미노 하나를 놓으려 한다.

적절히 놓아 칸에 쓰여 있는 수들의 합을 최대를 찾아라

 

일종의 그래프 문제이다. 연결되어 있다는 것 자체가 두 노드 사이에 간선이 존재한다는 뜻이다.

꼭짓점 조건에 따라 대각선은 제외되고 상하좌우만 판단하게 된다.

예를 들어 테트로미노는 시작점 1개를 기준으로 총 3개의 노드를 상하좌우로 탐색해 나간 것이다.

단순한 방법으로는 NxM 크기의 배열을 순회하며 시작점을 정하고 해당 시작점에서 탐색을 하는 방법이 있다. 테트로미노이기 때문에 4개의 노드를 탐색하면 된다.

 

그런데 여기서 문제가 발생한다.

dfs로 탐색하게 되면 T 모양의 형태를 탐색할 수가 없다.

T모양을 어떻게 판단할 수 있을까? 그냥 별도로 처리하자.

나머지 모든 모양들은 dfs로 처리가 가능하며 T모양만 별도로 구성해 처리를 하겠다.

T모양을 어떻게 별도로 처리할까?

우선 현재 위치에서 상하좌우 중 3방향을 고르면 된다.

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

    static FastReader scan = new FastReader();
    static int N, M;
    static int[][] board;
    static int[][] DIRECTION = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    static int max = 0;

    static void input() {
        N = scan.nextInt();
        M = scan.nextInt();
        board = new int[N][M];
        for (int r = 0; r < N; r++) {
            for (int c = 0; c < M; c++) {
                board[r][c] = scan.nextInt();
            }
        }
    }

    static void dfs(int depth, int r, int c, int sum, boolean[][] visited) {
        if (depth == 4) {
            max = Math.max(max, sum);
            return;
        }

        for (int d = 0; d < 4; d++) {
            int nr = r + DIRECTION[d][0];
            int nc = c + DIRECTION[d][1];

            if (nr < 0 || nr >= N || nc < 0 || nc >= M) {
                continue;
            }
            if (visited[nr][nc]) {
                continue;
            }
            visited[nr][nc] = true;
            dfs(depth + 1, nr, nc, sum + board[nr][nc], visited);
            visited[nr][nc] = false;
        }
    }

    static void checkTShape(int r, int c) {
        // 현재 위치 r,c 에서 3가지 방향을 정해야한다. -> 어차피 최대를 찾아야 하므로 4방향을 다 보고 최소값을 -1 하자.
        int min = Integer.MAX_VALUE;
        int count = 0;
        int sum = board[r][c];

        for (int d = 0; d < 4; d++) {
            int nr = r + DIRECTION[d][0];
            int nc = c + DIRECTION[d][1];

            if (nr < 0 || nr >= N || nc < 0 || nc >= M) continue;
            sum += board[nr][nc];
            min = Math.min(min, board[nr][nc]);
            count++;
        }

        if (count == 3) {
            // 딱 T 모양을 만드는 경우
            max = Math.max(sum, max);
        } else if (count == 4) {
            // 4방향 모두 탐색 가능
            max = Math.max(sum - min, max);
        }
    }

    public static void main(String[] args) {
        input();
        boolean[][] visited = new boolean[N][M];
        for (int r = 0; r < N; r++) {
            for (int c = 0; c < M; c++) {
                visited[r][c] = true;
                dfs(1, r, c, board[r][c], visited);
                visited[r][c] = false;
                checkTShape(r, c);
            }
        }

        System.out.println(max);
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }

        public FastReader(String s) throws FileNotFoundException {
            br = new BufferedReader(new FileReader(s));
        }

        public String next() {
            while (st == null || !st.hasMoreElements()) {
                try {
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return st.nextToken();
        }

        public int nextInt() {
            return Integer.parseInt(next());
        }
    }

}

'Algorithm > BOJ' 카테고리의 다른 글

BOJ 14502 - 연구소  (0) 2024.10.10
BOJ 14501 - 퇴사  (1) 2024.10.09
BOJ 14499 - 주사위 굴리기  (1) 2024.10.09
BOJ 13458 - 시험 감독  (1) 2024.10.09
BOJ 3190- 뱀  (2) 2024.10.08

14499번: 주사위 굴리기 (acmicpc.net)

 

NxM 크기의 지도에 주사위를 굴린다.

지도의 값이 0이면 주사위의 바닥면의 숫자가 지도로 복사되고 지도의 값이 0이 아니면 주사위의 바닥면에 지도의 숫자가 덮어씌워지며 지도의 숫자는 0이 된다.

매 이동시마다 주사위 상단에 쓰여 있는 값을 구하는 프로그램을 작성해라.

범위를 벗어나는 명령은 무시하라.

이동 명령에 따라 주사위의 바닥과 상단을 파악해야 한다.

초기에 바닥은 1이고 상단을 6 그리고 3이 동쪽을 바라보고 있는 상태이다.

주사위는 총 6개의 값을 가진 2차원 평면도로 표현할 수 있다.

[0, 2, 0]
[4, 1, 3]  
[0, 5, 0]
[0, 6, 0]

여기서 동, 서, 남, 북으로 이동을 평면도 상에서 표시해 보자. 동/서 와 남/북은 이동의 방향이 다르기 때문에 따로 생각해야 할 것 같다.

가로 방향 이동인 동쪽으로 3번 움직여보자. 4번 움직이면 원상복구 되니 규칙이 보일 것이다.

[0, 2, 0]     [0, 2, 0]     [0, 2, 0]     [0, 2, 0]
[4, 1, 3]     [1, 3, 6]     [3, 6, 4]     [6, 4, 1] 
[0, 5, 0]     [0, 5, 0]     [0, 5, 0]     [0, 5, 0]
[0, 6, 0]     [0, 4, 0]     [0, 1, 0]     [0, 3, 0]

규칙을 보면 1열이 좌로 1회 회전한 다음 [1][2] 와 [3][1]이 교환되는 형태이다.

1의 이동 : [1][1] → [1][0] → [3][1] → [1][2]

3의 이동: [1][2] → [1][1] → [1][0] → [3][1]

4의 이동: [1][0] → [3][1] →[1][2] → [1][1]

6의 이동: [3][1] → [1][2] → [1][1] → [1][0]

이렇게 4개의 위치가 반복되고 있다. 서쪽은 그저 화살표 방향이 반대인 것이다.

자세히 살펴보면 회전하고 있다는 것을 알 수 있다. [4, 1, 3, 6] 배열이 회전하는 것인데 마지막 위치의 숫자만 [3][1]에 존재하는 것이다.

이번에는 북쪽으로 3번 움직여보자

[0, 2, 0]     [0, 6, 0]     [0, 5, 0]     [0, 1, 0]
[4, 1, 3]     [4, 2, 3]     [4, 6, 3]     [4, 5, 3] 
[0, 5, 0]     [0, 1, 0]     [0, 2, 0]     [0, 6, 0]
[0, 6, 0]     [0, 5, 0]     [0, 1, 0]     [0, 2, 0]

북/남 이동도 역시 1열이 그냥 1칸씩 회전하는 것이다. 북쪽 이동은 아래로 1칸 회전, 남쪽 이동은 위로 1칸 이동이다.

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;

public class Main {

    static FastReader scan = new FastReader();
    static int N, M, initR, initC, K;
    static int[][] map;
    static int[] ORDERS; // 동쪽 1, 서쪽 2, 북쪽 3, 남쪽 4
    static final int[][] DIRECTION = {{0, 0}, {0, 1}, {0, -1}, {-1, 0}, {1, 0}}; // x, 동, 서, 북, 남

    static class Dice {
        int[] row = new int[4];
        int[] col = new int[4];
        int r;
        int c;

        public Dice(int r, int c) {
            this.r = r;
            this.c = c;
        }

        public void move(int dir) {
            boolean rowMove = false;
            int nr = r + DIRECTION[dir][0];
            int nc = c + DIRECTION[dir][1];

            if (nr < 0 || nr >= N || nc < 0 || nc >= M) return;

            if (dir == 1) {
                rotateLeft(row);
                rowMove = true;
            } else if (dir == 2) {
                rotateRight(row);
                rowMove = true;
            } else if (dir == 3) {
                rotateLeft(col);
            } else {
                rotateRight(col);
            }

            this.r = nr;
            this.c = nc;

            // 상,하 동기화
            if (rowMove) {
                col[1] = row[1];
                col[3] = row[3];
            } else {
                row[1] = col[1];
                row[3] = col[3];
            }

            // 회전해서 도착지에 도달한 상태 -> 업데이트 필요
            if (map[nr][nc] == 0) {
                map[nr][nc] = getBottom();
            } else {
                setBottom(map[nr][nc]);
                map[nr][nc] = 0;
            }

            System.out.println(getTop());
        }

        public int getTop() {
            return row[1];
        }

        public int getBottom() {
            return row[3];
        }

        public void setBottom(int value) {
            row[3] = value;
            col[3] = value;
        }

        private void rotateLeft(int[] arr) {
            // arr 왼쪽으로 1칸 회전
            reverse(arr, 0, arr.length - 1);
            reverse(arr, 0, arr.length - 2);
        }

        private void rotateRight(int[] arr) {
            // arr 오른쪽으로 1칸 회전
            reverse(arr, 0, arr.length - 1);
            reverse(arr, 1, arr.length - 1);
        }

        private void reverse(int[] arr, int from, int to) {
            while (from < to) {
                int tmp = arr[from];
                arr[from] = arr[to];
                arr[to] = tmp;
                from++;
                to--;
            }
        }
    }

    static void input() {
        N = scan.nextInt();
        M = scan.nextInt();
        initR = scan.nextInt();
        initC = scan.nextInt();
        K = scan.nextInt();

        map = new int[N][M];
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                map[i][j] = scan.nextInt();
            }
        }

        ORDERS = new int[K];
        for (int i = 0; i < K; i++) {
            ORDERS[i] = scan.nextInt();
        }
    }

    public static void main(String[] args) {
        input();
        Dice dice = new Dice(initR, initC);
        for (int dir : ORDERS) {
            dice.move(dir);
        }
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }

        public FastReader(String s) throws FileNotFoundException {
            br = new BufferedReader(new FileReader(new File(s)));
        }

        String next() {
            while (st == null || !st.hasMoreElements()) {
                try {
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return st.nextToken();
        }

        int nextInt() {
            return Integer.parseInt(next());
        }
    }
}

'Algorithm > BOJ' 카테고리의 다른 글

BOJ 14501 - 퇴사  (1) 2024.10.09
BOJ 14500 - 테트로미노  (1) 2024.10.09
BOJ 13458 - 시험 감독  (1) 2024.10.09
BOJ 3190- 뱀  (2) 2024.10.08
BOJ 12100 - 2048  (0) 2024.10.07

13458번: 시험 감독 (acmicpc.net)

 

N개의 시험장. i번 시험장에 있는 응시자 수 Ai명

감독관은 총감독관과 부감독관이 있다.

총감독관은 한 시험장에서 B명을 감시할 수 있고 부감독관은 C명을 감시할 수 있다.

각 시험장에 총 감독관은 오직 1명만 있어야 하고 부감독관은 여러 명 있어도 된다.

모든 응시생을 감시하기 위해 필요한 감독관 수의 최솟값은?

 

감독관 수의 최솟값이라는 것은 한 감독관이 최대한 많은 응시자를 감시해야 한다.

우선 총감독관은 시험장에 반드시 1명만 있어야 한다. 총감독관 혼자 해당 시험장을 감시할 수 있다면 부감독관은 투입하지 않아도 된다.

우선은 N명의 총감독관은 반드시 들어간다. 따라서 응시자 수에서 B를 일관적으로 뺀다.

그 후 남아있는 응시자 수를 C로 나눈다. 이때 올림 처리 한 것이 필요한 부감독관 수이다.

이 과정을 통해 O(N)의 시간복잡도로 해결 가능할 것이다.

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

public class Main {

    static FastReader scan = new FastReader();
    static int N, B, C;
    static int[] rooms;

    static void input() {
        N = scan.nextInt();
        rooms = new int[N];
        for (int i = 0; i < N; i++) {
            rooms[i] = scan.nextInt();
        }
        B = scan.nextInt();
        C = scan.nextInt();
    }

    public static void main(String[] args) {
        input();
        long answer = 0;
        answer += N;

        for (int i = 0; i < N; i++) {
            double remain = rooms[i] - B > 0 ? rooms[i] - B : 0;
            answer += Math.ceil(remain / C);
        }

        System.out.println(answer);
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }

        String next() {
            while (st == null || !st.hasMoreElements()) {
                try {
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return st.nextToken();
        }

        int nextInt() {
            return Integer.parseInt(next());
        }
    }
}

여기서 정말 중요한 것이 하나 있다.

바로 필요한 감독관 수를 저장하는 answer 변수의 타입이다.

Ai가 최대 1,000,000이고 N도 최대 1,000,000이다.

때문에 총 필요한 감독관의 최대 수는 1,000,000 * 1,000,000 = 1,000,000,000,000이 된다. 이는 int 범위를 넘어서는 수이므로 long으로 처리해야 한다.

'Algorithm > BOJ' 카테고리의 다른 글

BOJ 14500 - 테트로미노  (1) 2024.10.09
BOJ 14499 - 주사위 굴리기  (1) 2024.10.09
BOJ 3190- 뱀  (2) 2024.10.08
BOJ 12100 - 2048  (0) 2024.10.07
BOJ 13460 - 구슬탈출2  (0) 2024.10.06

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

 

사과를 먹으면 뱀 길이가 늘어난다.

뱀이 벽 또는 자기 자신의 몸과 부딪히면 게임이 끝난다.

사과 위치와 명령어가 주어졌을 때 게임이 몇 초에 끝나는지 출력하라.

 

관리해야 하는 상태를 생각해보자. 일단 현재 보드판의 상태, 진행 시간이 필요하다.

그리고 뱀의 위치, 사과의 위치를 관리해야 한다.

게임은 매 초 뱀의 머리가 이동한다. 초기 방향은 오른쪽이다.

→ 무한 루프를 사용해 이동한다.

이벤트는 사과를 먹는 이벤트와 벽에 닿는 이벤트 그리고 자신의 몸에 닿는 이벤트가 있다.

이 중 뒤의 두 개의 이벤트는 무한 루프를 종료시키는 이벤트이다.

이동이라는 부분에 대해 생각해보자. 머리만 이동하는 게 아니라 몸통도 따라서 움직어야 한다.

이동이라는 것은 앞의 것이 이동하면 그 뒤에 있는 것이 앞의 것이 있던 위치로 변경되는 작업을 말한다.

제일 앞의 것 즉 머리의 이동만 처리하면 나머지는 그냥 반복을 통한 작업 일 뿐이다.

 

이는 Queue를 통해 관리할 수 있을 것 같다.

Queue의 내부에 들어갈 뱀의 정보는 어떤 식으로 관리를 할 까? int [ ]로 위치를 관리하는 방법과 명시적으로 객체를 하나 만들어 관리하는 방법이 있겠다.

개인적으로는 시각적으로 확실하게 무슨 뜻인지 알 수 있으니까 객체를 만드는 방법을 선호한다.

이동에는 두 가지가 존재한다. 기존 방향으로 계속 이동과 방향을 꺾는 이동이다.

이를 위해서는 기존에 이동하던 방향을 알고 있어야 한다.

머리의 이동과 동시에 위의 세 가지 이벤트를 확인할 수 있다.

  1. 머리가 이동한 위치에 사과가 있는 경우 몸통의 길이가 늘어 나야 한다. Queue에 몸통 하나를 추가해 준다. prev를 관리하고 있기 때문에 문제없다.
  2. 머리가 이동한 위치가 벽인 경우 머리가 범위를 벗어난 경우이다. 이 때도 게임을 종료하면 된다.
  3. 머리가 이동한 위치가 몸통인 경우 이 경우도 그냥 머리가 이동한 위치가 공백도 사과도 아닐 경우 게임을 종료하면 된다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;

public class Main{

    static FastReader scan = new FastReader();
    static int N, L;
    static int[][] board;
    static Map<Integer, String> orders;
    static int[][] DIRECTION = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; //상좌하우
    static final int APPLE = 2;
    static final int SNAKE = 1;

    static class Snake {
        final int r;
        final int c;
        final int dir;
        final boolean head;

        public Snake(int r, int c, int dir, boolean head) {
            this.r = r;
            this.c = c;
            this.dir = dir;
            this.head = head;
        }

        Snake changeDirection(String dir) {
            int newDir = 0;
            if (dir.equals("L")) {
                newDir = (this.dir + 1) % 4;
            } else {
                newDir = (this.dir + 3) % 4;
            }
            return new Snake(this.r, this.c, newDir, this.head);
        }

        Snake moveHead() {
            int nr = this.r + DIRECTION[dir][0];
            int nc = this.c + DIRECTION[dir][1];
            return new Snake(nr, nc, dir, true);
        }

        Snake movePart(Snake prev) {
            return new Snake(prev.r, prev.c, dir, false);
        }
    }

    static class Board {
        int[][] board;
        Deque<Snake> snakes;
        int time;
        boolean isEnd;

        Board move(String dir) {
            int[][] newBoard = deepCopy(board);
            Deque<Snake> newSnakes = new ArrayDeque<>(snakes);

            if (dir != null) {
                Snake head = newSnakes.pollFirst();
                newSnakes.addFirst(head.changeDirection(dir));
            }

            Snake prev = null;
            boolean expandable = false;
            for (int i = 0; i < newSnakes.size(); i++) {
                Snake part = newSnakes.pollFirst();
                Snake moved = null;
                if (part.head) {
                    moved = part.moveHead();
                    if (moved.r < 0 || moved.r >= N || moved.c < 0 || moved.c >= N
                        || newBoard[moved.r][moved.c] == SNAKE) {
                        // 범위 밖 -> break;
                        isEnd = true;
                        break;
                    }
                    if (newBoard[moved.r][moved.c] == APPLE) {
                        expandable = true;
                    }
                } else {
                    moved = part.movePart(prev);
                }
                prev = part;
                newSnakes.addLast(moved);
                newBoard[part.r][part.c] = 0;
                newBoard[moved.r][moved.c] = 1;
            }
            if (expandable) {
                newSnakes.addLast(new Snake(prev.r, prev.c, prev.dir, false));
                newBoard[prev.r][prev.c] = 1;
            }

            return new Board(newBoard, newSnakes, time + 1, isEnd);
        }

        private void printBoard() {
            for (int[] row : board) {
                System.out.println(Arrays.toString(row));
            }
            System.out.println();
        }

        private int[][] deepCopy(int[][] board) {
            return Arrays.stream(board)
                    .map(int[]::clone)
                    .toArray(int[][]::new);
        }

        public Board(int[][] board, Deque<Snake> snakes, int time, boolean isEnd) {
            this.board = board;
            this.snakes = snakes;
            this.time = time;
            this.isEnd = isEnd;
        }
    }

    static void input() {
        N = scan.nextInt();
        board = new int[N][N];
        orders = new HashMap<>();
        int apples = scan.nextInt();
        for (int i = 0; i < apples; i++) {
            int r = scan.nextInt() - 1;
            int c = scan.nextInt() - 1;
            // 사과 위치
            board[r][c] = APPLE;
        }
        L = scan.nextInt();
        for (int i = 0; i < L; i++) {
            int t = scan.nextInt();
            String dir = scan.next();
            orders.put(t, dir);
        }
        board[0][0] = 1;
    }

    public static void main(String[] args) {
        input();
        Deque<Snake> snakes = new ArrayDeque<>();
        snakes.add(new Snake(0, 0, 3, true));
        Board state = new Board(board, snakes, 0, false);
        int t = 0;
        while (true) {
            Board nextState = null;
            if (orders.containsKey(t)) {
                // 방향전환 필요
                String dir = orders.get(t);
                nextState = state.move(dir);
            } else {
                // 방향전환 없이 이동
                nextState = state.move(null);
            }

            if (nextState.isEnd) {
                System.out.println(nextState.time);
                break;
            }

            state = nextState;
            t++;
        }
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }

        String next() {
            while (st == null || !st.hasMoreElements()) {
                try {
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return st.nextToken();
        }

        int nextInt() {
            return Integer.parseInt(next());
        }
    }
}

 

결과는 메모리 초과이다. 이동시마다 새로운 객체들을 너무 생성하고 있다.

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

public class Main {
    static FastReader scan = new FastReader();
    static int N, L;
    static int[][] board;
    static Map<Integer, Character> orders;
    static int[][] DIRECTION = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 동, 남, 서, 북
    static final int APPLE = 2;

    static void input() {
        N = scan.nextInt();
        board = new int[N + 1][N + 1];
        int K = scan.nextInt();
        for (int i = 0; i < K; i++) {
            int r = scan.nextInt();
            int c = scan.nextInt();
            board[r][c] = APPLE;
        }
        L = scan.nextInt();
        orders = new HashMap<>();
        for (int i = 0; i < L; i++) {
            int time = scan.nextInt();
            char dir = scan.next().charAt(0);
            orders.put(time, dir);
        }
    }

    public static void main(String[] args) {
        input();
        Deque<Position> snake = new ArrayDeque<>();
        Set<Position> snakeBody = new HashSet<>();
        int r = 1, c = 1;
        int dir = 0; // 초기 방향: 동쪽
        int time = 0;
        snake.addFirst(new Position(r, c));
        snakeBody.add(new Position(r, c));

        while (true) {
            time++;
            int nr = r + DIRECTION[dir][0];
            int nc = c + DIRECTION[dir][1];

            // 벽 충돌 체크
            if (nr <= 0 || nc <= 0 || nr > N || nc > N) {
                break;
            }

            // 자기 몸통과의 충돌 체크
            Position newHead = new Position(nr, nc);
            if (snakeBody.contains(newHead)) {
                break;
            }

            // 사과 여부 확인
            if (board[nr][nc] == APPLE) {
                board[nr][nc] = 0; // 사과 제거
            } else {
                // 꼬리 제거
                Position tail = snake.removeLast();
                snakeBody.remove(tail);
            }

            // 머리 추가
            snake.addFirst(newHead);
            snakeBody.add(newHead);

            // 방향 전환 체크
            if (orders.containsKey(time)) {
                char C = orders.get(time);
                if (C == 'L') {
                    dir = (dir + 3) % 4; // 왼쪽 회전
                } else {
                    dir = (dir + 1) % 4; // 오른쪽 회전
                }
            }

            // 현재 위치 업데이트
            r = nr;
            c = nc;
        }
        System.out.println(time);
    }

    static class Position {
        int x, y;
        public Position(int x, int y){
            this.x = x;
            this.y = y;
        }
        @Override
        public boolean equals(Object obj){
            if(obj instanceof Position){
                Position p = (Position) obj;
                return this.x == p.x && this.y == p.y;
            }
            return false;
        }
        @Override
        public int hashCode(){
            return Objects.hash(x, y);
        }
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;
        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }
        String next() {
            while (st == null || !st.hasMoreElements()) {
                try { st = new StringTokenizer(br.readLine()); }
                catch (IOException e) { e.printStackTrace(); }
            }
            return st.nextToken();
        }
        int nextInt() {
            return Integer.parseInt(next());
        }
    }
}

객체의 생성을 줄이고 그냥 이동로직을 main으로 변경했다.

'Algorithm > BOJ' 카테고리의 다른 글

BOJ 14500 - 테트로미노  (1) 2024.10.09
BOJ 14499 - 주사위 굴리기  (1) 2024.10.09
BOJ 13458 - 시험 감독  (1) 2024.10.09
BOJ 12100 - 2048  (0) 2024.10.07
BOJ 13460 - 구슬탈출2  (0) 2024.10.06

https://leetcode.com/problems/substring-with-concatenation-of-all-words

 

문자열 s, 같은 길이의 단어가 들어있는 String [] words

concatenated string이라는 것이 등장한다. 이는 words의 단어들을 순열로 만들어 하나로 합친 것들이다.

예를 들면 words = [”ab”, “cd”, “ef”] 일 때 이 배열의 순열들은 다음과 같다.

[”ab”, “cd”, “ef”], [”ab”, “ef”, “cd”], [”cd”, “ab”, “ef”], [”cd”, “ef”, “ab”], [”ef”, “ab”, “cd”], [”ef”, “cd”, “ab”] 이렇게 총 6가지이다.

따라서 concatenated string은 다음의 6가지가 된다.

“abcdef”, “abefcd”, “cdabef”, “cdefab”, “efabcd”, “efcdab”

문자열 S에서 모든 concatenated string의 substring 위치를 담은 리스트를 반환하라.

 

substring의 위치 같은 경우에는 indexOf를 사용하면 찾을 수 있을 것 같다.

결국 필요한 것은 words의 단어들로 순열을 만들어 concatenated string를 구성하는 것이다.

주의할 점은 words 내에 똑같은 단어가 여러 번 등장할 수 있다는 것이다.

이렇게 되면 순열을 구성시에 중복된 concatenated string이 등장할 수 있다. 따라서 중복처리가 필요하다.

중복처리는 그냥 간단하게 Set을 통해 처리하겠다.

또 다른 문제도 있다. 문자열 s에서 같은 substring이 2번 등장했을 때이다. indexOf는 처음으로 등장하는 substring(부분 문자열)의 index만을 반환한다.

이를 해결하려면 substring의 위치를 판단하는 메서드를 새롭게 작성하던가 indexOf 중 오버로딩 된 시작 위치를 변경해 가며 찾는 메서드를 사용하는 방식이 있다. 나는 후자를 선택했다.

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        Set<Integer> answer = new HashSet<>();
        permutation(0, words, new boolean[words.length], new StringBuilder(), answer, s);
        return answer.stream().collect(Collectors.toList());
    }

    private void permutation(int idx, String[] words, boolean[] visited, StringBuilder sb, Set<Integer> answer, String s) {
        if (idx == words.length) {
            // 마지막 위치까지 확인
            int from = 0;
            while (from < s.length()) {
                int substringIdx = s.indexOf(sb.toString(), from);
                if (substringIdx == -1) {
                    break;
                }
                answer.add(substringIdx);
                from = substringIdx + 1;
            }
            return;
        }

        for (int i = 0; i < words.length; i++) {
            if (visited[i]) continue;
            visited[i] = true;
            permutation(idx + 1, words, visited, new StringBuilder(sb.toString()).append(words[i]), answer, s);
            visited[i] = false;
        }
    }
}    

시간초과가 발생했다.

s = "fffffffffffffffffffffffffffffffff”

words = ["a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a"]

이 경우에서 실패했다.

그냥 위의 생각 자체가 틀렸다. 문제의 조건을 다시 보자. words 배열의 최대 길이는 5000이다.

그렇게 되면 만들어지는 수열은 최대 5000! 이 된다. 이는 절대로 위의 방식으로는 풀 수 없는 수치이다.

또한 indexOf로 substring을 검색하는 방식도 s가 길어질수록 비효율적으로 변한다.

 

다른 방식을 생각해 보자.

사용하지 않은 조건 중에 words의 모든 단어가 같은 길이를 갖는다는 조건이 있다.

여기서 얻을 수 있는 정보는 유효한 substring의 길이가 항상 일정하다는 것이다.

일정한 크기의 슬라이딩 윈도인가? 하는 생각이 든다.

또한 words의 모든 단어가 1번씩은 등장한다는 조건도 있다.

 

이를 통해서 생각을 정리해 보자.

해당 슬라이딩 윈도 크기 내에서 등장하는 words의 단어들의 빈도수를 알면 된다.

예를 들어 words = [”foo”, “bar”, “foo”] 이면 “foo”가 2번, “bar”가 1번 등장해야만 한다.

슬라이딩 윈도를 이동해 가면서 해당 범위 내에서 “foo”가 2번, “bar”가 1번 등장했는지 확인만 하면 된다.

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> answer = new ArrayList<>();
        Map<String, Integer> wordsMap = new HashMap<>();
        int size = words[0].length();
        int windowSize = words.length * size;
        
        for (String word : words) {
            wordsMap.put(word, wordsMap.getOrDefault(word, 0) + 1);
        }

        int left = 0;
        for (int right = windowSize; right <= s.length(); right++) {
            String substring = s.substring(left, right);
            // substring을 size(단어 길이) 단위로 분할
            int start = 0;
            Map<String, Integer> substringWordMap = new HashMap<>();
            while (start < substring.length()) {
                String word = substring.substring(start, start + size);
                substringWordMap.put(word, substringWordMap.getOrDefault(word, 0) + 1);
                start += size;
            }
            if (wordsMap.equals(substringWordMap)) answer.add(left);

            left++;
        } 
        
        return answer;
    }
}

정답은 통과됐다. 그런데 시간 효율이 엄청나게 별로다.

가장 좋은 시간 효율을 가진 사람에 비해 터무니없이 느리다.

 

여기서부터는 당장 개선 방법이 안 떠오르므로 gpt의 도움을 받아 정답 코드를 분석해 보자.

import java.util.*;

public class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> result = new ArrayList<>();
        if(s == null || s.length() == 0 || words == null || words.length == 0) return result;
        
        int wordLength = words[0].length();
        int totalLength = wordLength * words.length;
        
        // 단어 빈도 맵 생성
        Map<String, Integer> wordCount = new HashMap<>();
        for(String word : words){
            wordCount.put(word, wordCount.getOrDefault(word, 0) +1);
        }
        
        // 단어 길이만큼 이동하며 검사
        for(int i = 0; i < wordLength; i++){
            int left = i, right = i, count = 0;
            Map<String, Integer> currentCount = new HashMap<>();
            while(right + wordLength <= s.length()){
                String word = s.substring(right, right + wordLength);
                right += wordLength;
                if(wordCount.containsKey(word)){
                    currentCount.put(word, currentCount.getOrDefault(word, 0) +1);
                    count++;
                    
                    // 빈도 초과시 왼쪽 포인터 이동
                    while(currentCount.get(word) > wordCount.get(word)){
                        String leftWord = s.substring(left, left + wordLength);
                        currentCount.put(leftWord, currentCount.get(leftWord) -1);
                        left += wordLength;
                        count--;
                    }
                    
                    // 모든 단어 매칭시 결과에 추가
                    if(count == words.length){
                        result.add(left);
                    }
                } else {
                    // 일치하지 않는 단어일 경우 초기화
                    currentCount.clear();
                    count = 0;
                    left = right;
                }
            }
        }
        return result;
    }
}

위의 코드는 엄청나게 빠르다. 기존의 방식과 뭐가 차이나길래 이렇게 속도차이가 날까?

 

외부 루프가 한 단어의 길이만큼만 반복하고 있다.

이 내부에서 슬라이딩 윈도를 사용해 문자열 s를 검사하는 방식을 쓴다.

하지만 내 코드 같은 경우에는 외부 루프가 1씩 증가하며 모든 지점을 검사하고 있다.

슬라이딩 윈도가 유의미하게 사용된 곳이 없다.

한 단어의 길이만큼 반복해도 모든 가능한 위치에서 검사할 수 있는 이유는 단어들이 모두 같은 길이를 가지기 때문이다.

예를 들어 한 단어가 “foo” 이면

i = 0 일 때는 0, 3, 6, 7, 12를 시작 인덱스로 검사하고

i = 1 일 때는 1, 4, 7, 10, 13 i = 2 일 때는 2, 5, 8, 11, 14가 된다.

 

솔직히 실제 상황에서 이렇게 풀라 하면 못 풀 것 같다.

'Algorithm > LeetCode' 카테고리의 다른 글

LeetCode 228 - Summary Ranges  (0) 2024.10.14
LeetCode 36 - Valid Sudoku  (0) 2024.10.14
LeetCode 3 - Longest Substring Without Repeating Characters  (0) 2024.10.07
LeetCode 209 - Minimum Size Subarray Sum  (0) 2024.10.04
LeetCode 15 - 3Sum  (0) 2024.10.04

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

 

보드판 내의 전체 블록이 함께 이동한다. 이동 방향은 상, 하, 좌, 우이다.

이때 같은 값을 갖는 두 블록이 충돌하면 두 블록은 합쳐지게 된다.

단, 한 번의 이동에서 이미 합쳐진 블록은 또 다른 블록과 다시 합쳐질 수 없다.

NxN 크기의 보드판에서 최대 5번 이동해서 만들 수 있는 가장 큰 블록의 값

보드판의 상태 관리가 필요하다.

각 블록을 관리를 할 텐데 해당 블록에는 값, 보드판에서의 위치, 그리고 이미 합쳐진 상태인지의 상태가 필요하다.

이동 방향 또한 중요한데 예를 들어 같은 값이 3개가 같은 열에 존재한다고 하자. 이 때 위쪽으로 이동하는 경우 위의 2개가 합쳐진다. 즉 위쪽에 가까운 것들이 먼저 합쳐진다.

또한 이동 횟수의 최댓값이 5로 정해져 있다.

 

백트랙킹 방법을 쓰면 될 것 같다.

백트랙킹의 탈출 조건이 뭘까 이동 횟수가 탈출 조건이 될 것이다. 이동 횟수가 5가 되면 최댓값을 갱신하고 탈출하면 된다.

또한 방문 처리를 해야 불필요한 반복을 하지 않는다. 하지만 이 방문을 어떤 기준으로 해야 할까?

그렇지만 이 방문 처리는 블록 내에 존재하는 모든 블록들을 통해 해야만 한다. N이 커질수록 블록의 개수가 NxN개가 되므로 곤란해진다.

최대 이동 횟수가 5회이기 때문에 방문처리를 안 하는 것도 방법일 수 있겠다.

그런데 블록을 어떻게 움직일 것인가.

  1. 그냥 각각의 블록들을 독립적으로 이동시켜 Queue에 쌓는다.
  2. 해당 Queue를 이동 거리에 따라 정렬한다.
  3. 앞에서부터 꺼낸다.
  4. 꺼내면서 값이 같으면 합치고 합쳐진 상태를 이전 것으로 기록한다.
  5. 다음 것을 꺼냈는데 이전 것과 같다면 이전것과 비교를 통해 합치거나 위치를 재정렬한다.

그런데 이 방법은 위치 조정이 복잡해질 것 같다. 또한 이전 블록과 비교해야 하는데 이거는 이동 방향에 따라 같은 열, 같은 행 등을 관리해줘야 한다.

따라서 그냥 블록을 이동방향에 따라 정렬하고 새로운 보드를 만들어 이동방향의 경계에 가까운 순서대로 배치해 나가자.

그런데 보드의 상태를 관리하는 클래스 내부에 보드판을 어떤 자료구조로 가지고 있어야 할까?

Block [][]과 int [][]의 선택지가 있다. Block의 경우 해당 위치에 블록의 병합 여부를 판단하기 쉬워지지만 객체를 제어해야 한다는 점에서 불편함이 늘어난다.

실수의 여지를 줄이기 위해 Block에서 병합 여부를 분리하고 별도의 boolean [][]을 사용해 관리하겠다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.StringTokenizer;
import java.util.stream.Collectors;

public class Test {

    static FastReader scan = new FastReader();
    static int N;
    static int[][] grid;
    static int maxValue = Integer.MIN_VALUE;
    static final int[][] DIRECTION = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    // 보드판의 상태
    static class Board {
        final int[][] board;
        final List<Block> blocks;
        final int maxValue;
        final int moveCount;

        public Board(int[][] board, List<Block> blocks, int maxValue, int moveCount) {
            this.board = board;
            this.blocks = blocks;
            this.maxValue = maxValue;
            this.moveCount = moveCount;
        }

        void printBoard(int[][] board) {
            for (int[] row : board) {
                System.out.println(Arrays.toString(row));
            }
            System.out.println();
        }

        Board move(int dir) {
            //이동 방향을 전달받아 현재 보드판의 상태를 보고 이동시킨 후 새로운 보드판을 리턴
            int[][] newBoard = new int[N][N];
            boolean[][] merged = new boolean[N][N];

            List<Block> sortedBlock = sortedBlockByDir(this.blocks, dir);

            for (Block block : sortedBlock) {
                Block movedBlock = moveBlock(block, dir, newBoard);
                int r = movedBlock.r;
                int c = movedBlock.c;
                int value = movedBlock.value;
                if (newBoard[r][c] != 0) {
                    // 이미 존재한다.
                    if (newBoard[r][c] == value && !merged[r][c]) {
                        // 값이 같은데 아직 병합이 안된 칸
                        newBoard[r][c] = value * 2;
                        merged[r][c] = true;
                    } else {
                        // 값이 다르거나 병합이 이미 됐거나 -> 위치 조정
                        newBoard[r - DIRECTION[dir][0]][c - DIRECTION[dir][1]] = value;
                    }
                } else {
                    // 빈 공간
                    newBoard[r][c] = value;
                }
            }
//            printBoard(newBoard);
            // 새로운 보드가 완성된 상태
            List<Block> newBlocks = new ArrayList<>();
            int maxValue = this.maxValue;
            for (int i = 0; i < N; i++) {
                for (int j = 0; j < N; j++) {
                    if (newBoard[i][j] == 0) {
                        continue;
                    }
                    maxValue = Math.max(maxValue, newBoard[i][j]);
                    Block block = new Block(i, j, newBoard[i][j]);
                    newBlocks.add(block);
                }
            }

            return new Board(newBoard, newBlocks, maxValue, moveCount + 1);
        }

        private Block moveBlock(Block block, int dir, int[][] board) {
            int r = block.r;
            int c = block.c;

            while (isValidPosition(r + DIRECTION[dir][0], c + DIRECTION[dir][1]) && board[r][c] == 0) {
                r += DIRECTION[dir][0];
                c += DIRECTION[dir][1];
            }

            return new Block(r, c, block.value);
        }

        private List<Block> sortedBlockByDir(List<Block> blocks, int dir) {
            if (dir == 0) {
                // 상 -> r오름차순
                return blocks.stream().sorted(Comparator.comparingInt(block -> block.r)).collect(Collectors.toList());
            } else if (dir == 1) {
                // 하 -> r내림차순
                return blocks.stream().sorted(Comparator.comparingInt(block -> -block.r)).collect(Collectors.toList());
            } else if (dir == 2) {
                // 좌 -> c오름차순
                return blocks.stream().sorted(Comparator.comparingInt(block -> block.c)).collect(Collectors.toList());
            } else {
                // 우 -> c내림차순
                return blocks.stream().sorted(Comparator.comparingInt(block -> -block.c)).collect(Collectors.toList());
            }
        }

        private boolean isValidPosition(int r, int c) {
            return r >= 0 && r < N && c >= 0 && c < N;
        }
    }

    // 보드판 내 블록
    static class Block {
        final int r;
        final int c;
        final int value;

        public Block(int r, int c, int value) {
            this.r = r;
            this.c = c;
            this.value = value;
        }
    }

    static void dfs(Board board) {
        if (board.moveCount >= 5) {
            maxValue = Math.max(maxValue, board.maxValue);
            return;
        }

        for (int dir = 0; dir < 4; dir++) {
            dfs(board.move(dir));
        }
    }

    static void input() {
        N = scan.nextInt();
        grid = new int[N][N];
    }

    public static void main(String[] args) {
        input();
        List<Block> blocks = new ArrayList<>();
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                grid[i][j] = scan.nextInt();
                if (grid[i][j] == 0) {
                    continue;
                }
                maxValue = Math.max(grid[i][j], maxValue);
                blocks.add(new Block(i, j, grid[i][j]));
            }
        }

        Board board = new Board(grid, blocks, maxValue, 0);
        dfs(board);

        System.out.println(maxValue);
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }

        String next() {
            while (st == null || !st.hasMoreElements()) {
                try {
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return st.nextToken();
        }

        String nextLine() {
            String str = "";
            try {
                str = br.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return str;
        }

        int nextInt() {
            return Integer.parseInt(next());
        }
    }
}

'Algorithm > BOJ' 카테고리의 다른 글

BOJ 14500 - 테트로미노  (1) 2024.10.09
BOJ 14499 - 주사위 굴리기  (1) 2024.10.09
BOJ 13458 - 시험 감독  (1) 2024.10.09
BOJ 3190- 뱀  (2) 2024.10.08
BOJ 13460 - 구슬탈출2  (0) 2024.10.06

https://leetcode.com/problems/longest-substring-without-repeating-characters

 

문자열 s가 주어졌을 때 반복되는 문자가 등장하지 않는 가장 긴 substring을 반환하라.

 

무선 문자의 등장 여부를 관리할 필요가 있겠다. 이는 Set을 통해 관리하면 1개만 등장하는지 여부를 빠르게 판단할 수 있을 것 같다.

이제는 substring을 판단해야 한다. 이는 슬라이딩 윈도를 통해 판단하겠다.

이 슬라이딩 윈도우를윈도를 확장하다 더 이상 확장할 수 없을 때 즉, 중복 단어가 등장했을 때 기존에 해당 단어가 위치했던 곳 다음을 시작점으로 다시 윈도를 확장한다.

class Solution {
    public int lengthOfLongestSubstring(String s) {
        if (s.length() == 0) return 0;

        Set<Character> charSet = new HashSet<>();
        int left = 0;
        int maxLength = Integer.MIN_VALUE;

        for (int right = 0; right < s.length(); right++) {
            while (charSet.contains(s.charAt(right))) {
                charSet.remove(s.charAt(left));
                left++;
            }

            charSet.add(s.charAt(right));
            maxLength = Math.max(maxLength, right - left + 1);
        }

        return maxLength;
    }
}

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

 

보드판 : 세로 크기 = N, 가로 크기 = M

게임의 목표 = 빨간 구슬을 구멍을 통해서 빼내는 것. 이때 파란 구슬이 구멍에 들어가면 안 된다.

구슬 이동 방법 = 보드판을 왼쪽, 오른쪽, 위쪽, 아래쪽으로 기울이기의 4가지 방법

→ 기울이면 빨간 구슬 뿐만 아니라 파란 구슬도 이동한다.

한 번 기울이면 구슬이 더 이상 이동하지 못할 때까지 이동한다. = 도중에 멈추기 불가능

최소 몇 번 만에 빨간 구슬을 구멍을 통해 빼낼 수 있을까?

즉 목표는 빨간 구슬을 최단 거리로 구멍(목적지)까지 이동시키는 것이다. 단, 이때 파란 구슬이 구멍에 들어가면 안 된다.

 

우선은 RED, BLUE, GOAL의 초기 위치를 파악해 두자.

어떻게 구슬을 이동시키는 것인가. 구슬 이동은 판을 기울이는 것으로 이동한다. 결국 해당 방향으로 더 이상 이동하지 못할 때까지 이동하는 것이다.

그런데 구슬이 두 종류가 존재한다. 또한 두 구슬이 동시에 이동한다.

두 구슬이 동일한 위치로 가는 것이 아니면 각각 움직이면 되니까 문제는 없다.

그런데 한 위치를 목표로 이동하게 되면 문제가 된다.

우선 해당 위치로 먼저 도착하는 구슬을 정해야 한다. 그리고 그 이후에 도착하는 구슬을 왔던 방향의 반대로 1개만큼 이동시키면 될 것 같다.

이동하면서 과정에 구멍이 있는지 파악을 해야 한다. 이동하는 과정에 구멍에 빠지는 경우에도 탈출한 것이다.

빨간 구슬이 구멍에 빠졌다고 해결이 된 것이 아니다. 파란 구슬이 뒤이어 빠지게 되면 해당 경로는 불가능한 경로가 된다.

 

백트랙킹이 필요하다.

dfs를 통해 이동 방향을 변경해 가며 탐색해 나간다. 그 과정에서 파란 구슬이 구멍에 빠지면 해당 경로는 불가능한 경로가 되기 때문에 백트랙킹을 한다.

특정 방향 이동 방법에 대해 생각해 보자. 해당 뱡향을 d라고 하겠다.

두 구슬의 현재 위치를 알아야 한다.

이동 후 두 구슬의 위치가 동일하다면 늦게 도착하는 구슬은 위치 보정을 해준다.

그 이외의 경우에는 그냥 구슬을 목표점까지 이동시키면 된다.

여기서 목표점이란 ‘#’ 즉 벽을 만날 때까지이다.

각각 이동하면서 매 위치마다 구멍이 존재하는지 확인해야 한다.

위의 두 특수 경우에는 구멍이 존재한다면 무조건 해당 경로는 불가능한 경로이다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Test {

    static FastReader scan = new FastReader();
    static int N,M;
    static char[][] board;
    static int[] initialRed = new int[2];
    static int[] initialBlue = new int[2];
    static boolean redGoal = false;
    static boolean blueGoal = false;
    static int minPath = Integer.MAX_VALUE;
    static int[][] DIRECTION = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}}; //상좌우하

    static void input() {
        N = scan.nextInt();
        M = scan.nextInt();

        board = new char[N][M];
        for (int i = 0; i < N; i++) {
            board[i] = scan.nextLine().toCharArray();
        }

        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                if (board[i][j] == 'R') {
                    initialRed = new int[]{i, j};
                } else if (board[i][j] == 'B') {
                    initialBlue = new int[]{i, j};
                }
            }
        }
    }

    static void dfs(int depth, int redR, int redC, int blueR,int blueC) {
        if (blueGoal) return; // blue가 골에 도달하면 안된다.
        if (redGoal && !blueGoal) {
            minPath = Math.min(minPath, depth);
            return;
        }

        for (int d = 0; d < 4; d++) {
            if (depth < 10) {
                int[][] newPosition = move(d, redR, redC, blueR, blueC);
                dfs(depth + 1, newPosition[0][0], newPosition[0][1], newPosition[1][0], newPosition[1][1]);
                redGoal = false;
                blueGoal = false;
            }
        }
    }

    static int[][] move(int d, int redR, int redC, int blueR, int blueC) {

        int[] redPosition = new int[2];
        int[] bluePosition = new int[2];

        int redDistance = 0;
        int blueDistance = 0;

        int newRedR = redR;
        int newRedC = redC;

        while (isValidPosition(newRedR, newRedC) && board[newRedR][newRedC] != '#') {
            newRedR += DIRECTION[d][0];
            newRedC += DIRECTION[d][1];
            redDistance++;

            if (board[newRedR][newRedC] == 'O') {
                redGoal = true;
                break;
            } else if (board[newRedR][newRedC] == '#') {
                newRedR += DIRECTION[3-d][0];
                newRedC += DIRECTION[3-d][1];
                redDistance--;
                break;
            }
        }

        int newBlueR = blueR;
        int newBlueC = blueC;

        while (isValidPosition(newBlueR, newBlueC) && board[newBlueR][newBlueC] != '#') {
            newBlueR += DIRECTION[d][0];
            newBlueC += DIRECTION[d][1];
            blueDistance++;

            if (board[newBlueR][newBlueC] == 'O') {
                blueGoal = true;
                break;
            } else if (board[newBlueR][newBlueC] == '#') {
                newBlueR += DIRECTION[3-d][0];
                newBlueC += DIRECTION[3-d][1];
                blueDistance--;
                break;
            }
        }

        if (newRedR == newBlueR && newRedC == newBlueC) {
            if (redDistance < blueDistance) {
                newBlueR += DIRECTION[3-d][0];
                newBlueC += DIRECTION[3-d][1];
            } else {
                newRedR += DIRECTION[3-d][0];
                newRedC += DIRECTION[3-d][1];
            }
        }

        // 이동 끝
        redPosition[0] = newRedR;
        redPosition[1] = newRedC;
        bluePosition[0] = newBlueR;
        bluePosition[1] = newBlueC;

        return new int[][]{redPosition, bluePosition};
    }

    static boolean isValidPosition(int r, int c) {
        return r >= 0 && r < N && c >= 0 && c < M;
    }

    public static void main(String[] args) {
        input();
        dfs(0, initialRed[0], initialRed[1], initialBlue[0], initialBlue[1]);
        int answer = -1;
        if (minPath != Integer.MAX_VALUE) {
            answer = minPath;
        }

        System.out.println(answer);
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }

        String next() {
            while (st == null || !st.hasMoreElements()) {
                try {
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            return st.nextToken();
        }

        String nextLine() {
            String str = "";
            try {
                str = br.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return str;
        }

        int nextInt() {
            return Integer.parseInt(next());
        }
    }
}

중복된 부분이 좀 있어 메서드 분리등의 리팩토링이 필요하지만 정답은 맞게 처리됐다.

하지만 지금 방문처리를 하지 않고 있기 때문에 중복된 방향으로 가지 않아도 되는데 같은 방향을 계속 시도하고 있다. 이런 부분을 해결하기 위해서는 방문처리를 하는 별도의 배열이 필요할 것이다.

또한 최소 이동 횟수를 찾는 문제이다. DFS보다는 BFS가 더 어울린다.

BFS로 풀어봄과 동시에 시뮬레이션 문제이니 객체지향적인 관점을 좀 도입해 보자.

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

public class Test {

    static FastReader scan = new FastReader();
    static int N, M;
    static char[][] board;

    // 위치를 관리할 클래스
    static class Point {
        private final int row;
        private final int col;

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

        int getRow() {
            return this.row;
        }

        int getCol() {
            return this.col;
        }
				
				// 같은 지점인지 판단하기 위한 간단한 equals
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (!(obj instanceof Point)) return false;
            Point point = (Point) obj;
            return this.row == point.getRow() && this.col == point.getCol();
        }

        @Override
        public int hashCode() {
            return Objects.hash(row, col);
        }
    }

    // 방향을 관리하는 enum
    enum Direction {
        UP(-1, 0),
        DOWN(1, 0),
        LEFT(0, -1),
        RIGHT(0, 1);

        private final int deltaRow;
        private final int deltaCol;

        Direction(int deltaRow, int deltaCol) {
            this.deltaRow = deltaRow;
            this.deltaCol = deltaCol;
        }

        public int getDeltaRow() {
            return deltaRow;
        }

        public int getDeltaCol() {
            return deltaCol;
        }
    }

    // 보드판 상태
    static class Board {
        private final char[][] board;
        private Point red;
        private Point blue;
        private final int moveCount;

        Board(char[][] board, Point red, Point blue, int moveCount) {
            this.board = board;
            this.red = red;
            this.blue = blue;
            this.moveCount = moveCount;
        }

        // 이동 후 보드판의 상태를 반환하는 메서드
        Board move(Direction dir) {
            char[][] newBoard = deepCopy(board);
            Point newRed = movePoint(newBoard, red, dir);
            Point newBlue = movePoint(newBoard, blue, dir);

            if (newRed.equals(newBlue) && newBoard[newRed.getRow()][newRed.getCol()] != 'O') {
                // 레드랑 블루랑 같은 위치인데 골이 아닌경우 -> 위치 조정 필요
                if (getDistance(red, newRed) < getDistance(blue, newBlue)) {
                    // 레드가 더 가까웠는 경우
                    newBlue = moveBack(newBlue, dir);
                } else {
                    newRed = moveBack(newRed, dir);
                }
            }
            // 보드판 내용 수정
            if (!red.equals(newRed)) {
                changeBoard(newBoard, red, newRed, 'R');
            }

            if (!blue.equals(newBlue)) {
                changeBoard(newBoard, blue, newBlue, 'B');
            }

            // 1회 이동이 끝난 뒤 새로운 보드판 상황 반환
            return new Board(newBoard, newRed, newBlue, moveCount + 1);
        }

        private void changeBoard(char[][] board, Point p1, Point p2, char type) {
            board[p1.getRow()][p1.getCol()] = '.';
            if (board[p2.getRow()][p2.getCol()] != 'O') {
                board[p2.getRow()][p2.getCol()] = type;
            }
        }

        private int getDistance(Point p1, Point p2) {
            return Math.abs(p1.getRow() - p2.getRow()) + Math.abs(p1.getCol() - p2.getCol());
        }

        private Point moveBack(Point point, Direction dir) {
            return new Point(point.getRow() - dir.getDeltaRow(), point.getCol() - dir.getDeltaCol());
        }

        private Point movePoint(char[][] board, Point start, Direction dir) {
            int row = start.getRow();
            int col = start.getCol();
            while (board[row + dir.getDeltaRow()][col + dir.getDeltaCol()] != '#' && board[row][col] != 'O') {
                // 다음위치가 # 이면 이동하면 안됨.
                row += dir.getDeltaRow();
                col += dir.getDeltaCol();
            }

            return new Point(row, col);
        }

        private char[][] deepCopy(char[][] board) {
            return Arrays.stream(board)
                    .map(char[]::clone)
                    .toArray(char[][]::new);
        }

        public Point getRed() {
            return red;
        }

        public Point getBlue() {
            return blue;
        }

        public int getMoveCount() {
            return moveCount;
        }

        public char[][] getBoard() {
            return board;
        }
    }

    static int bfs() {
        Point red = null;
        Point blue = null;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                if (board[i][j] == 'R') {
                    red = new Point(i, j);
                } else if (board[i][j] == 'B') {
                    blue = new Point(i, j);
                }
            }
        }

        Queue<Board> q = new LinkedList<>(); // 보드판 상태를 가지는 큐
        Set<String> visited = new HashSet<>();
        Board initialState = new Board(board, red, blue, 0);
        q.offer(initialState);
        visited.add(stateToString(initialState));

        while (!q.isEmpty()) {
            Board currentState = q.poll();
            if (currentState.moveCount > 10) break; // 10회 이상 이동한 상태
            if (currentState.getBoard()[currentState.getBlue().getRow()][currentState.getBlue().getCol()] == 'O') continue; // 블루가 목적지에 도달한 경우
            if (currentState.getBoard()[currentState.getRed().getRow()][currentState.getRed().getCol()] == 'O') {
                return currentState.getMoveCount();
            }

            for (Direction dir : Direction.values()) {
                Board nextState = currentState.move(dir);
                String stateKey = stateToString(nextState);
                if (!visited.contains(stateKey)) {
                    // 처음 방문하는 상태의 경우
                    visited.add(stateKey);
                    q.offer(nextState);
                }
            }
        }

        return -1; // 모든 탐색이 끝났는데 답을 못찾았다.
    }

    static String stateToString(Board board) {
        return board.getRed().getRow() + "," + board.getRed().getCol() + "," + board.getBlue().getRow() + "," +board.getBlue().getCol();
    }

    static void input() {
        N = scan.nextInt();
        M = scan.nextInt();
        board = new char[N][M];
        for (int i = 0; i < N; i++) {
            board[i] = scan.nextLine().toCharArray();
        }
    }

    public static void main(String[] args) {
        input();
        int answer = bfs();
        System.out.println(answer);
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }

        String next() {
            while (st == null || !st.hasMoreElements()) {
                try {
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            return st.nextToken();
        }

        String nextLine() {
            String str = "";
            try {
                str = br.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return str;
        }

        int nextInt() {
            return Integer.parseInt(next());
        }
    }
}

하고 나서 보니까 Direction의 Enum 도입은 오히려 복잡성만 증가시켰다는 생각이 든다. 그냥 배열로 관리하는 게 더 직관적이었을 것 같다. 또한 getter의 경우에도 알고리즘 문제에서는 쓸데없는 코드의 길이만 늘린다.

'Algorithm > BOJ' 카테고리의 다른 글

BOJ 14500 - 테트로미노  (1) 2024.10.09
BOJ 14499 - 주사위 굴리기  (1) 2024.10.09
BOJ 13458 - 시험 감독  (1) 2024.10.09
BOJ 3190- 뱀  (2) 2024.10.08
BOJ 12100 - 2048  (0) 2024.10.07

+ Recent posts