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

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

 

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

 

3190번: 뱀

 'Dummy' 라는 도스게임이 있다. 이 게임에는 뱀이 나와서 기어다니는데, 사과를 먹으면 뱀 길이가 늘어난다. 뱀이 이리저리 기어다니다가 벽 또는 자기자신의 몸과 부딪히면 게임이 끝난다. 게임

www.acmicpc.net

 

1. 풀이 소스:

 

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

import static java.lang.Integer.parseInt;

public class Main {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = parseInt(br.readLine());
        int K = parseInt(br.readLine());

        Game game = new Game(N); // game 인스턴스 선언

        StringTokenizer st;
        for (int i = 0; i < K; i++) {
            st = new StringTokenizer(br.readLine());
            game.setApple(parseInt(st.nextToken()), parseInt(st.nextToken())); // 사과 등록
        }

        int L = parseInt(br.readLine());
        for (int i = 0; i < L; i++) {
            st = new StringTokenizer(br.readLine());
            game.addDirection(parseInt(st.nextToken()), st.nextToken()); // 시간에 맞춰 변경해야하는 시간과 방향 설정
        }

        System.out.println(game.run());
    }
}

class Game {

    int n;
    int time; // 출력해야 하는 시간
    boolean[][] nowVisited; // 현재 뱀의 몸(머리, 꼬리 포함)이 걸쳐 있는지 판단
    boolean[][] map; // 사과 있는지 판단
    char nowDir = 'R'; // 현재 방향 (계속 바뀜)
    List<Tail> tails = new LinkedList<>(); // 꼬리 추가 및 삭제를 위한 링크드 리스트
    Queue<Direction> dirs = new ArrayDeque<>(); // 시간에 맞춰 방향 변경을 위한 큐

    Game (int n) {
        this.n = n;
        map = new boolean[n + 1][n + 1]; // n by n 이므로 n + 1로 초기화
        nowVisited = new boolean[n + 1][n + 1];
    }

    void setApple(int row, int col) {
        map[row][col] = true; // apple 설정
    }

    void addDirection(int ttl, String dir) {
        dirs.add(new Direction(ttl, dir)); // 시간에 맞는 방향 설정
    }

    int run() {

        int r = 1; // 행
        int c = 1; // 열
        tails.add(new Tail(r, c)); // 시작은 머리와 꼬리가 동일 하므로 꼬리에 추가

        while (true) { // 반복

            if (!dirs.isEmpty() && time == dirs.peek().ttl) { //비어 있지 않고 ttl와 같음
                Direction nextDir = dirs.poll(); // 다음 방향을 위한 Direction 인스턴스 큐에서 빼기

                if (nowDir == 'U') { // 각 방향에 맞춰 회전 후 방향 재조정
                    if (nextDir.dir.equals("L")) nowDir = 'L';
                    else nowDir = 'R';
                }

                else if(nowDir == 'D') {
                    if (nextDir.dir.equals("L")) nowDir = 'R';
                    else nowDir = 'L';
                }

                else if (nowDir == 'L') {
                    if (nextDir.dir.equals("L"))nowDir = 'D';
                    else nowDir = 'U';
                }

                else if (nowDir == 'R') {
                    if (nextDir.dir.equals("L")) nowDir = 'U';
                    else nowDir = 'D';
                }
            }

            // 회전을 마친 경우 혹은 회전이 없는 경우 nowDir에 방향에 맞춰 움직이기 시작
            switch (nowDir) {
                case 'U':
                    r--;
                    break;
                case 'D':
                    r++;
                    break;
                case 'L':
                    c--;
                    break;
                case 'R':
                    c++;
                    break;
            }

            if (!isValid(r, c)) { // 이동하려는 곳이 인덱스에 벗어나거나, 몸통이 있는 곳이라면 종료
                time++; // 종료시간은 마지막 움직이는 경우도 포함이므로
                break;
            }

            nowVisited[r][c] = true; // 머리가 방문한 곳 방문 추가

            // 위치한 곳에 사과가 있는지 파악하기
            if (!map[r][c]) { // 없다면 머리 증가 후 꼬리 제거
                tails.add(new Tail(r, c)); // 머리 추가

                Tail tail = tails.get(0); // 맨 마지막 꼬리 가져오기
                nowVisited[tail.r][tail.c] = false; // 꼬리가 사라지므로 false
                tails.remove(0); // 꼬리 제거
            }

            else {
                tails.add(new Tail(r, c)); // 사과가 있다면 머리 위치 추가
                map[r][c] = false; // 사과 제거하기
            }

            time++; // 모든 움직임이 끝났으므로 시간++
        }

        return time;
    }

    // 이미 몸체의 일부가 방문하고 있는 경우 종료
    boolean isValid(int r, int c) { // 맵의 유효한 범위에 아직 방문하지 않은 곳이여야
        return r > 0 && r <= n && c > 0 && c <= n && !nowVisited[r][c];
    }

    class Tail {
        int r; // row
        int c; // col

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

    class Direction {
        int ttl; // time to live
        String dir; // 방향성

        Direction (int ttl, String dir) {
            this.ttl = ttl;
            this.dir = dir;
        }
    }
}

 

 

2. 풀이 중점 사항

 

해당 문제는 뱀이 움직일 때, 사과가 있는지 없는지 여부에 따라 몸통의 길이가 달라집니다.

따라서, 다음의 의사코드를 바탕으로 문제를 해결해야 합니다.

 

  • 현재의 위치에서 방향을 먼저 설정하기
  • 각 방향대로 움직이되, 보드 밖을 벗어나는 곳으로 이동하거나 혹은 이동한 곳에 뱀의 몸통이 존재한다면 종료하기
  • 만약 움직인 곳이 유효하다면,
  • 사과가 있다면 사과를 지우고 머리를 tails 링크드리스트에 추가하기, 현재 머리가 있는 곳에 몸통 여부 true 표시하기
  • 사과가 없다면, 머리를 tails 링크드 리스트에 추가하고, 머리가 있는 곳에 몸통 여부 표시하기, 꼬리는 머리가 움직이므로 있던 장소에 몸통 여부를 false로 표시하고, tails에서 remove(0) 하기
  • 모든 움직임이 마쳤으므로 시간을 ++ 하고 다시 while루프로 돌아가기

 

한 문제에 큐, 링크드리스트, 클래스, 이차원 배열, 방향 전환 등을 경험할 수 있는 좋은 시간이었던 것 같습니다.

자세한 풀이는 모두 주석으로 처리하였습니다.!

 

이상으로 뱀 자바 풀이를 마치도록 하겠습니다.

 

감사합니다.!

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

 

이번 포스팅은 백준 SW 기출문제 - 2048(Easy)를 해결한 과정을 정리하고자 합니다.!

 

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

 

12100번: 2048 (Easy)

첫째 줄에 보드의 크기 N (1 ≤ N ≤ 20)이 주어진다. 둘째 줄부터 N개의 줄에는 게임판의 초기 상태가 주어진다. 0은 빈 칸을 나타내며, 이외의 값은 모두 블록을 나타낸다. 블록에 쓰여 있는 수는 2

www.acmicpc.net

 

 

1. 풀이 소스:

 

package sw_12100.success;

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

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

        int n = parseInt(br.readLine());

        Board board = new Board(n);

        for (int i = 0 ; i < n; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j < n; j++) {
                board.setBoard(i, j, parseInt(st.nextToken()));
            }
        }
        board.dfs(0);
        System.out.println(board.getAnswer());
    }
}

class Board {

    static final char[] DIR = {'U', 'D', 'L', 'R'}; // up, down, left, right

    int n;
    int answer;
    int[][] map;

    Board (int n) {
        this.n = n;
        map = new int[n][n];
    }

    void setBoard(int x, int y, int v) {
        map[x][y] = v;
    }

    int getAnswer() {
        return this.answer;
    }

