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

https://leetcode.com/problems/minimum-size-subarray-sum

 

subarray의 합이 target보다 크거나 같은 subarray 중 최소 길이를 구하라.

subarray이기에 정렬이 수행되면 안 된다.

 

연속된 수를 확인할 때는 슬라이딩 윈도 기법이 좋다.

여기서 슬라이딩 윈도의 크기를 조절하며 최소 길이를 갱신하는 방법을 쓰면 될 것 같다.

 

이 슬라이딩 윈도는 두 개의 포인터로 구성이 돼있다.

두 포인터 사이에 있는 값들의 합이 target보다 크면 최소 길이를 갱신한다. 하지만 최소를 구해야 하기 때문에 범위를 줄여가며 체크를 해줘야 한다.

따라서 왼쪽 범위를 하나 줄이고 다시 체크를 하는 동작을 반복한다.

그러다가 target보다 합이 작아져 더 이상 왼쪽을 줄일 수 없을 때 오른쪽 범위를 늘려주도록 한다.

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int minLength = Integer.MAX_VALUE;
        int sum = 0;
        int left = 0;

        for (int right = 0; right < nums.length; right++) {
            sum += nums[right];

            while (sum >= target) {
                minLength = Math.min(minLength, right - left + 1);
                sum -= nums[left];
                left++;
            }
        }

        return minLength == Integer.MAX_VALUE ? 0 : minLength;
    }
}

https://leetcode.com/problems/3sum

 

i ≠ j, i ≠ k, j ≠ k 이면서 nums [i] + nums [j] + nums [k] == 0 인 nums [i], nums [j], nums [j]의 조합을 모두 찾아라.

nums 배열을 보면 중복된 숫자가 존재할 수 있다.

int[ ] nums = [-1, 0, 1, 2, -1, -4]로 주어졌다고 가정하자.

합이 0이 되는 경우는 -1 + 0 + 1, -1 + -1 + 2가 존재한다. 하나씩 전부 확인하는 브루트포스방식을 사용하면 찾을 수는 있지만 아주 비효율적이다. 어떻게 하면 효율적으로 찾을 수 있을까?

보통 다른 문제에서는 두 수의 합이 target이 되는 경우를 찾는다. 그리고 그때 정렬과 투 포인터를 주로 사용한다.

그런 관점에서 보면 이 문제는 target이 변하는 투 포인터 문자라고도 볼 수 있다고 생각한다.

 

우선 정렬을 수행해보자. 정렬을 하면 중복을 뛰어넘기 쉽고 투 포인터 사용에도 용이하다.

nums = [-4, -1, -1, 0, 1, 2]이다. 0이 되기 위해서 하나의 숫자를 고정해 보다. 그 숫자가 -4라고 하자. 그렇다면 두 수의 합이 4가 되는 수를 찾는 것이다. 그다음은 그 숫자가 -1이 되고 합이 1이 되는 숫자를 찾는 동작이다. 이 동작을 끝까지 반복하는 것이다.

 

해야 할 일을 분리해보자.

  1. target을 정한다.
  2. 투 포인터를 사용해 target과 일치하는 조합을 찾는다.
  3. 중복을 피하기 위한 로직을 추가한다.
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> answer = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length - 2; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) continue;
                findTriplets(i, nums, answer);
        }

        return answer;
    }

    private void findTriplets(int targetIdx, int[] nums, List<List<Integer>> answer) {
        int left = targetIdx + 1;
        int right = nums.length - 1;

        while (left < right) {
            int sum = nums[left] + nums[right] + nums[targetIdx];

            if (sum == 0) {
                answer.add(Arrays.asList(nums[targetIdx], nums[left], nums[right]));

                while (left < right && nums[left] == nums[left + 1]) left++;
                while (left < right && nums[right] == nums[right - 1]) right--;

                left++;
                right--;
            } else if (sum < 0) {
                left++;
            } else {
                right--;
            }
        }
    }
}

https://leetcode.com/problems/container-with-most-water

 

가장 많은 물을 가두기 위해 어느 두 점을 선택해야 하는가.

너비라는 것이 가로 x 세로이다. 일단 가로를 가장 길게 설정해 둔 다음 가로를 좁혀가며 최댓값을 변경하면 될 것 같다.

이때 가로를 어떻게 좁히느냐가 문제인데 작은 것을 기준으로 세로가 정해지기 때문에 세로값이 작은 것을 변경시키는 방향으로 가는 것이 맞다.

