enoch012 / JavaBasicStudy

Java 기초 스터디 (2023.09 ~ 10)
0 stars 1 forks source link

11월 30일 / 코딩테스트 연습 (gwangho) #57

Open SDeung01 opened 11 months ago

SDeung01 commented 11 months ago

문제 1 : 게임 맵 최단거리

이번에는 탐색 알고리즘 문제 중에서 BFS 위주로 풀어보았습니다. 알고리즘 설명을 보아도 도저히 구현하는 방법을 모르겠어서 다른 사람의 풀이를 참고한 후 일부 변형하여 다시 푼 문제입니다.

문제

ROR 게임은 두 팀으로 나누어서 진행하며, 상대 팀 진영을 먼저 파괴하면 이기는 게임입니다. 따라서, 각 팀은 상대 팀 진영에 최대한 빨리 도착하는 것이 유리합니다.

지금부터 당신은 한 팀의 팀원이 되어 게임을 진행하려고 합니다. 다음은 5 x 5 크기의 맵에, 당신의 캐릭터가 (행: 1, 열: 1) 위치에 있고, 상대 팀 진영은 (행: 5, 열: 5) 위치에 있는 경우의 예시입니다.

image

위 그림에서 검은색 부분은 벽으로 막혀있어 갈 수 없는 길이며, 흰색 부분은 갈 수 있는 길입니다. 캐릭터가 움직일 때는 동, 서, 남, 북 방향으로 한 칸씩 이동하며, 게임 맵을 벗어난 길은 갈 수 없습니다. 아래 예시는 캐릭터가 상대 팀 진영으로 가는 두 가지 방법을 나타내고 있습니다.

image

image

위 예시에서는 첫 번째 방법보다 더 빠르게 상대팀 진영에 도착하는 방법은 없으므로, 이 방법이 상대 팀 진영으로 가는 가장 빠른 방법입니다.

만약, 상대 팀이 자신의 팀 진영 주위에 벽을 세워두었다면 상대 팀 진영에 도착하지 못할 수도 있습니다. 예를 들어, 다음과 같은 경우에 당신의 캐릭터는 상대 팀 진영에 도착할 수 없습니다.

image

게임 맵의 상태 maps가 매개변수로 주어질 때, 캐릭터가 상대 팀 진영에 도착하기 위해서 지나가야 하는 칸의 개수의 최솟값을 return 하도록 solution 함수를 완성해주세요. 단, 상대 팀 진영에 도착할 수 없을 때는 -1을 return 해주세요.

입출력 예

maps answer
[[1,0,1,1,1],[1,0,1,0,1],[1,0,1,1,1],[1,1,1,0,1],[0,0,0,0,1]] 11
[[1,0,1,1,1],[1,0,1,0,1],[1,0,1,1,1],[1,1,1,0,0],[0,0,0,0,1]] -1

문제 풀이

import java.util.LinkedList;
import java.util.Queue;

class Solution {

    // 탐색지의 위치 정보를 가지는 내부 클래스 Point
    class Point {     
        private int x;
        private int y;
        private int distance;

        public Point(int x, int y, int distance) {
            this.x = x;
            this.y = y;
            this.distance = distance;
        }
    }

    public int solution(int[][] maps) {
        int xLength = maps[0].length;
        int yLength = maps.length;

        // 탐색지 주변 상하좌우
        int[] aroundX = {0, 0, -1, 1};
        int[] aroundY = {-1, 1, 0, 0};

        // 해당 노드의 탐색여부를 기록하는 boolean 배열
        boolean[][] visited = new boolean[yLength][xLength];

        // 탐색 중인 노드의 정보(Point)를 저장하는 큐
        // 큐의 사이즈와 한번에 탐색중인 길의 갈래 수는 일치한다.
        Queue<Point> roadQ = new LinkedList<>();

        // 출발지점의 위치를 큐와 탐색여부 배열에 저장, 출발 위치도 1칸으로 치므로 Point(0,0,1)
        roadQ.offer(new Point(0, 0, 1));
        visited[0][0] = true;

        // 큐가 빌 때 까지 반복(큐가 빈다면 더이상 탐색이 불가능하다는 의미)
        while(!roadQ.isEmpty()){
            Point road = roadQ.poll();
            int distance = road.distance;

            for(int j = 0; j < 4; j++){
                int x = road.x + aroundX[j];
                int y = road.y + aroundY[j];

                // 만약 출구에 도착한다면 현재 위치 + 1 (= 탐색할 위치, 출구)를 return
                if(x == xLength -1 && y == yLength -1) return distance+1;

                // 맵의 범위 내에서, 길이 막혔거나(0) 이미 방문한 지점이라면 탐색하지 않는다.
                // 탐색 가능한 장소라면 방문여부(true)를 기록하고 해당 탐색지의 정보를 큐에 저장한다.
                // 만약 탐색 가능한 장소가 1곳 이상(갈림길)이라면 큐에 저장되는 Point객체도 늘어난다.
                if(x >= 0 && x < xLength && y >= 0 && y < yLength){
                    if(maps[y][x] == 0 || visited[y][x]) continue;
                    visited[y][x] = true;
                    roadQ.offer(new Point(x, y, distance +1));
                }
            }
        }

        return -1;  // 출구에 도착하지 못하고 탐색이 종료되면 -1 리턴
    }

}