    // dfs로 최대 5번 반복되는 과정에서 최대값 찾기
    void dfs(int step) {
        if (step == 5) { // count가 5라면 최대값 찾기
            for (int[] mapRow : map) {
                for (int num : mapRow) {
                    answer = Math.max(answer, num);
                }
            }
            return;
        }

        int[][] clone = new int[n][n];
        for (int i = 0; i < n; i++) {
            clone[i] = map[i].clone();
        }

        for (int i = 0; i < DIR.length; i++) { // 상 하 좌 우
            moveBlock(DIR[i]); // 블록 움직이기
            dfs(step + 1); // step 증가시켜서 다음 step 진행

            for (int j = 0; j < n; j++) map[j] = clone[j].clone();
        }
    }

    // block의 역할 --> 미는 블록이 두번 합쳐지더라도 다음 block을 0으로 만들어 세번 겹치지 않도록 보호
    void moveBlock(char dir) {
        if (dir == 'U') { // 블록을 위로 몰기 (낮은 행으로 몰기)
            for (int c = 0; c < n; c++) { // 선택된 맵의 열 (col)
                int row = 0;
                int block = 0;
                for (int r = 0; r < n; r++) { //선택된 맵의 행 (row)
                    if (map[r][c] != 0) { // 0이 아니라면 블록이 있음
                        if (map[r][c] == block) { // 블록과 선택한 블록이 같다면
                            map[row - 1][c] = block * 2; // 낮은 행으로 합치기
                            map[r][c] = 0; // 선택된 맵 위치 블록 0
                            block = 0; // 그 위치에 있는 블록 0
                        }

                        // 만약 r = 1, c = 1, row = 0 인 경우, 앞에 블록이 없으므로
                        // r = 0인 경우 map[r][c] != 0 통과, block은 0이 유지되므로
                        // 자연스럽게 블록이 없는 공간에 밀어넣게 됨 이후 row++
                        else {
                            block = map[r][c]; // 현재위치 블록 설정
                            map[r][c] = 0; // r, c 위치 0으로 설정하기
                            map[row][c] = block; // 위로 밀어서 블록 값을 위치 시키기 (이 경우 제자리에 위치할 수 있으므로 map[r][c]보다 뒤에 있어야 함!!!
                            row++; // 다음 밀 곳은 한 칸 아래로 내린 곳
                        }
                    }
                }
            }
        }

        else if (dir == 'D') { // 블록을 아래로 몰기
            for (int c = 0; c < n; c++) {
                int row = n - 1; // 처음으로 옮겨져야 하는 도착 위치는 마지막 행
                int block = 0;
                for (int r = n - 1; r >= 0; r--) {
                    if (map[r][c] != 0) {
                        if (map[r][c] == block) {
                            map[row + 1][c] = block * 2;
                            map[r][c] = 0;
                            block = 0;
                        }

                        else {
                            block = map[r][c];
                            map[r][c] = 0;
                            map[row][c] = block;
                            row--; // 다음 번 블록은 맨 아래 위치에서 한 칸 올린 곳
                        }
                    }
                }
            }
        }

        else if (dir == 'L') { // 블록을 왼쪽으로 몰기
            for (int r = 0; r < n; r++) {
                int col = 0; // 옮겨질 위치의 열
                int block = 0;
                for (int c = 0; c < n; c++) {
                    if (map[r][c] != 0) {
                        if (map[r][c] == block) {
                            map[r][col - 1] = block * 2;
                            map[r][c] = 0;
                            block = 0;
                        }
                        else {
                            block = map[r][c];
                            map[r][c] = 0;
                            map[r][col] = block;
                            col++;
                        }
                    }
                }
            }
        }

        else if (dir == 'R') { // 블록을 오른쪽으로 몰기
            for (int r = 0; r < n; r++) {
                int col = n - 1;
                int block = 0;
                for (int c = n - 1; c >= 0; c--) {
                    if (map[r][c] != 0) {
                        if (map[r][c] == block) {
                            map[r][col + 1] = block * 2;
                            map[r][c] = 0;
                            block = 0;
                        }

                        else {
                            block = map[r][c];
                            map[r][c] = 0;
                            map[r][col] = block;
                            col--;
                        }
                    }
                }
            }
        }
    }
}

 

 

2. 풀이 과정:

 

해당 문제는, 블록을 최대 5번까지 이동시킨 후, 블록에 최댓값을 구하는 것입니다. 최대 5번이므로 3번 4번에서도 최댓값이 나올 수 있지만, 블록을 합치는 것은 x, y 좌표에 대한 값 V는 증가함수 형태이므로 (양수의 값을 합치므로) 5번을 실행한 후 최댓값을 계산하면 중간 계산 과정을 생략할 수 있습니다.

 

배열 복제

제가 계속 실수 했던 부분은 배열 복제 부분입니다. map.clone()을 하게 되면 1차원 배열은 값이 복사되어 다른 배열을 생성할 수 있습니다. 하지만 map이 2차원 배열인 경우 참조를 복사하므로 '얕은 복사'가 수행되게 됩니다.

 

int[][] clone = new int[n][n];
for (int i = 0; i < n; i++) {
    clone[i] = map[i].clone();
}

 따라서, 다음과 같이 for문으로 1차원 배열 단위로 clone()하여 값이 복사될 수 있도록 처리해야 합니다.

 

헷갈리는 상 하 좌 우

상 하 좌 우를 처리하다보면 수학은 보통 (0, 0)이 아래에 위치합니다. 하지만 데이터베이스나 이러한 map 문제를 해결해야 할 때는 (0, 0) 혹은 (1, 1)이 왼쪽 상단에 위치하는 경우가 많습니다. 따라서, 행을 내려야 할 때, 혹은 행을 올려야 할 때 
Down 작업은 row++, Up 작업은 row-- 하는 것이 필요합니다.

 

이전에 i, j를 쓸 때는 이 과정이 너무 헷갈려서 추후 풀 때는, r, c로 설정하여 헷갈림이 덜 하도록 구현하였습니다.

 

 

블록 밀기

 

블록을 미는 과정에서 아래와 같은 소스가 있습니다. 해당 소스는 풀이 소스에 자세하게 주석처리 하였지만, 중요한 포인트는 block의 쓰임새입니다. 

 

for (int r = 0; r < n; r++) {
    int col = n - 1;
    int block = 0;
    for (int c = n - 1; c >= 0; c--) {
        if (map[r][c] != 0) {
            if (map[r][c] == block) {
                map[r][col + 1] = block * 2;
                map[r][c] = 0;
                block = 0;
            }

            else {
                block = map[r][c];
                map[r][c] = 0;
                map[r][col] = block;
                col--;
            }
        }
    }
}

 

열을 오른쪽으로 민다고 하는 것은 n - 1에서 시작해서 n - 2 -> n - 1 / n - 3 -> n -2 -> n - 1 (가능하다면) 이러한 방식으로 
밀어야 하는 곳의 가까운 곳에서부터 블록을 합쳐야 합니다.

 

만약 밀고자 하는 곳에서 값이 0이 아니라는 것은 블록이 있다는 것을 의미하고,

해당 위치가 블록의 값과 같다면 블록에 2배를 합니다.

 

이 후, 블록을 0으로 설정하는데, 이는 무조건 다음 혹은 이후에 실행되는 반복에서 아래의 코드가 실행되는 것을 방지합니다.

 

map[r][c] != 0 && map[r][c] == block

블록이 0이 되어 있으므로 map[r][c]가 == 0이라면 다음 반복으로 넘어가므로, 블록이 값을 가져야만 해당 구문이 실행될 수 있습니다. 따라서, 새로운 값으로 블록을 생성해야 하므로, 블록을 미는 과정에서 3번 이상 겹치는 일이 발생하지 않습니다.

 

 

이상으로 2048 문제 풀이를 마치도록 하겠습니다.! 감사합니다 !!

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

 

이번 포스팅은 SW 기출문제인 구슬 탈출2 문제를 해결하는 과정을 정리하도록 하겠습니다.

 

문제 출처: 

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

 

13460번: 구슬 탈출 2