class Solution {
    public int maxArea(int[] height) {
        int max = Integer.MIN_VALUE;

        int left = 0;
        int right = height.length - 1;

        while (left < right) {
            int sum = (right - left) * Math.min(height[left], height[right]);
            max = Math.max(max, sum);

            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }

        return max;
    }
}

https://leetcode.com/problems/two-sum-ii-input-array-is-sorted

 

오름차순으로 정렬된 int [] numbers가 주어졌을 때 합이 target이 되는 두 수를 찾아라.

정확히 하나의 답이 있다고 보장.

두 개의 포인터를 가지고 합을 확인하는 과정이 필요하다.

이때 정렬 돼 있다는 조건을 활용하기 위해 투 포인터를 양 쪽 끝에서 출발해 범위를 좁혀온다.

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int left = 0;
        int right = numbers.length - 1;

        while (left < right) {
            int sum = numbers[left] + numbers[right];
            if (sum > target) {
                right--;
            } else if (sum < target) {
                left++;
            } else {
                return new int[]{left + 1, right + 1};
            }
        }
        return null; // 어차피 답은 반드시 존재한다.
    }
}

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

LeetCode 15 - 3Sum  (0) 2024.10.04
LeetCode 11 - Container With Most Water  (0) 2024.10.04
LeetCode 6 - Zigzag Conversion  (1) 2024.10.03
LeetCode 151 - Reverse Words in a String  (1) 2024.10.03
LeetCode 12 - Integer to Roman  (0) 2024.10.03

https://leetcode.com/problems/zigzag-conversion

주어진 문자열 s를 numRows에 맞게 지그재그 패턴으로 만든 다음 row별로 읽어서 합친다.

예를 들어 s = “PAYPALISHIRING”, numRows = 4 이면 output은 “PINALSIGYAHRPI”가 된다.

P     I    N
A   L S  I G
Y A   H R
P     I

규칙을 살펴보자.

나는 String [] 배열이 numRows 수만큼 존재한다고 가정하겠다.

그렇다면 배열의 크기를 정하는게 중요해지는데 numRows = 1인 경우 가장 긴 길이의 배열이 필요할 것이다.

따라서 s의 길이만큼의 배열을 생성하도록 하겠다.

문자의 할당은 어떻게 할 것인가.

문자의 할당 방식은 아래쪽 방향 이동, 오른쪽 위 대각선 방향 이동 두 가지가 존재한다.

우선은 아래 방향으로 이동하다가 최대로 이동할 수 있는 곳(numRows)에 도달하면 방향을 바꿔 오른쪽 대각선 방향 이동을 하게 하면 된다.

class Solution {

    private static final int[][] DIRECTION = {{1, 0}, {-1, 1}};

    public String convert(String s, int numRows) {
        if (numRows == 1) {
            return s;
        }

        int n = s.length();
        char[][] rows = new char[numRows][n];
        for (char[] row : rows) {
            Arrays.fill(row, ' ');
        }
        // 시작 위치
        int r = 0;
        int c = 0;
        int d = 0;

        for (char ch : s.toCharArray()) {

            rows[r][c] = ch;

            r += DIRECTION[d][0];
            c += DIRECTION[d][1]; 

            if (!isValidPosition(r, c, rows)) {
                r -= DIRECTION[d][0];
                c -= DIRECTION[d][1];   
                d = (d + 1) % DIRECTION.length;
                r += DIRECTION[d][0];
                c += DIRECTION[d][1]; 
            }
        }
        
        StringBuilder sb = new StringBuilder();
        for (char[] row : rows) {
            for (char ch : row) {
                if (ch != ' ') {
                    sb.append(ch);
                }
            }
        }

        return sb.toString();
    }

    private boolean isValidPosition(int r, int c, char[][] rows) {
        return r >= 0 && r < rows.length && c >= 0 && c < rows[r].length;
    }
}

 

코드를 개선해보자. 현재 낭비되는 공간이 많다.

또한 대각선 이동도 정확한 열의 위치가 필요한 것이 아니다. 단지 어느 행에 위치하는지가 중요하다.

2차원 배열이 아니라 StringBuilder 배열로 변경하면 각 row를 처리하면서 공간 낭비를 줄일 수 있다.