회고

class Solution {

class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

public int solution(int[][] maps) {
    int xLength = maps[0].length;
    int yLength = maps.length;
    int x = 0, y = 0;

    int[] aroundX = {0, 0, -1, 1};
    int[] aroundY = {-1, 1, 0, 0};

    int cnt = 0;
    boolean[][] visited = new boolean[yLength][xLength];
    Queue<Point> roadQ = new LinkedList<>();
    roadQ.offer(new Point(x, y));
    visited[y][x] = true;

    while(!roadQ.isEmpty()){
        cnt++;
        for(int i = 0; i < roadQ.size(); i++){
            Point road = roadQ.poll();
            for(int j = 0; j < 4; j++){
                x = road.x + aroundX[j];
                y = road.y + aroundY[j];
                if(x == xLength -1 && y == yLength -1) return ++cnt;
                if(x >= 0 && x < xLength && y >= 0 && y < yLength){
                    if(maps[y][x] == 0 || visited[y][x]) continue;
                    visited[y][x] = true;
                    roadQ.offer(new Point(x,y));
                }
            }
        }

    }
    return -1;
} 

}


- 거의 유사한 접근 방법인데 이상하게도 일부는 맞고 일부는 틀린 것으로 나와 이유를 찾지 못한 채 한참 헤맸다.
- 원인은 의외로 간단했는데 큐의 size만큼 반복하는 for문의 조건식에 `roadQ.size()`를 바로 사용했던 것.
- 반복문에서 `roadQ.poll()`을 사용하기 때문에 코드가 반복될 떄마다 큐의 size가 변동하여 의도한 것과 다른 결과가 나온 탓이었다.
- 따라서 `roadQ.size()` 의 값을 미리 변수에 저장하여 변동하지 않는 상수 취급을 한 후 사용해주어야 한다.
```java
       while(!roadQ.isEmpty()){
            cnt++;
            int size = roadQ.size();
            for(int i = 0; i < size; i++){
                Point road = roadQ.poll();
                for(int j = 0; j < 4; j++){
                    x = road.x + aroundX[j];
                    y = road.y + aroundY[j];
                    if(x == xLength -1 && y == yLength -1) return ++cnt;
                    if(x >= 0 && x < xLength && y >= 0 && y < yLength){
                        if(maps[y][x] == 0 || visited[y][x]) continue;
                        visited[y][x] = true;
                        roadQ.offer(new Point(x,y));
                    }
                }
            }
        }
SDeung01 commented 11 months ago

문제 2 : 무인도 여행

문제

메리는 여름을 맞아 무인도로 여행을 가기 위해 지도를 보고 있습니다. 지도에는 바다와 무인도들에 대한 정보가 표시돼 있습니다. 지도는 1 x 1크기의 사각형들로 이루어진 직사각형 격자 형태이며, 격자의 각 칸에는 'X' 또는 1에서 9 사이의 자연수가 적혀있습니다. 지도의 'X'는 바다를 나타내며, 숫자는 무인도를 나타냅니다. 이때, 상, 하, 좌, 우로 연결되는 땅들은 하나의 무인도를 이룹니다. 지도의 각 칸에 적힌 숫자는 식량을 나타내는데, 상, 하, 좌, 우로 연결되는 칸에 적힌 숫자를 모두 합한 값은 해당 무인도에서 최대 며칠동안 머물 수 있는지를 나타냅니다. 어떤 섬으로 놀러 갈지 못 정한 메리는 우선 각 섬에서 최대 며칠씩 머물 수 있는지 알아본 후 놀러갈 섬을 결정하려 합니다.

지도를 나타내는 문자열 배열 maps가 매개변수로 주어질 때, 각 섬에서 최대 며칠씩 머무를 수 있는지 배열에 오름차순으로 담아 return 하는 solution 함수를 완성해주세요. 만약 지낼 수 있는 무인도가 없다면 -1을 배열에 담아 return 해주세요.

제한사항

입출력 예

maps result
["X591X","X1X5X","X231X", "1XXX1"] [1, 1, 27]
["XXX","XXX","XXX"] [-1]

문제풀이

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Queue;

class Solution {

    // 위치 정보를 가지는 내부 클래스
    class Pos{
        private int x;
        private int y;

        public Pos(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    public int[] solution(String[] maps) {
        int[][] newMap = newMap(maps);
        int xLength = maps[0].length();
        int yLength = maps.length;
        int[] aroundX = {0, 0, -1, 1};
        int[] aroundY = {-1, 1, 0, 0};

        boolean[][] visited = new boolean[yLength][xLength];
        Queue<Pos> posQ = new LinkedList<>();
        // BFS로 탐색을 완료한 섬 하나의 음식 총량을 List에 담는다.
        ArrayList<Integer> islandList = new ArrayList<>();

        // [0,0] 좌표부터 탐색 시작점을 옮겨가며 BFS를 반복한다.
        for(int y = 0; y < yLength; y++){
            for (int x = 0; x < xLength; x++){
                int sum = 0;
                // 탐색시작점이 바다가 아니고 아직 탐색하지 않은 장소라면 BFS 시작
                if(newMap[y][x] > 0 && !visited[y][x]){
                    // 시작지점 세팅
                    // 미리 큐에 담은 Pos가 없고, 탐색 시 큐를 모두 비울때까지 탐색하므로 시작시 큐는 항상 비어있다.
                    if(posQ.isEmpty()){
                        posQ.offer(new Pos(x,y));
                        visited[y][x] = true;
                        sum += newMap[y][x];
                    }
                    // BFS
                    while(!posQ.isEmpty()) {
                        Pos pos = posQ.poll();
                        for(int i = 0; i < 4; i++){
                            int expX = pos.x + aroundX[i];
                            int expY = pos.y + aroundY[i];
                            if(expX >= 0 && expX < xLength && expY >= 0 && expY < yLength){
                                if(newMap[expY][expX] == 0 || visited[expY][expX]) continue;
                                visited[expY][expX] = true;
                                sum += newMap[expY][expX]; // 탐색 중 발견한 음식의 양을 더함
                                posQ.offer(new Pos(expX, expY));
                            }
                        }
                    } islandList.add(sum);
                }
            }
        }
        Collections.sort(islandList);

        int[] result = islandList.stream().mapToInt(i->i).toArray();

        return islandList.isEmpty() ? new int[] {-1} : result;
    }

    // String 배열 형태의 maps를 문자 단위로 쪼갠 후 숫자로 변환하여 int 2차원 배열의 형태로 반환
    // 음식(숫자)은 1~9까지이므로 음식이 없는 위치(바다, X)는 0으로 저장
    private int[][] newMap(String[] maps){
        int[][] newMap = new int[maps.length][maps[0].length()];
        for(int y = 0; y < maps.length; y++){
            for(int x = 0; x < maps[0].length(); x++){
                char food = maps[y].charAt(x);
                if(Character.isDigit(food)){ 
                    // 제한조건에 위배되지 않으므로 편의를 위해 .isDigit()을 사용하였으나
                    // 예상 못한 오류를 피하려면 1~9까지의 범위를 명확히 주는 것이 좋다.
                    newMap[y][x] = food - '0';
                }else {
                    newMap[y][x] = 0;
                }
            }
        } return newMap;
    }
}

회고

class Solution {

static int[] dI = {1,0,-1,0};
static int[] dJ = {0,-1,0,1};
static ArrayList<Integer> arr = new ArrayList<>();

public int[] solution(String[] maps) {

    int[][] map = new int[maps.length+2][maps[0].length()+2];
    Boolean[][] visit = new Boolean[maps.length+2][maps[0].length()+2];
    for(int[] temp: map) {
        Arrays.fill(temp, 0);
    }
    for(Boolean[] temp: visit) {
        Arrays.fill(temp, true);
    }

    Boolean flag =false;

    for(int i = 1; i <= maps.length; i++) {
        for(int j = 1; j <= maps[0].length(); j++) {
            if(maps[i-1].charAt(j-1) == 'X') {
                map[i][j] = 0;
            }
            else {
                map[i][j] = maps[i-1].charAt(j-1) - '0';
                visit[i][j] = false;
                flag = true;
            }
        }
    }

    if(!flag) {
        return new int[] {-1};
    }

    for(int i = 1; i <= maps.length; i++) {
        for(int j = 1; j <= maps[0].length(); j++) {
            if(map[i][j] > 0 && !visit[i][j]) {
                DFS(visit, map, i, j,0);
            }
        }
    }

    int ans[] = new int[arr.size()];
    for(int i = 0; i < arr.size(); i++) {
        ans[i] = arr.get(i);
    }
    Arrays.sort(ans);
    return ans;
}

int DFS(Boolean[][] visit, int[][] map, int i, int j, int depth) {
    if(visit[i][j] || map[i][j] == 0) {
        return 0;
    }

    visit[i][j] = true;
    int ans = map[i][j];
    for(int k = 0; k < 4; k++) {
        ans += DFS(visit,map,i+dI[k],j+dJ[k], depth+1);
    }
    if(depth == 0) {
    arr.add(ans);
    }
    return ans;
}

}

SDeung01 commented 11 months ago

문제3 : 미로 탈출

문제

1 x 1 크기의 칸들로 이루어진 직사각형 격자 형태의 미로에서 탈출하려고 합니다. 각 칸은 통로 또는 벽으로 구성되어 있으며, 벽으로 된 칸은 지나갈 수 없고 통로로 된 칸으로만 이동할 수 있습니다. 통로들 중 한 칸에는 미로를 빠져나가는 문이 있는데, 이 문은 레버를 당겨서만 열 수 있습니다. 레버 또한 통로들 중 한 칸에 있습니다. 따라서, 출발 지점에서 먼저 레버가 있는 칸으로 이동하여 레버를 당긴 후 미로를 빠져나가는 문이 있는 칸으로 이동하면 됩니다. 이때 아직 레버를 당기지 않았더라도 출구가 있는 칸을 지나갈 수 있습니다. 미로에서 한 칸을 이동하는데 1초가 걸린다고 할 때, 최대한 빠르게 미로를 빠져나가는데 걸리는 시간을 구하려 합니다.

미로를 나타낸 문자열 배열 maps가 매개변수로 주어질 때, 미로를 탈출하는데 필요한 최소 시간을 return 하는 solution 함수를 완성해주세요. 만약, 탈출할 수 없다면 -1을 return 해주세요.

제한사항

입출력 예

maps result
["SOOOL","XXXXO","OOOOO","OXXXX","OOOOE"] 16
["LOOXS","OOOOX","OOOOO","OOOOO","EOOOO"] -1

문제풀이

class Solution {

class Point{
    private final int x;
    private final int y;
    private final int sec;

    public Point(int x, int y, int sec) {
        this.x = x;
        this.y = y;
        this.sec = sec;
    }
}

public int solution(String[] maps) {

    // BFS 탐색을 위한 기본 세팅
    int xLength = maps[0].length();
    int yLength = maps.length;
    boolean[][] visited = new boolean[yLength][xLength];
    Queue<Point> roadQ = new LinkedList<>();
    int[][] direction = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; // x,y 상하좌우

    // 시작 지점을 탐색하여 roadQ와 visited에 시작지점의 위치를 세팅
    int[] start = findStart(maps);
    roadQ.offer(new Point(start[0], start[1], 0));
    visited[start[1]][start[0]] = true;

    // 레버 위치까지 탐색하고 만약 레버에 도착하지 못했다면 -1 리턴
    char targetL = 'L';
    Point leverPoint = goToPoint(roadQ, maps, visited, direction, xLength, yLength, targetL);
    if(leverPoint == null) return -1;

    // 새 탐색을 위해 roadQ에 저장된 값을 모두 제거
    // visited를 새로 초기화 (지나온 길을 다시 지나갈 수 있음)
    roadQ.clear();
    visited = new boolean[yLength][xLength];

    // 출발 위치(레버)를 세팅
    roadQ.offer(leverPoint);
    visited[leverPoint.y][leverPoint.x] = true;

    // 출구 위치까지 탐색하고 만약 출구에 도착하지 못했다면 -1 리턴
    char targetE = 'E';
    Point exitPoint = goToPoint(roadQ, maps, visited, direction, xLength, yLength, targetE);
    if(exitPoint == null) return -1;

    return exitPoint.sec;
}

// 시작 지점 찾기
private int[] findStart(String[] maps){
    for(int y = 0; y < maps.length; y++){
        for(int x = 0; x < maps[0].length(); x++){
            if(maps[y].charAt(x) == 'S') return new int[] {x, y};
        }
    } return new int[] {0, 0};
}

// 시작지점(혹은 레버 위치)에서 타겟 지점(레버, 츌구)까지 최단거리 탐색을 위한 BFS
// 타겟 지점의 위치와 이동 시간을 포함하는 Point 클래스 반환
private Point goToPoint(Queue<Point> roadQ, String[] maps, boolean[][] visited, int[][] direction, int xLength, int yLength, char target){
    while(!roadQ.isEmpty()){
        Point road = roadQ.poll();
        int sec = road.sec;
        for(int[] dir : direction){
            int x = road.x + dir[0];
            int y = road.y + dir[1];
            if(x >= 0 && x < xLength && y >= 0 && y < yLength){
                if(visited[y][x] || maps[y].charAt(x) == 'X') continue;
                if(maps[y].charAt(x) == target) return new Point(x,y,sec+1);
                visited[y][x] = true;
                roadQ.offer(new Point(x,y,sec+1));
            }
        }
    } return null;
}

}


## 회고
- 풀이 후 조금 아쉬웠던 부분을 리팩토링 해보았다.
- `findStart` 메소드의 리턴타입을 Point로 변경하고 관련 부분을 수정하였다.
- `goToPoint` 메소드의 파라매터가 너무 구구절절 길어져서 이부분을 조금 고쳐보았다.
   - 탐색지의 상하좌우 방향을 나타낼 배열 direction은 어차피 해당 메소드 내에서만 사용되므로 메소드 안에서 지역변수 선언한다.
   - 이미 maps가 파라매터로 들어가므로 굳이 그 길이를 나타내는 xLength, yLength를 파라매터로 줄 필요는 없다. 다만 이미 선언한 변수를 메소드 안에서 다시 선언해야 하는게 조금 걸린다.
```java
import java.util.LinkedList;
import java.util.Queue;