첫 번째 줄에는 보드의 세로, 가로 크기를 의미하는 두 정수 N, M (3 ≤ N, M ≤ 10)이 주어진다. 다음 N개의 줄에 보드의 모양을 나타내는 길이 M의 문자열이 주어진다. 이 문자열은 '.', '#', 'O', 'R', 'B'

www.acmicpc.net

 

 

1. 풀이 소스 

 

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

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

class Board {
    
    static final int[] DX = {-1, 0, 1, 0};
    static final int[] DY = {0, -1, 0, 1};
    
    int n, m;
    Marble redMarble, blueMarble;
    char[][] map;
    boolean[][][][] visited; // Red, Blue 구슬의 각 각 x, y의 방문 여부
    int answer = -1;
    
    Board(int n, int m) {
        this.n = n;
        this.m = m;
        map = new char[n][m];
        visited = new boolean[n][m][n][m];
    }
    
    void setMap(String str, int idx) {
        for (int i = 0; i < str.length(); i++) {
            char ch = str.charAt(i);
            map[idx][i] = ch;
            
            if (ch == 'R') redMarble = new Marble(idx, i, 0);
            else if (ch == 'B') blueMarble = new Marble(idx, i, 0);
        }
    }
    
    void moveMarble() {

        Queue<Marble> redQ = new ArrayDeque<>();
        Queue<Marble> blueQ = new ArrayDeque<>();
        
        redQ.add(redMarble);
        blueQ.add(blueMarble);
        
        while(!redQ.isEmpty() && !blueQ.isEmpty()) {
            Marble red = redQ.poll();
            Marble blue = blueQ.poll();

            if (red.step > 10) { // bfs 원리로 인해 x 위치에서 -1, +1 등을 수행하더라도 각 움직인 위치는 step + 1
                answer = -1;
                return;
            }
            
            // 파란색이 0에 도착한 경우
            if (map[blue.x][blue.y] == 'O') continue; // 다른 방법이 존재할 수 있으므로 해당 경로로 진행하는 것만 중지
            
            // 파란색이 도착하지 않고 빨간색이 O에 도착한 경우
            if (map[red.x][red.y] == 'O') {
                answer = red.step;
                return;
            }    
            
            for (int i = 0; i < 4; i++) {
                int bx = blue.x; // 반드시 blue부터 실행
                int by = blue.y; 

                boolean isBlueCheck = false; // 파란 구슬이 'O'에 들어가는지 체크

                while (true) {
                    bx += DX[i];
                    by += DY[i];
                    
                    if (map[bx][by] == 'O') {
                        isBlueCheck = true;
                        break;
                    }
                    
                    else if (map[bx][by] == '#') {
                        bx -= DX[i];
                        by -= DY[i];
                        break;
                    }
                }

                if (isBlueCheck) continue; // 만약 'O'에 파란 구슬이 도착하는 경우 선행 조건과 관계없이 queue에 넣지 않음
                
                int rx = red.x;
                int ry = red.y;
                
                while(true) {
                    rx += DX[i];
                    ry += DY[i];
                    
                    if (map[rx][ry] == 'O') break;
                    else if (map[rx][ry] == '#') {
                        rx -= DX[i];
                        ry -= DY[i];
                        break;
                    }
                }
                
                // 두개의 위치가 같다면 
                // 서로 다른 큐에 의해 구슬의 앞 뒤 관계를 고려하지 않고 이동하였으므로
                // 두 구슬의 우선순위에 따라 배치를 다르게 해야 함
                if (bx == rx && by == ry && map[rx][ry] != 'O') {
                    
                    // 맨해튼 거리 공식을 쓰는 이유 -> x, y 가 한칸씩 움직일 수 있으므로
                    int redDist = Math.abs(red.x - rx) + Math.abs(red.y - ry);
                    int blueDist = Math.abs(blue.x - bx) + Math.abs(blue.y - by);
                    
                    if (redDist > blueDist) {
                        rx -= DX[i];
                        ry -= DY[i];
                    } else {
                        bx -= DX[i];
                        by -= DY[i];
                    }
                }
                
                if (!visited[rx][ry][bx][by]) {
                    visited[rx][ry][bx][by] = true;
                    
                    redQ.add(new Marble(rx, ry, red.step + 1));
                    blueQ.add(new Marble(bx, by, blue.step + 1));
                }
            }
        }
    }
    
    int getAnswer() {
        return this.answer;
    }
    
    class Marble {
        int x;
        int y;
        int step;
        
        Marble(int x, int y, int step) {
            this.x = x;
            this.y = y;
            this.step = step;
        }
    }
}

 

 

2. 풀이 과정

 

해당 문제는 해결하지 못하여 하단의 블로그를 참고하였습니다. 

풀이 중점 사항은 다음과 같습니다.

 

빨간 구슬, 파란 구슬을 저장할 Queue를 각각 선언합니다.

빨간 구슬, 파란 구슬이 움직일 때, 상하좌우 인접한 곳에 구슬이 있다면 이동에 제약이 생길 수 있습니다.

이를 모두 한 번에 고려하게 된다면 복잡해지기 때문에, 하나의 통일된 조건으로 묶어서 판단할 수 있습니다.

  • 상하좌우로 각 구슬을 각 각 이동시킵니다.
  • 예를 들어 위로 구슬을 이동시킬 경우, 빨강, 파랑 구슬은 동시에 위로 올라가게 됩니다.
  • 위치한 곳이 서로 다르다면, 겹치는 경로로 움직이지 않았음을 의미합니다.
  • 만약 이동한 곳이 동일하다면, 두 구슬은 인접한 곳에서 겹치는 경로로 이동하였음을 의미합니다.
  • 겹치는 경로로 이동한 경우 어떤 구슬이 앞 서 있었는지 판단하기 위해 맨해튼 거리 공식을 활용하여 최초 출발한 위치에서 도착한 위치를 빼서 더 먼 구슬은 도착한 위치 -1을 합니다.
  • 이동하는 순서는 반드시 파란 구슬부터 진행하여, 만약에 파란 구슬이 이동하는 시점에 'O'를 마주한다면 queue에 넣지 않고 continue 합니다.
  • 한 번 설정한 좌표를 토대로 while문으로 장애물 혹은 'O'을 만날 때까지 반복하는 과정에서 파란 구슬은 빨간 구슬의 선행 조건에 상관없이 'O'에 도착한다면 종료합니다.
  • 그 이유는 'O'에 빨간 구슬이 도착하든 혹은 파란 구슬이 먼저 도착하든 구슬은 빠져나가게 되어있습니다. 따라서, 빨간 구슬이 선행되어 있더라도 파란 구슬이 이어서 'O'에 오게 되므로 종료시킵니다.
  • 반면, 빨간 구슬이 'O'에 도착한 경우 바로 return 하지 않는 이유는 queue에 넣는 조건이 red.step > 10이므로 큐에서 나와 들어갔을 때는, step이 10일 수도 있습니다. 따라서, step + 1인 경우 11이므로 -1을 리턴해야 할 수 있습니다.
  • 해당 조건 코드는 queue에서 나올 때 검증하므로 while문에는 작성하지 않았습니다.

 

중요한 포인트는, bfs를 구현하기 위해 방문 위치를 4차원 배열로 설정하여 방문한 곳에 동일하게 방문하는 것을 방지해야합니다. 나아가, 두 큐를 분리하였기 때문에 두 구슬이 동일한 위치에 도착한다면 선행 조건을 파악하기 위해 맨해튼 거리 공식을 적용해야 했습니다.

 

많이 어려웠던 문제였고 해이해진 정신을 바로잡을 수 있는 기회였습니다.

이상으로 SW 기출문제 구슬 탈출2 문제 풀이를 마치겠습니다. 감사합니다.!!!

 

 

참고 블로그: https://wellbell.tistory.com/44

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

 

이번 포스팅은 프로그래머스 표현 가능한 이진트리 자바 풀이를 작성하고자 합니다.

 

문제 출처: https://school.programmers.co.kr/learn/courses/30/lessons/150367

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

1. 풀이 소스

 

class Solution {
    
    int possible;
    