class Solution {
    public String convert(String s, int numRows) {
        if (numRows == 1 || s.length() <= numRows) {
            return s;
        }

        StringBuilder[] rows = new StringBuilder[numRows];
        for (int i = 0; i < numRows; i++) {
            rows[i] = new StringBuilder();
        }
        
        int currentRow = 0;
        boolean goingDown = false;

        for (char c : s.toCharArray()) {
            rows[currentRow].append(c);

            if (currentRow == 0 || currentRow == numRows - 1) {
                // 방향 전환
                goingDown = !goingDown;
            }

            currentRow += goingDown ? 1 : -1;
        }

        StringBuilder result = new StringBuilder();
        for (StringBuilder row : rows) {
            result.append(row);
        }

        return result.toString();
    }
}

https://leetcode.com/problems/reverse-words-in-a-string

 

문자열에서 단어들을 꺼내 역순으로 반환하는 문제이다. 각 단어들은 하나의 공백을 사이에 두고

합쳐진다.

예를 들어 “ hello world “는 “world hello”로 반환된다.

보다시피 원본 문자열에 띄어쓰기가 여러 개 존재할 수도 있다.

가장 먼저 떠오른 방법은 문자열 리스트에 그냥 단어별로 파싱 해서 넣고 조작하는 것이다.

class Solution {
    public String reverseWords(String s) {
        List<String> words = new ArrayList<>();

        StringBuilder word = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == ' ') {
                if (!word.isEmpty()) {
                    words.add(word.toString());
                }
                word = new StringBuilder();
            } else {
                word.append(s.charAt(i));
            }
        }
        if (!word.isEmpty()) {
            words.add(word.toString());
        }

        StringBuilder answer = new StringBuilder();
        for (int i = words.size() - 1; i > 0; i--) {
            answer.append(words.get(i)).append(" ");
        }
        answer.append(words.get(0));

        return answer.toString();
    }
}

앞에서부터 문자를 하나씩 탐색하며 단어를 완성시켜 나간다.

그러다가 공백을 만나면 기존에 쌓아왔던 문자들을 하나의 단어로 판정하고 리스트에 넣고 쌓아오던 문자열을 초기화한다.

최종적으로는 역순으로 뒤에서부터 단어를 꺼내 결과를 생성한다.

그런데 자바에서 제공해 주는 메서드를 통해 더 간단하게 해결이 가능하다.

class Solution {
    public String reverseWords(String s) {
        
        String[] words = s.trim().split("\\\\s+");// 좌우 공백제거, 공백이 1개 이상인 것을 기준으로 분리
        StringBuilder reversed = new StringBuilder();
        
        for (int i = words.length - 1; i >= 0; i--) {
            reversed.append(words[i]);
            if (i > 0) reversed.append(" ");
        }

        return reversed.toString();
    }
}

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

LeetCode 167 - Two Sum II - Input Array Is Sorted  (0) 2024.10.03
LeetCode 6 - Zigzag Conversion  (1) 2024.10.03
LeetCode 12 - Integer to Roman  (0) 2024.10.03
LeetCode 13 - Roman to Integer  (1) 2024.10.03
LeetCode 42 - Trapping Rain Water  (0) 2024.10.02

https://leetcode.com/problems/integer-to-roman

 

Symbol 별로 숫자가 할당되어 있다.

10진수의 수를 이제 로마수로 변경해야 한다. 변경은 다음과 같은 과정을 통한다.

  1. 수가 4 혹은 9로 시작하지 않는다면 input으로부터 뺄 수 있는 최대 symbol을 선택한다. 그리고 해당 symbol을 결과에 추가한다. 그리고 해당 숫자를 뺀 나머지를 로마수로 재할당한다.
  2. 만약 수가 4 혹은 9로 시작하다면, 해당 값을 뺄샘 형태로 변환하여 로마수로 표현한다. 예를 들어 4는 5(V)에서 1(I)을 뺀 값이 되므로 IV가 된다.
  3. 10의 거듭제곱은 최대 3번까지 연속해서 붙일 수 있다. 하지만 5(V), 50(L), 500(D)은 여러 번 반복해서 사용할 수 없다. 만약 어떤 기호가 4번 반복해서 사용해야 할 경우에는 뺄셈 형태로 변환한다.

우선 하나씩 생각해보자.

주어진 숫자 num의 첫자리 숫자를 어떻게 판단 할 것인가?

4냐 400이냐에 따라 조합이 달라지기 때문에 첫 자리 숫자가 어떤 수인지를 넘어 자릿수까지 판단을 해야 한다.

문자열로 판단하는 게 낫겠다는 생각이 든다. int num = 3749라고 가정해 보자.

문자열로 바꾸면 “3749”, 길이가 4인 문자열이 된다.