class Solution {

    class Point{
        private final int x;
        private final int y;
        private final int sec;

        public Point(int x, int y, int sec) {
            this.x = x;
            this.y = y;
            this.sec = sec;
        }
    }

    public int solution(String[] maps) {

        // BFS 탐색을 위한 기본 세팅
        int xLength = maps[0].length();
        int yLength = maps.length;
        boolean[][] visited = new boolean[yLength][xLength];
        Queue<Point> roadQ = new LinkedList<>();

        // 시작 지점을 탐색하여 roadQ와 visited에 시작지점의 위치를 세팅
        Point startPoint = findStart(maps);
        roadQ.offer(startPoint);
        visited[startPoint.y][startPoint.x] = true;

        // 레버 위치까지 탐색하고 만약 레버에 도착하지 못했다면 -1 리턴
        char targetL = 'L';
        Point leverPoint = goToPoint(roadQ, maps, visited, targetL);
        if(leverPoint == null) return -1;

        // 새 탐색을 위해 roadQ에 저장된 값을 모두 제거
        // visited를 새로 초기화 (지나온 길을 다시 지나갈 수 있음)
        roadQ.clear();
        visited = new boolean[yLength][xLength];

        // 출발 위치(레버)를 세팅
        roadQ.offer(leverPoint);
        visited[leverPoint.y][leverPoint.x] = true;

        // 출구 위치까지 탐색하고 만약 출구에 도착하지 못했다면 -1 리턴
        char targetE = 'E';
        Point exitPoint = goToPoint(roadQ, maps, visited, targetE);
        if(exitPoint == null) return -1;

        return exitPoint.sec;
    }