    public int[] solution(long[] numbers) {
        int[] answer = new int[numbers.length];
        
        for (int k = 0; k < answer.length; k++) {
            String binaryNum = Long.toBinaryString(numbers[k]); // 숫자를 바이너리 이진 문자열로 변경 toBinaryString()
            
            int fullTreeLen = 0;
            int h = 1; // 포화 이진트리를 만들기 위한 높이
            
            while (fullTreeLen < binaryNum.length()) {
                fullTreeLen = (int) Math.pow(2, h++) - 1;
            }
            
            boolean[] isOne = new boolean[fullTreeLen]; // 포화 이진 트리를 생성하기 위한 배열
            
            // 참조#1
            int notDummyIdx = isOne.length - binaryNum.length(); // 더미 문자열이 아닌 인덱스
            for (int i = 0; i < binaryNum.length(); i++) {
                isOne[notDummyIdx++] = binaryNum.charAt(i) == '1'; // 더미 문자열이 아닌 인덱스부터 대응되는 인덱스의 문자열이 1인지 판단 
            }
            
            possible = 1; // 정답 초기화
            dfs(0, isOne.length - 1, false, isOne); 
            answer[k] = possible;
            
        }
        return answer;
    }
    
    void dfs(int start, int end, boolean isParentZero, boolean[] isOne) {
        if (possible == 0) return;
        
        int mid = (start + end) / 2; // 포화 이진 트리에서 부모 노드는 자식 노드의 위치의 중간

        if(isParentZero && isOne[mid]) { // 만약 부모가 zero인데, 해당 자식 노드가 1이라면 유효성 X
            possible = 0;
            return; // 종료
        }

        if(start != end) { // 참조#2
            dfs(start, mid - 1, !isOne[mid], isOne); // !fullTree[mid] -> 만약 1이었다면 true 이므로 false
            dfs(mid + 1, end, !isOne[mid], isOne); // 만약 0이었다면 false -> true 즉 부모가 0이므로 자식 노트가 1이 된다면 유효성 X
        }

    }
}

// 문제 해결 방안
// 1. 이진 스트링으로 만들기
// 2. 포화 이진트리로 만들기
// 3. 각 노드가 부모노드가 된다고 가정할 때, 부모노드가 0이라면 자식 노드 1을 가질 수 없으므로 유효성 판단

 

 

2. 풀이 중점 사항

 

문제 풀이 순서는 다음과 같습니다.

-  이진 스트링으로 만들기

-  포화 이진트리로 만들기

-  포화 이진트리로 생성하는 과정에서 이진 문자열을  다시 숫자로 변경한다고 생각할 때, 값이 변경되지 않는 선에서 앞에 0을 여러 개 추가할 수 있습니다. (포화 이진트리 개수 내로)

-  각 노드가 부모노드가 된다고 가정할 때, 부모 노드가 0이라면 자식 노드 1을 가질 수 없으므로 유효성을 판단하기

 

 

이진 스트링으로 변경하기

 

 String binaryNum = Long.toBinaryString(numbers[k]); // 숫자를 바이너리 이진 문자열로 변경 toBinaryString()

 

포화 이진트리로 만들기

 

int fullTreeLen = 0;
int h = 1; // 포화 이진트리를 만들기 위한 높이

while (fullTreeLen < binaryNum.length()) {
    fullTreeLen = (int) Math.pow(2, h++) - 1;
}

 

포화 이진트리는 노드의 개수가 2^h - 1입니다. 따라서, 이진 스트링으로 설정된 문자열의 개수가 포화 이진트리에 해당할 때까지, 반목문으로 fullTreeLen의 개수를 설정합니다.

 

 

참조 #1

 

// 참조#1
int notDummyIdx = isOne.length - binaryNum.length(); // 더미 문자열이 아닌 인덱스
for (int i = 0; i < binaryNum.length(); i++) {
    isOne[notDummyIdx++] = binaryNum.charAt(i) == '1'; // 더미 문자열이 아닌 인덱스부터 대응되는 인덱스의 문자열이 1인지 판단 
}

 

만약 문자열이 101010 (42) 이라고 가정하겠습니다.

해당 문자열의 값을 바꾸지 않는 선에서 포화 이진트리를 만들려면 0101010 이렇게 만들 수 있습니다.

 

//           1
//        1      1
//      0   0  0   0

 

여기서 더미 문자열이 아닌 인덱스 notDummyIdx란 실제 이진 문자열에서 유효한 0, 1이 아닌, 앞에 추가될 0의 개수를 의미합니다. isOne 배열은 포화 이진트리 개수로 선언된 boolean 배열이므로 만약 이진 스트링의 특정 위치의 값이 1이 아닌 경우 모두 false로 설정합니다.

 

참조 #2

 

앞 서 1 : true, 0 : false로 설정한 이유는 다음과 같습니다.

값을 변경하지 않는 선에서 포화 이진 트리를 구성하기 위해서는 부모 노드가 1인 경우 자식 노드를 0으로 추가하여 만들 수 있습니다. 하지만 부모 노드가 0 임에도 자식 노드가 1인 값이 존재한다면, 이는 유효한 경우가 아닙니다.

 

// 95

// 1011111
//             1
//           0    1
//         1  1  1  1 

// 42
// 010101
//           1
//         1   1
//        0 0 0 0

 

예를 들어, 1(노드)을 연결하여 이진 트리를 생성한 후, 빈 공간에 0을 삽입하여 포화 이진트리를 생성해야 하는데,
부모노드가 0이고 자식 노드가 1이라면, 이는 하나로 연결된 트리를 만들 수 없습니다.

따라서, 부모 노드가 0인 경우에 자식 노드가 1인 경우를 찾아서 하나라도 있다면 0을 리턴하면 됩니다.

그 외에는 모두 더미 노드로 0을 채워 넣을 수 있으므로 포화 이진트리로 요구하는 숫자를 완성시킬 수 있습니다.

 

    void dfs(int start, int end, boolean isParentZero, boolean[] isOne) {
        if (possible == 0) return;
        
        int mid = (start + end) / 2; // 포화 이진 트리에서 부모 노드는 자식 노드의 위치의 중간

        if(isParentZero && isOne[mid]) { // 만약 부모가 zero인데, 해당 자식 노드가 1이라면 유효성 X
            possible = 0;
            return; // 종료
        }

        if(start != end) { // 참조#2
            dfs(start, mid - 1, !isOne[mid], isOne); // !fullTree[mid] -> 만약 1이었다면 true 이므로 false
            dfs(mid + 1, end, !isOne[mid], isOne); // 만약 0이었다면 false -> true 즉 부모가 0이므로 자식 노트가 1이 된다면 유효성 X
        }

    }

 

부모 노드를 찾기 위해 자식 노드들의 위치의 중간 인덱스를 찾은 후, start, end가 다르다면 (계속 좁혀가기 때문에 결국 하나로 수렴하게 됨) 부모 노드의 자식 노드를 탐색하여 1 여부를 판단합니다.

!isOne [mid]는 만약 mid 인덱스에 속해있는 값이 1이라면 isOne[mid]는 true입니다.