첫자리는 3 * 10 ^4 그다음은 7 * 10^3, 410^2, 910^1 이렇게 된다. 매 순간 판단하는 숫자가 문자열의 길이와 chartAt(n)으로 편리하게 구할 수 있다.

class Solution {

    private static final Map<Integer, String> symbolMap = new LinkedHashMap<>();

    static {
        symbolMap.put(1000, "M");
        symbolMap.put(500, "D");
        symbolMap.put(100, "C");
        symbolMap.put(50, "L");
        symbolMap.put(10, "X");
        symbolMap.put(5, "V");
        symbolMap.put(1, "I");
    }

    public String intToRoman(int num) {
        StringBuilder sb = new StringBuilder();
        String number = Integer.toString(num);
        int length = number.length();

        for (int i = 0; i < length; i++) {
            int coefficient = number.charAt(i) - '0';
            int exponent = length - i - 1;
            
            int now = coefficient * (int)Math.pow(10, exponent);
            
            if (coefficient == 4 || coefficient == 9) {
                for (int key : symbolMap.keySet()) {
                    if (key > now && symbolMap.containsKey(key - now)) {
                        sb.append(symbolMap.get(key - now));
                        sb.append(symbolMap.get(key));
                    }
                }
            } else {
                for (int key : symbolMap.keySet()) {
                    while (now >= key) {
                        now -= key;
                        sb.append(symbolMap.get(key));
                    }
                }
            }
        }

        return sb.toString();
    }
}

일단 통과는 됐다.

간단하게 설명하자면 우선 LinkedHashMap을 사용해 데이터를 삽입된 순서대로 저장되도록 처리했다.

그 후 주어진 num을 문자로 변경하고 이 문자열을 조작해 문제를 풀었다.

그런데 매 숫자마다 일일이 확인하고 변환하는 과정 때문에 효율적이지 않다.

그러면 애초에 가능한 로마 symbol을 다 저장해 놓으면 되는 거 아닐까? 생각보다 개수가 몇 개 없으니 가능한 방식이다.

class Solution {

    private static final Map<Integer, String> SYMBOL_MAP = new LinkedHashMap<>();

    static {
        SYMBOL_MAP.put(1000, "M");
        SYMBOL_MAP.put(900, "CM");
        SYMBOL_MAP.put(500, "D");
        SYMBOL_MAP.put(400, "CD");
        SYMBOL_MAP.put(100, "C");
        SYMBOL_MAP.put(90, "XC");
        SYMBOL_MAP.put(50, "L");
        SYMBOL_MAP.put(40, "XL");
        SYMBOL_MAP.put(10, "X");
        SYMBOL_MAP.put(9, "IX");
        SYMBOL_MAP.put(5, "V");
        SYMBOL_MAP.put(4, "IV");
        SYMBOL_MAP.put(1, "I");
    }

    public String intToRoman(int num) {
        StringBuilder sb = new StringBuilder();

        for (int key : SYMBOL_MAP.keySet()) {
            while (num >= key) {
                num -= key;
                sb.append(SYMBOL_MAP.get(key));
            }
        }

        return sb.toString();
    }
}

그런데 더 개선할 점이 있는 것 같다.

데이터셋 저장을 LinkedHashMap을 사용함으로써 삽입 순서를 유지한 것은 좋았지만 각 반복에서 Map의 엔트리에 접근하는 데 추가적인 동작이 들어간다.

LinkedHashMap은 내부적으로 LinkedList를 사용하니까 메모리 접근이 배열에 비해서 덜 효율적일 수 있다.

따라서 해당 저장을 두 개의 배열로 나누어 저장해서 접근하면 조금 더 효율적인 결과를 얻을 수 있을 것이다.

class Solution {
    private static final int[] VALUES = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
    private static final String[] SYMBOLS = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};

    public String intToRoman(int num) {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < VALUES.length; i++) {
            while (num >= VALUES[i]) {
                num -= VALUES[i];
                sb.append(SYMBOLS[i]);
            }
        }

        return sb.toString();
    }
}

이 문제는 결국 공간과 시간의 트레이드오프를 보여줬던 문제였던 것 같다.

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

LeetCode 6 - Zigzag Conversion  (1) 2024.10.03
LeetCode 151 - Reverse Words in a String  (1) 2024.10.03
LeetCode 13 - Roman to Integer  (1) 2024.10.03
LeetCode 42 - Trapping Rain Water  (0) 2024.10.02
LeetCode 150 - Candy  (1) 2024.09.30

+ Recent posts