    // 시작 지점 찾기
    private Point findStart(String[] maps){
        for(int y = 0; y < maps.length; y++){
            for(int x = 0; x < maps[0].length(); x++){
                if(maps[y].charAt(x) == 'S') return new Point(x,y,0);
            }
        } return null;
    }

    // 시작지점(혹은 레버 위치)에서 타겟 지점(레버, 츌구)까지 최단거리 탐색을 위한 BFS
    // 타겟 지점의 위치와 이동 시간을 포함하는 Point 클래스 반환
    private Point goToPoint(Queue<Point> roadQ, String[] maps, boolean[][] visited, char target){
        int[][] direction = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; // x,y 상하좌우 
        int xLength = maps[0].length();
        int yLength = maps.length;

        while(!roadQ.isEmpty()){
            Point road = roadQ.poll();
            int sec = road.sec;
            for(int[] dir : direction){
                int x = road.x + dir[0];
                int y = road.y + dir[1];
                if(x >= 0 && x < xLength && y >= 0 && y < yLength){
                    if(visited[y][x] || maps[y].charAt(x) == 'X') continue;
                    if(maps[y].charAt(x) == target) return new Point(x,y,sec+1);
                    visited[y][x] = true;
                    roadQ.offer(new Point(x,y,sec+1));
                }
            }
        } return null;
    }   
}