다음 노드들은 0 혹은 1이어도 상관없으므로 false를 리턴하여 
if (isParentZero && isOne[mid]에 걸리지 않습니다.

 

반면, 0이라면, isOne[mid]는 false가 되고, !isOne[mid]가 true가 되므로,
parent가 0이면서, 자식 노드의 값이 1이라면 possible = 0이 되어 리턴됩니다.

 

 

이상으로 표현 가능 이진트리에 대한 설명을 마치도록 하겠습니다.

감사합니다!

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

 

이번 포스팅은 프로그래머스 합승 택시 요금 자바 풀이를 작성하고자 합니다.

 

문제 출처: https://school.programmers.co.kr/learn/courses/30/lessons/72413

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

1. 풀이 소스

 

import java.util.*;

class Solution {
    
    static final int MAX = Integer.MAX_VALUE;
    List<List<Edge>> graph = new ArrayList<>();
    
    // 다익스트라
    // 각 노드간 연결관계 및 가중치 설정
    // 각 간선의 가중치 최대로 초기화
    // 우선순위 큐로 간선의 최단거리로 업데이트 
    public int solution(int n, int s, int a, int b, int[][] fares) {        
        for (int i = 0; i <= n; i++) graph.add(new ArrayList<>()); // 이차원 리스트 생성
        
        for (int[] fare : fares) {
            graph.get(fare[0]).add(new Edge(fare[1], fare[2])); // 양방향 간선 설정
            graph.get(fare[1]).add(new Edge(fare[0], fare[2]));
        }
        
        int[] costS = new int[n + 1]; // 다익스트라 배열 초기화        
        int[] costA = new int[n + 1];
        int[] costB = new int[n + 1];
        
        Arrays.fill(costS, MAX); // 다익스트라 알고리즘을 적용하기 위해 값 MAX_VALUE 설정
        Arrays.fill(costA, MAX);
        Arrays.fill(costB, MAX); 
        
        costS = dijkstra(s, costS); // 시작점(s)을 기준으로 각 노드별 최단 거리 구하기 참조#1
        costA = dijkstra(a, costA); // 시작점(a)를 기준으로 각 노드별 최단 거리 구하기 
        costB = dijkstra(b, costB); // 시작점(b)를 기준으로 각 노드별 최단 거리 구하기
        
        int answer = MAX;
        for (int i = 1; i <= n; i++) answer = Math.min(answer, costS[i] + costA[i] + costB[i]);  // 참조#2 
        
        return answer;
    }
    
    int[] dijkstra(int start, int[] costs) {
        PriorityQueue<Edge> queue = new PriorityQueue<>(Comparator.comparing((Edge e) -> e.cost));
        
        queue.add(new Edge(start, 0)); // 시작점에서 시작점으로 출발하는 노드이므로 0으로 초기화
        costs[start] = 0; // 시작점의 비용은 0
        
        while(!queue.isEmpty()) {
            Edge now = queue.poll(); // 우선순위 큐에 의해 간선이 적은 순서대로 queue에서 poll
            
            if (now.cost > costs[now.e]) continue; // 만약 x 노드로 가려는 가중치(비용)이 x로 가는 최단거리 배열의 값보다 크다면 패스
            
            for (Edge next : graph.get(now.e)) { // 그래프에 있는 e에서 출발하는 간선을 꺼내 간선이 이동하는 위치에 대한 거리가 최단 거리 값보다 작으면 업데이트
                int cost = next.cost + costs[now.e]; // 다음 노드로 가는 간선의 비용 + 현재 노드로 오는데 걸린 최단거리 비용 => 다음 노드에 대한 최종 비용 
                
                if (costs[next.e] > cost) { // 위에서 계산한 cost가 더 작다면
                    costs[next.e] = cost;
                    queue.add(next); // 현재 계산한 간선은 유효성이 있으므로 (더 작은 비용으로 다음 노드로 움직일 수 있으므로) queue에 입력
                }
            }
        }   
        return costs;
    }    
}

class Edge {
    
    int e; // 노드 번호
    int cost; // 비용
    
    Edge(int e, int cost) {
        this.e = e;
        this.cost = cost;
    }
}

 

 

2. 풀이 과정

 

 

참조#1

 

다익스트라 알고리즘은 특정한 정점에서 다른 정점으로 가는 최단거리를 계산할 수 있습니다. S지점은 합승하여 이동하는 구간으로 S를 시작점으로 가는 정점들은 모두 합승하였을 때, 드는 비용입니다.

 

A를 시작점으로 가는 정점들은 A(도착점)을 기준올 A가 혼자 택시를 타고 이동할 때 드는 비용입니다.

 

즉 합승은 시작점을 S,

A, B가 도착해야 하는 도착지점을 시작점으로 계산하여 다익스트라 알고리즘을 적용하는 것입니다.

 

이렇게 계산하게 된다면, 참조#2를 이용할 수 있습니다.

 

참조#2

 

(해당 그림은 양방향 간선으로 표현해야 하지만, 이해를 돕기 위해 출발 지점에서 어디로 움직인다는 화살표를 추가하였습니다)

 

각 택시요금의 합은 S -> 합승 끝 비용 + A -> S 비용 + B -> S의 비용으로 구할 수 있습니다.

즉, 합승이 끝나는 지점과 A, B가 끝나는 지점까지 드는 비용의 합이 최소가 되는 지점을 구한다면 가장 비용을 절감하는 위치를 구할 수 있는 것입니다. 

 

따라서, 어느 지점 (인덱스)에 도착했을 때, 세 비용의 합이 가장 적은 위치를 구하면 되는 것입니다.

for (int i = 1; i <= n; i++) answer = Math.min(answer, costS[i] + costA[i] + costB[i]);  // 참조#2

 

이상으로 합승 택시 요금 구하기 문제 풀이를 마치도록 하겠습니다. 감사합니다.!

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

 

이번 포스팅은 프로그래머스 - 숫자 카드 나누기 문제 해결에 대한 글을 작성하도록 하겠습니다.

 

문제 소스: https://school.programmers.co.kr/learn/courses/30/lessons/135807

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

1. 풀이 소스

 

//14:49 

class Solution {
    public int solution(int[] arrayA, int[] arrayB) {
        // 나눌 수 있다 = 정수로 약분 가능 = A는 gcdA의 배수이다 
        
        int gcdA = findGCD(arrayA); // A 최대 공약수 찾기
        int gcdB = findGCD(arrayB);
        
        if (gcdA == gcdB) return 0; // 만약 두 최대 공약수가 같다면 어떻게든 두 배열에서 하나 원소 이상씩은 나눠짐
        
        boolean isGCDA = isGCD(gcdA, arrayB); // A배열의 최대 공약수로 B 배열을 나눌 수 있는지 판단
        boolean isGCDB = isGCD(gcdB, arrayA);
        
        if (!isGCDA && !isGCDB) return gcdA <= gcdB ? gcdB : gcdA; // 둘 다 가능한 경우 (false) 더 큰 수
        else if (!isGCDA) return gcdA; // 만약 gcdA만 arrayB의 원소를 나눌 수 없는 경우
        else if (!isGCDB) return gcdB;
       
        return 0;
    }
    
    
    int findGCD(int[] arr) { // 배열에서 공통된 최대 공약수 찾기
        int gcd = arr[0]; // 첫번째 원소
        
        for (int num : arr) {
            gcd = getGCD(gcd, num);
        }
        
        return gcd;
    }
    
    int getGCD(int x, int y) { // 유클리드 호제법으로 최대 공약수 찾기
        if (y == 0) return x;        
        return getGCD(y, x % y);
    }
    
    boolean isGCD(int gcd, int[] arr) { // 찾은 최대 공약수가 배열에서 나뉘어 지는지
        for (int num : arr) {
            if (num % gcd == 0) return true; 
        }
        
        return false;
    }

}

 

 

2. 풀이 과정

 

해당 문제는 최대 공약수를 찾는 과정이 key였습니다. 최대 공약수는 재귀로 해결할 수 있습니다.

 

int getGCD(int x, int y) {
    if (y == 0) return x;
    else getGCD(y, x % y);
}

 

만약 getGCD(4, 2)를 입력할 경우 시뮬에이션은 다음과 같습니다.

 

getGCD(4, 2)

= getGCD(2, 4 % 2)

= getGCD(2, 0)

 

y == 0 : return x => 2

따라서, return 2

 

이러한 방식으로 배열 원소에 대한 공통의 최대 공약수를 찾은 후, gcdA는 arrayB, gcdB는 arrayA에 적용하여 나눌 수 있는지 파악하는 것이 중요하였습니다.

 

이상으로 프로그래머스 숫자 카드 나누기 자바 문제를 마치도록 하겠습니다.

 

감사합니다.!

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

 

이번 포스팅은 프로그매러스 광고 삽입 자바 풀이를 작성하고자 합니다.

 

문제 출처:  https://school.programmers.co.kr/learn/courses/30/lessons/72414#

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

1. 풀이 소스 

 

import static java.lang.Integer.parseInt;

class Solution {
    public String solution(String play_time, String adv_time, String[] logs) {
        
        if (play_time.equals(adv_time)) return "00:00:00";

        int[] allTime = new int[getSecondsTime(play_time) + 1]; // 00:00:00 ~ 00:00:02은 2개의 배열 공간 필요
        
        for (String log : logs) {
            String[] times = log.split("-");
            String sTime = times[0];
            String eTime = times[1];
            
            int start = getSecondsTime(sTime);
            int end = getSecondsTime(eTime);
            
            allTime[start] += 1; // 누적합을 위한 시작 지점 
            allTime[end] -= 1; // 누적합을 위한 마지막 지점에 -1  // 00:00:00 ~ 00:00:02 [1, 1, 0] (00:00 ~ 00:01, 00:00)
        }
        
        for (int i = 1; i < allTime.length; i++) {
            allTime[i] = allTime[i - 1] + allTime[i]; // 이전항에 누적합으로 더함 (00:00:00 ~ 00:00:10)
        }     
        
        int advTime = getSecondsTime(adv_time); // 00:00:00 ~ 00:00:02 (2초)
        long sum = 0;
        
        for (int i = 0; i < advTime; i++) { // advTime이 2초 하면 
            sum += allTime[i];  // ex) 0초부터 ~ 2초까지 누적합을 계산 [1, 1, 0] -> 2
        }
                
        int idx = advTime; // 시작점을 advTime 으로 설정 (2)
        long maxSum = sum; // 가장 큰 토탈 시간
        int startPoint = 0; // 가장 큰 토탈 시간에 맞는 포인트 
        int startCnt = 0; // 누적합에서 포인터를 이동시킬 cnt
        
        // idx = 3, 3초까지의 누적합 0 ~ 1초까지의 누적합 
        for (int i = idx; i < allTime.length; i++) {
            
            sum = sum + allTime[i] - allTime[startCnt]; 
            startCnt++; // 누적합에서 빠진 인덱스이므로 시작 포인트를 위해 ++
            
            if (maxSum < sum) {
                maxSum = sum;
                startPoint = startCnt; // 시작점은 누적합에 포함되기 시작한 (= 누적합에서 빠진 마지막 인덱스)   
            }
        }
        
        return getTime(startPoint);
    }
    
    private int getSecondsTime(String times) {
        String[] time = times.split(":");
        return parseInt(time[0]) * 60 * 60 + parseInt(time[1]) * 60 + parseInt(time[2]);
    }
    
    private String getTime(int point) {
        int hour = point / 3600;
        int minute = (point % 3600) / 60;
        int seconds = (point % 3600) % 60;
            
        return getStringTime(hour) + ":" + getStringTime(minute) + ":" + getStringTime(seconds);
    }
    
    private String getStringTime(int time) {
        String result = "";
        if (time < 10) result = "0" + String.valueOf(time);
        else result = String.valueOf(time);
        return result;
    }
}

 

 

2. 풀이 과정

 

해당 문제는 이전에 풀었던 문제인 누적합 개념을 적용하면 보다 빠르게 해결할 수 있습니다.

누적합이란 임의의 점이 n ~ m까지 범위이고 특정 범위로 시작점 끝점이 주어질 때,

매번 for문으로 주어진 범위까지 1씩 증가하면서 반복해서 더하는 것이 아니라, 주어진 시작점과 끝점에 포인트를 기록하여 

단 한 번의 포문으로 누적합이 될 수 있도록 구현하는 방법입니다.

 

예를 들면 다음과 같습니다.

int[] point = {4, 8};
int[] arr = new int[10];

for (int i = point[0]; i <= point[1]; i++) {
    arr[i] += 1;
}


int[] arr2 = new int[10];

arr2[point[0]] += 1;
arr2[point[1] + 1] -= 1;

for (int i = 1; i < arr2.length; i++) {
    arr2[i] = arr2[i] + arr2[i - 1];
}

 

위의 예시는 누적합을 적용하지 않은 개념으로 범위 계산 시 매번 해당 범위사이에 있는 값을 +1 해야 합니다.

하지만 누적합은 누적할 범위 시작점에 +1, 마지막 점 + 1에 -1을 해줌으로써 누적합을 구현할 수 있습니다.

 

최종적으로 누적합을 계산하면 둘 다 5가 나옵니다. 만약 구해야 하는 point가 10만 개이고, 범위가 최대 1000만이라면
앞 서 하나씩 1을 증가시켜서 누적합을 계산하는 경우 최악은 10만 * 범위(1000만) 까지의 시간 복잡도가 나옵니다.

 

반면, 누적합을 구하는 경우 point 시작점, 끝점 계산은 2개가 소요되고, 마지막 누적합만 적용하면 되므로

2 * 10만 + 1000만의 시간 복잡도가 소모됩니다.

 

따라서, 이 원리를 적용하여 해당 문제를 해결하려면 범위 계산을 위해 각 영상 시작 시간과 종료시간을 모두 초 단위로 바꿔야 합니다. 

 

예를 들어 "01:00:00 - 01:01:00" 은 초단위로 설정하면 3600 * 1  ~ 3600 * 1 + 60 * 1입니다.

즉 영상 시작이 0초대에 시작한다고 하면 영상 시작으로 3600초 ~ 3660 사이입니다. 

 

그렇다면 해당 영상 누적 합을 위해 범위를 어떻게 설정해야 할까요?

0 ~ 1초까지의 누적시간은 1초입니다. 이 1초를 어느 위치에 선정하느냐에 따라 접근 방식이 달라질 수 있습니다.

 

저는 누적 시간이 가장 크면서 가장 빠른 시간을 구해야 하므로,

0 ~ 1초 사이의 누적합 1초는 인덱스 0에 기록하는 방법을 택했습니다.

 

즉, [0, 0, 0] (0 ~ 3초)의 영상 시간에 광고가 0 ~ 1초 동안 지속한다면, 

[1, -1, 0]로 누적합을 위한 배열 A를 설정하고, 최종 누적합으로 인한 계산은 [1, 0, 0]이 나옵니다.

 

이를 이용하여, 누적합을 계산한 후,

for문으로 다음 인덱스와 시작점 인덱스를 옮긴 후, 고정된 K 범위 내에서 가장 많은 누적시간을 기록한 startPoint를 찾습니다.

오름차순으로 for문 인덱스를 설정하는 경우 이전 maxSum과 sum을 비교하여 sum이 더 큰 경우를 찾는다면 가장 빠른 startPoint를 찾을 수 있습니다.

 

 

테스트 케이스 공유 : 

 

"99:59:59" "00:00:01" ["69:59:59-89:59:59", "01:00:00-21:00:00", "79:59:59-99:59:59", "11:00:00-31:00:00"] "11:00:00"

"00:00:10"  "00:00:09"  ["00:00:00-00:00:03", "00:00:03-00:00:04", "00:00:03-00:00:05", "00:00:09-00:00:10"]  "00:00:00"

"00:00:10"  "00:00:09"   ["00:00:01-00:00:03", "00:00:03-00:00:04", "00:00:03-00:00:05", "00:00:09-00:00:10", "00:00:09-00:00:10"] "00:00:01"

"00:00:10"  "00:00:07"  ["00:00:02-00:00:03", "00:00:03-00:00:04", "00:00:03-00:00:05"]  "00:00:00"

"00:00:10" "00:00:01" ["00:00:01-00:00:02", "00:00:03-00:00:04", "00:00:03-00:00:05"] "00:00:03"

"00:00:10" "00:00:01" ["00:00:01-00:00:02", "00:00:03-00:00:04", "00:00:02-00:00:05"] "00:00:03"

"00:00:10" "00:00:02" ["00:00:01-00:00:02", "00:00:02-00:00:04", "00:00:02-00:00:05"] "00:00:02"

"50:00:00" "49:50:00" ["15:36:51-38:21:49", "10:14:18-15:36:51", "38:21:49-42:51:45"] "00:00:00"

 

 

이상으로 해당 문제 풀이를 마치도록 하겠습니다. 감사합니다.!!

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

 

이번 포스팅은 프로그래멋 110 옮기기 자바 풀이를 작성하고자 합니다.

 

문제 출처: https://school.programmers.co.kr/learn/courses/30/lessons/77886

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

해당 문제는 풀지 못하여 블로그를 참고하여 코드를 작성하였고 이에 대한 방법을 주석화하여 정리하였습니다.

 

 

 

1. 풀이 소스 

import java.util.*;

class Solution {
    public String[] solution(String[] s) {
        String[] answer = new String[s.length]; // 정답 배열 설정
        StringBuilder sb; 
        
        for (int i = 0; i < s.length; i++) {
            String str = s[i]; 
            Stack<Character> stack = new Stack<>(); 
            
            int cnt = 0; // 연속된 캐릭터 타입의 값이 110인 것의 개수 
            
            for (int j = 0; j < str.length(); j++) {
                char z = str.charAt(j); // 스택이 2 이상 && z가 0이라면 스택에서 두개를 빼서  110 비교
                
                if(z == '0' && stack.size() >= 2) {
                    char y = stack.pop(); 
                    char x = stack.pop();  
                    
                    if (x == '1' && y == '1' && z == '0') {
                        cnt++; 
                    } else {
                        stack.push(x);  // 110이 아니라면 다시 후입 선출을 위해 x y z 순으로 스택에 넣기
                        stack.push(y);
                        stack.push(z);
                    }
                } else {
                    stack.push(z); // 스택 사이즈가 2보다 작다면 z가 1이더라도 스택에서 빼서 110을 만들 수 없으므로
                }     
            }
            
            int idx = stack.size(); // idx는 stack 사이즈 (연속 수가 110이 아닌 경우 스택에 다 넣음)
            
            sb = new StringBuilder();
            
            // 참조1
            boolean flag = false; // 맨 처음 0이 위치하는 지점 전까지 idx를 감소하기 위함 하단에 예시
            
            while(!stack.isEmpty()) {
                if (!flag && stack.peek() == '1') idx--;
                
                if (!flag && stack.peek() == '0') flag = true;  // 스택에서 뺀 값이 0이라면 0 자리에 위치
                
                sb.insert(0, stack.pop()); // sb에 정순으로 입력함으로써 원래 문자열에서 110만 뺀 상태
            }
            
            if (cnt > 0) { // 110 개수가 양수라면
                while (cnt > 0) {
                    sb.insert(idx, "110"); // 스택에서 뺄 때 (역순이므로 나중에 나오는 값이 앞쪽) 0이 나오는 다음의 인덱스
                    idx += 3; // 3개를 추가했으므로 +3
                    cnt --; // cnt--
                }
                answer[i] = sb.toString();
            } else {
                answer[i] = s[i]; // 만약 추가할 110이 없다면 문자열 그대로 리턴
            }
        }
        return answer;
    }

}

 

 

2. 풀이 중점 사항

 

해당 문제의 중요한 포인트는 stack을 활용하여 문자열 탐색을 합니다. 110이 나온다면 카운트를 증가시키고 그 외의 문자열은 모두 스택에 넣습니다. 사전순 정렬을 이용한다면 110은 10, 0보다 앞서 있을 수 없습니다. 

즉 0이 나오는 시점 마지막 시점의 바로 뒤에 위치한다면 00000101000 110 11111 사전순 정렬에서 앞에 만들 수 있습니다.

 

이를 구현하기 위한 자료는 참고#1입니다.

 

가령 문자열이 다음과 같이 있습니다.

문자열: 11010111 

먼저 문자열을 탐색하여 110이 있다면 카운트를 증가시키고 연속된 숫자로 110이 아니라면 모두 스택에 넣습니다.

cnt = 1

stack : 10111        (후입 선출로 인덱스 4가 이 먼저 출력)

idx = 5 (stack size)

boolean flag = false // 해당 flag는 문자열에서 가장 마지막의 0, 스텍 기준으로는 가장 빠른 0이 나오면 true로 바꿈

sb.insert(0, stack.pop()) 스텍에서 나온 문자열을 정순으로 배치하기 위해 놓습니다.

 

시뮬레이션을 해보면 

sb = 111

stack : 10

flag = false

idx = 2

cnt = 1

 

sb = 0111

stack = 1 

flag = true

idx = 2

cnt = 1

 

sb = 10111

stack : x

flag = true

idx = 2

cnt = 1

 

cnt > 0 이므로 idx의 위치인 10111 의 10 <1> 11에 110을 추가합니다.

10110111 이렇게 설정하게 되면 문자열 기준으로 사전 순 정렬에서 더 빠른 정렬을 만들 수 있습니다.

만약 101이더라도 101101을 만들어야 하는데, 그 이유는 110을 사전순 정렬을 위해 잠시 빼놓았을 뿐 반드시 써야 하기 때문에

1이 하나더라도 1101로 만들면 이전 최초 문자열보다 더 빠른 문자열을 만들 수 있습니다.

 

최초 11010111 

변경 10110111

 

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

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

 

참고 출처: https://wellbell.tistory.com/228

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

 

이번 문제는 프로그래머스 모두 0으로 만들기 자바 풀이를 작성하도록 하겠습니다.

 

문제 출처: https://school.programmers.co.kr/learn/courses/30/lessons/76503#

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

1. 풀이 소스

 

import java.util.*;

class Solution {
    public long solution(int[] a, int[][] edges) {
        Graph graph = new Graph(a);
        
        if (graph.isInitEnd() == 0 || graph.isInitEnd() == -1) {
            return graph.isInitEnd(); // 0 또는 -1 종료 조건
        }
        
        graph.addEdge(edges); // graph에 edge 설정
        graph.dfs(0);      
        
        return graph.getAnswer();
    }
}

class Graph {
    
    Node[] nodes; // 노드 배열
    boolean[] visited; // 방문 여부
    long answer; // 정답 long 타입!
    boolean isAllZero; // 모두 0인지 아니면 어떤 수는 0이 아닌지로 종료 여부 체크
    long isPossibleSumZero; // 합이 0이 가능한지 체크
    
    class Node {
        int x;
        long w;
        List<Node> adjacent = new ArrayList<>();
        
        Node (int x, long w) {
            this.x = x;
            this.w = w;
        }
    }
    
    Graph(int[] arr) {
        nodes = new Node[arr.length]; // 노드 배열
        visited = new boolean[arr.length]; // 방문 여부 체크
         
        for (int i = 0; i < nodes.length; i++) {
            nodes[i] = new Node(i, arr[i]); // 노드 생성
            if (arr[i] != 0) this.isAllZero = false; // 하나라도 0이 아니라면 어떤 수는 0이 아니므로 false
            isPossibleSumZero += arr[i]; // 모든 수의 합이 zero가 될 수 있는지 체크
        }
        
    } 
    
    
    int isInitEnd() {
        if (isPossibleSumZero != 0) return -1; // 모든 수의 합이 0이 안된다면 조료
        if (this.isAllZero) return 0; // 이미 모든 수가 0이라면 종료
        return 1; 
    }
    
    void addEdge(int[][] edges) {
        
        for (int i = 0; i < edges.length; i++) {
            int u = edges[i][0]; // edge 설정
            int v = edges[i][1];
            
            Node n1 = nodes[u];
            Node n2 = nodes[v];
            
            n1.adjacent.add(n2); // 양방향 연결관계
            n2.adjacent.add(n1);
        }
    }
    
    long dfs(int x) {
        
        visited[x] = true; // 방문 체크
        
        Node node = nodes[x]; // 노드 설정
        
        for (int i = 0; i < node.adjacent.size(); i++) {
            Node adjacent = node.adjacent.get(i); // 인접한 노드 가져옴
            
            if (!visited[adjacent.x]) {
                node.w += dfs(adjacent.x); // dfs로 인접한 노드로부터 가중치를 업데이트하여 가져옴 --> 중간 노드가 선택된다면 리프노드로 계속 방문하여 w 업데이트 
            }
        }
        
        this.answer += Math.abs(node.w); // 노드의 가중치의 절대값을 더함 (- 혹은 + 도 0으로 만들기 위해서는 정량적인 수가 필요하므로)
        
        return node.w; // 노드의 가중치를 리턴하여 dfs
    }
    
    
    long getAnswer() {
        return this.answer;
    }
    
}

 

 

2. 풀이 과정

 

해당 문제는 연결된 트리가 zeroSum을 통해 0이 될 수 있는지 판별할 수 있는 조건이 필요했습니다.

이를 위해 모든 수의 합이 0이 된다면, 연결위치에 상관없이 모두 0으로 만들 수 있다는 사실에 근거하여,

먼저 isPossibleSumZero가 가능한지 판별하였습니다.

또한 모두 0인지 판단하기 위한 조건을 isAllZero를 추가하여 조기 종료가 수행될 수 있도록 하였습니다.

 

핵심 로직은 dfs를 이용하여 풀이를 했습니다. 

 

          1 -- 1 -- 1 -- 1
          |
 -9  -- 1 -- 2 
  |     |   

  0     2

 

이와 같은 트리 구조가 있다고 가정하겠습니다.

dfs로 0번째 인덱스를 선택했을 때, 0이 선택된다면 인접한 노드를 검색합니다. 

 

인접한 노드 1(가중치 -9)를 선택하게 되면서 노드 1에 대한 dfs를  진행합니다. 

즉, 노드 1의 인접한 노드인 노드 2(가중치 1)를 찾은 후 다시 dfs를 적용하는 방식으로 재귀적 호출로 최종 리프(루트) 노드에 도달하게 된다면, 더 이상 방문할 노드가 없으므로 answer를 업데이트하고 node.w를 리턴하며 재귀적으로 node.w가 업데이트하게 됩니다,

 

최종적으로 answer를 리턴하면 됩니다.

해당 문제에 대한 테스트 케이스를 추가로 작성하였는데 공유드리겠습니다.

 

//        1 -- 1 
//        |
// -7  -- 1 -- 2
//  |     |
//  0     2
//  

// 7 + 2 + 4  = 14


//        1 -- 1 -- 1 -- 1
//        |
// -9  -- 1 -- 2 
//  |     |    
//  0     2


//  9 + 2 + 2 + 1 + 2 + 3 + 4  = 23


//                              -2
//                             /
//   2 -- 2 -- 2       -8 -- -7 -- -2 
//              \     /  \
//    3 -- 2 -- 8 -- 7    -2 -- -3
//                          \
//                           -2
// 92

 

테스트 케이스 

[-7, 0, 2, 1, 2, 1, 1], [[0, 1], [3, 4], [2, 3], [0, 3], [5, 3], [6, 5]], 14

 

[-9, 0, 2, 1, 2, 1, 1, 1, 1], [[0, 1], [3, 4], [2, 3], [0, 3], [5, 3], [6, 5], [7, 6], [8, 7]], 23

 

[-9, 0, 2, 1, 2, 1, 1, 1, 1, -18, 18]. [[0, 1], [3, 4], [2, 3], [0, 3], [5, 3], [6, 5], [7, 6], [8, 7], [9, 0], [10, 2]], 95

 

[2, 2, 2, 3, 2, 8, 7, -8, -7, -2, -2, -2, -3, -2], [[0, 1], [1, 2], [3, 4], [4, 5], [2, 5], [5, 6], [6, 7], [7, 8], [8, 9], [8, 10], [7, 11], [11, 12], [11, 13]], 92

 

이상으로 해당 문제 풀이를 마치겠습니다.! 감사합니다.

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

 

이번 포스팅은 프로그래머스 주차 요금 계산에 대한 글을 작성하고자 합니다.

 

문제 출처: https://school.programmers.co.kr/learn/courses/30/lessons/92341

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

1. 풀이 소스

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

class Solution {
    public int[] solution(int[] fees, String[] records) {
        Parking park = new Parking(fees); // 인스턴스 생성
        return park.apply(records);
    }   
}

class Parking {
    static int basicTime; // 기본 제공 시간
    static int basicFee; // 기본 요금
    static int unit; // 단위 시간
    static int unitFee; // 단위 요금
    static final int LAST_TIME = 1439; // 1380 + 59 = 1439 (23:59)
    
    Map<String, ParkFee> map = new HashMap<>(); // 차량 번호 기록 map
    
    class ParkFee {
        int inTime; // 입차
        int usedTime; // 누적 사용 시간

        ParkFee(int inTime) {
            this.inTime = inTime;
        }
    }
    
    Parking(int[] fees) { // 초기화
        basicTime = fees[0];
        basicFee = fees[1];
        unit = fees[2];
        unitFee = fees[3];
    }
    
    void add(String carNumber, int inTime) { // time: 분으로 설정된 시간
        if (map.containsKey(carNumber)) { 
            ParkFee parkFee = map.get(carNumber); // 차량 시간이 있다면 intime 값 변경
            parkFee.inTime = inTime;
        } else map.put(carNumber, new ParkFee(inTime)); // 없다면 인스턴스 생성
    }
    
    int out(String carNumber, int outTime) {     
        ParkFee parkFee = map.get(carNumber);
        int inTime = parkFee.inTime;
        parkFee.inTime = -1;  // 00:00의 경우 inTime이 0이므로 -1로 설정
        return outTime - inTime; // 출차 - 입차 시간 계산
    }
    
    int calFee(int usedTime) {
        if (usedTime <= basicTime) return basicFee; // 만약 기본 제공 시간보다 적은 경우 기본 요금
        int additionalTime = (usedTime - basicTime) % unit == 0 ?  (usedTime - basicTime) / unit : ((usedTime - basicTime) / unit) + 1;   // 단위 계산을 위한 올림 계산
        return basicFee + additionalTime * unitFee; // 기본 요금 + 추가 시간 * 단위 비용
    }
    
    
    int[] apply (String[] records) {
        for (String record : records) {
            String[] rc = record.split(" ");
            String[] timeZone = rc[0].split(":");
            int time = parseInt(timeZone[0]) * 60 + parseInt(timeZone[1]);
            
            String carNumber = rc[1];
            boolean in = rc[2].equals("IN");
            
            if (in) add(carNumber, time);
            
            else {
                ParkFee parkFee = map.get(carNumber);
                parkFee.usedTime += out(carNumber, time); // 출차 - 입차로 누적 시간 업데이트
            }          
        }
        
        List<String> keys = new ArrayList<>();
        keys.addAll(map.keySet()); // ketSet (차 번호) 모두 keys에 업데이트
        keys.sort(Comparator.comparing(s -> s)); // 차 번호로 정렬
        
        int[] result = new int[keys.size()];
        
        for (int i = 0; i < keys.size(); i++) {
            ParkFee parkFee = map.get(keys.get(i));
           
            int fee = 0;
            if (parkFee.inTime != -1) {
                parkFee.usedTime += LAST_TIME - parkFee.inTime; // 누적 시간 업데이트
            } 
            
            result[i] = calFee(parkFee.usedTime); // 누적시간으로 금액 계산
        }
        
        return result;
    }
}

 

 

2. 풀이 과정

 

해당 문제는 inTime, outTime을 설정할 때, 누적 시간을 체크해야 하는 부분이 있습니다.

즉, 여러 번 입차 및 출차하였더라도 누적 시간으로 금액을 측정해야하므로 ParkFee라는 객체에 누적 시간을 더하는 usedTime을 적용하여, 출차 시 출차 - 입차 시간을 더하는 과정을 수행하였습니다.

 

처음에 실수한 부분은 "00:00"은 시간 * 60 + 분 설정에 의하면 0이 나옵니다. 여기서 출차 후 inTime을 0으로 설정하게 되면,
해당 값이 00:00에 입차를 하여 출차가 된 것인지 아니면 아직 출차가 안된 채로 남아있는지 구분이 어려울 수 있습니다.
따라서, -1로 설정하여 구분하도록 하였고 마지막에 calFee로 요금을 계산하는 과정을 처리하였습니다.

 

자세한 사항은 주석으로 표기하였습니다.

 

감사합니다.!

+ Recent posts