6월 28, 2024

[백준] 1600번 말이 되고픈 원숭이 문제 BFS로 풀어보기

1. 문제

www.acmicpc.net/problem/1600

문제의 입력, 출력, 더 자세한 instruction은 위 백준 링크에서 확인하고 오늘은 1600번 풀이법에 대해 알아보도록 하자.


2. BFS 개념

대표적으로 BFS를 사용할 수 있는 문제이다.

https://www.programmingstory.com/2024/02/dfs-bfs.html

BFS에 대해 익숙하지 않다면 위의 개념을 먼저 보고 오는 것을 추천한다.


3. 풀이

BFS에 관한 전형적인 문제인데 하나 변형된 부분이 있다. 바로 k번만 특수하게 움직일 수 있다는 것이다. 우리는 그동안 BFS를 풀 때 2차원 배열을 사용하여 행의 좌표, 열의 좌표를 사용하였다. 하지만 이 경우에는 k번만 특수하게 움직일 수 있으니 그동안 얼만큼 움직였는지를 별도로 기록할 필요가 있다. 

 

따라서 이번에는 3차원 배열을 만들어야 한다. 그리고 이동할 수 있는 방향도 총 12가지로 늘어난다. 예전에는 4가지가 대부분이었는데 이번에는 k번 이하로 대각선 방향도 움직일 수 있는 자유가 주어진다.

 

그래서 이번에는 

static final int[] dx = {0,0,1,-1,-2,-1,1,2,2,1,-1,-2};
static final int[] dy = {1,-1,0,0,1,2,2,1,-1,-2,-2,-1};
static final int[] used = {0,0,0,0,1,1,1,1,1,1,1,1};

이런식으로 static 변수들을 가져온다. dx와 dy모두 이동할 수 있는 방향을 뜻하고 used 라는 배열은 대각선으로 이동할 경우에 k번 중에 1번을 쓰는 것이므로 이런 식으로 하나의 배열을 더 준비했다.


처음에는 모든 d 배열을 -1로 초기화를 한 뒤에 만약 해당 d 배열의 값이 -1이라면 아직 방문하지 않았다는 뜻이므로 +1을 해준다. 이 문제에서는 인자가 세개가 필요하다는 것을 알 수 있을 것이다. 대각선 방향으로 몇 번 움직였는지의 회수도 queue에 함께 넣어주고 이것이 문제에서 입력받은 횟수보다 작거나 같은지를 확인해주어야 한다. 

 

이후 이 문제의 답은 d[n-1][m-1][k] 에 있다고 생각하면 잘못된 것이다. 문제에서 분명히 k 이하라고 했기 때문에 우리는 k의 값을 0부터 k까지 모두 다 검사하면서 최소값을 찾아야 하는 것이다. 


3. 코드

 

import java.util.*;

public class Main{
     static final int[] dx = {0,0,1,-1,-2,-1,1,2,2,1,-1,-2};
    static final int[] dy = {1,-1,0,0,1,2,2,1,-1,-2,-2,-1};
    static final int[] used = {0,0,0,0,1,1,1,1,1,1,1,1};
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int l = sc.nextInt();
        int m = sc.nextInt();
        int n = sc.nextInt();
        int[][] a = new int[n][m];
        for (int i=0; i<n; i++) {
            for (int j=0; j<m; j++) {
                a[i][j] = sc.nextInt();
            }
        }
        int[][][] d = new int[n][m][l+1];
        for (int i=0; i<n; i++) {
            for (int j=0; j<m; j++) {
                Arrays.fill(d[i][j],-1);
            }
        }
        Queue<Integer> q=new LinkedList<>();
        q.add(0); q.add(0); q.add(0);
        d[0][0][0]=0;
        while(!q.isEmpty()){
            int x=q.remove();
            int y=q.remove();
            int c=q.remove();
            for(int k=0; k<12; k++){
                int nx=x+dx[k];
                int ny=y+dy[k];
                int nc=c+used[k];
                if (nx>=0 && nx<n &&ny>=0 &&ny<m && nc<=l){
                    if (a[nx][ny]!=1){
                        if (d[nx][ny][nc]==-1){
                            d[nx][ny][nc]=d[x][y][c]+1;
                            q.add(nx);
                            q.add(ny);
                            q.add(nc);
                        }
                    }
                }
            }
        }
        int ans = -1;
        for (int i=0; i<=l; i++) {
            if (d[n-1][m-1][i] != -1){
                if (ans == -1 || ans > d[n-1][m-1][i]) {
                ans = d[n-1][m-1][i];
            }
            } 
            
        }
        System.out.print(ans);
    }
}

이렇게 변형문제가 있을 수 있으니 잘 연습해두자.


6월 28, 2024

[백준] 17141번 연구소2 문제 BFS로 풀어보기

1. 문제

 www.acmicpc.net/problem/17141


자세한 문제의 사항은 위의 링크를 클릭하여 백준 사이트에서 확인해보자.


2. 풀이

먼저 이 문제에서는 바이러스를 최대 m개 놓을 수 있기 때문에 어떤 위치에 바이러스 m개를 배치할지를 결정해주어야 한다. 그러기 위해서 먼저 문제에서 2라고 표시된 부분에 바이러스를 놓을 수 있다고 했기 때문에 가능한 바이러스의 위치를 ArrayList에다 넣어주도록 하겠다 (나는 virus라는 이름의 ArrayList를 만들어주었다). 그런 다음에 2라고 표시된 부분은 벽이 아니므로 자유롭게 움직일 수 있기에, 다시 0으로 바꾸어준다. 이러면 바이러스 자리인지 빈칸 자리인지 구분이 되지 않지만 우리는 ArrayList에다 넣어주었기 때문에 문제가 없다. 

 

다음으로 m개의 바이러스 자리를 결정하기 위해서 재귀함수를 사용해준다.

static void recur(int index, int cnt) {
        if (index == virus.size()) { 
            if (cnt == m) {//m개를 다 결정한 경우 
                bfs();
            }
        } else {
            int x = virus.get(index).x;
            int y = virus.get(index).y;
            a[x][y] = 3;
            recur(index+1, cnt+1); //해당 배열의 위치에 바이러스를 놓기로 결정한 경우
            a[x][y] = 0;
            recur(index+1, cnt);//해당 배열의 위치에 바이러스를 놓지 않기로 결정한 경우
        }
    }

위와 같은 재귀함수를 만들어주었다. 이런 식으로 재귀함수로 바이러스 m개의 위치를 결정해준다. 만약 마지막 바이러스의 위치에 도착했는데 cnt가 m개라면 m개의 바이러스 위치를 모두 결정해준 것이니 bfs 함수를 호출해주면 된다. 

 

여기서 바이러스의 진짜 위치를 표시해주기 위해서 만약 해당 좌표에 바이러스가 놓인 것이라면 그 값을 3으로 바꾸어주는 추가 작업도 실시해준다.


이제 그러면 bfs 함수가 어떻게 구성되어있는지 보도록 하겠다. (variable이 헷갈린다면 먼저 이 글의 가장 하단의 전체 코드를 보고 오는 것이 더 이해가 빠를 수 있다 )

static void bfs() {
        for (int i=0; i<n; i++) {
            for(int j=0; j<n; j++){
                d[i][j]=-1;
            }
        }
        Queue<Pair> q = new LinkedList<>();
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                if (a[i][j] == 3) {
                    q.add(new Pair(i,j));
                    d[i][j] = 0;
                }
            }
        }
        while (!q.isEmpty()) {
            Pair p = q.remove();
            int x = p.x;
            int y = p.y;
            for (int k=0; k<4; k++) {
                int nx = x+dx[k];
                int ny = y+dy[k];
                if (0 <= nx && nx < n && 0 <= ny && ny < n) {
                    if (a[nx][ny] != 1 && d[nx][ny] == -1) {
                        d[nx][ny] = d[x][y] + 1;
                        q.add(new Pair(nx, ny));
                    }
                }
            }
        }
        int cur = 0;
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                if (a[i][j] != 1) {
                    if (d[i][j] == -1) return;
                    if (cur < d[i][j]) cur = d[i][j];
                }
            }
        }
        if (ans == -1 || ans > cur) {
            ans = cur;
        }
    }

먼저 d라는 배열을 모두 -1로 초기화해준다. 그런 다음 바이러스의 위치를 모두 queue에다가 넣어준다. 만약 벽이 아니고 방문하지 않은 칸이라면 이를 다시 queue에 넣어두는 등 일반적으로 우리가 BFS를 구할 때 하는 행동을 해준다. 

 

그런 다음에 d의 최댓값을 찾는 것이 사실상 이 문제에서 구하고자 하는 값이다. 따라서 모든 배열의 칸을 돌면서 현재의 ans가 iteration에서 돌아서 나온 최댓값보다 작다면 이를 새로 update시키는 과정을 거친다. 

 

이렇게 모든 과정을 거치게 되면 우리는 문제에서 구하고자 하는 값을 얻을 수 있다. 아래는 Java로 구현한 전체 코드이다.


3. 코드



import java.util.*;
class Pair {
    int x, y;
    Pair(int x, int y) {
        this.x = x;
        this.y = y;
    }
}
public class Main {
    static int[][] a;
    static int[][] d;
    static final int[] dx = {0,0,1,-1};
    static final int[] dy = {1,-1,0,0};
    static int n, m;
    static ArrayList<Pair> virus = new ArrayList<>();
    static int ans = -1;
    static void bfs() {
        for (int i=0; i<n; i++) {
            for(int j=0; j<n; j++){
                d[i][j]=-1;
            }
        }
        Queue<Pair> q = new LinkedList<>();
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                if (a[i][j] == 3) {
                    q.add(new Pair(i,j));
                    d[i][j] = 0;
                }
            }
        }
        while (!q.isEmpty()) {
            Pair p = q.remove();
            int x = p.x;
            int y = p.y;
            for (int k=0; k<4; k++) {
                int nx = x+dx[k];
                int ny = y+dy[k];
                if (0 <= nx && nx < n && 0 <= ny && ny < n) {
                    if (a[nx][ny] != 1 && d[nx][ny] == -1) {
                        d[nx][ny] = d[x][y] + 1;
                        q.add(new Pair(nx, ny));
                    }
                }
            }
        }
        int cur = 0;
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                if (a[i][j] != 1) {
                    if (d[i][j] == -1) return;
                    if (cur < d[i][j]) cur = d[i][j];
                }
            }
        }
        if (ans == -1 || ans > cur) {
            ans = cur;
        }
    }
    static void recur(int index, int cnt) {
        if (index == virus.size()) {
            if (cnt == m) {
                bfs();
            }
        } else {
            int x = virus.get(index).x;
            int y = virus.get(index).y;
            a[x][y] = 3;
            recur(index+1, cnt+1);
            a[x][y] = 0;
            recur(index+1, cnt);
        }
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        a = new int[n][n];
        d = new int[n][n];
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                a[i][j] = sc.nextInt();
                if (a[i][j] == 2) {
                    a[i][j] = 0;
                    virus.add(new Pair(i,j));
                }
            }
        }
        recur(0,0);
        System.out.println(ans);
    }
}

5월 23, 2024

[백준] 2251번 물통문제 BFS로 풀어보기

1. 문제

1) 링크

www.acmicpc.net/problem/2251

더 자세한 문제의 제한은 위의 링크에 들어가서 확인해보자

 


2. 풀이

이 물통 문제는 처음에 어떻게 풀까 고민을 하다가 위 풀이를 보고 BFS로 풀면 된다는 것을 알게 되었다.

 


물통 초기상태

물통의 총합이 변하지 않는다는 것이 문제에서 가장 중요한 열쇠이다. 그러면 물통 자체는 3개가 있지만 두개만 우리는 미지수로 두고 나머지 하나는 총합에서 빼는 식으로 구상하면 된다. 그리고 심지어 전체 합은 문제 시작 때 주어져있기 때문에 (2번 물통 양이 처음에는 sum이다) 매우 편하게 구할 수 있다.

 

그런 다음에 처음 물통 0과 물통 1은 0,0으로 시작하니 이들을 queue에 넣어주고 여기서부터 물통에 물을 옮겨담을 수 있는 모든 조합을 시도해보는 것이다. 

 

순열로 해도 되지만 이렇게 6개밖에 되지 않는 것은 그냥 배열로 from, to 해서 여섯가지를 모두 시도해보는 것이 간단하다. 

그리고 이 문제에서 한 물통이 비거나, 다른 한 물통이 가득 찰 때까지 물을 부을 수 있다고 했으므로 우선 다른 물통에 물을 다 부어놓고 이것이 용량을 초과하게 되면 다시 원래 물통에 넘친 만큼 부어준다는 식으로 구현을 하면 될 것 같다. 

그러면 비록 넘칠 때까지 부었어도 넘친 부분을 원상복구시켜주면서 물통이 가득찰 때까지만 부은 것이 되기 때문이다. 

 

그리고 제한이 200밖에 되지 않기 때문에 200까지 배열의 용량을 넉넉히 만들어 구해주면 된다.

 

Pair라는 class를 만들어도 되지만 나는 그것이 번거로워서 그냥 0번째 물통 값 넣어주고 1번째 물통 값 넣어주고 이런 식으로 구현했다. 전혀 문제 없는 방식이고 대신 queue에서 뺄 때도 두개를 다 빼주어야 한다.

 

3. 코드 

import java.util.*;

public class Main{
    final static int to[]={0,0,1,1,2,2};
    final static int from[]={1,2,0,2,0,1};
    public static void main(String[] arg){
        Scanner sc=new Scanner(System.in);
        int water[]=new int [3];
        for(int i=0; i<3; i++){
            water[i]=sc.nextInt();
        }
        int sum=water[2];
        boolean check[][]=new boolean[201][201];
        boolean ans[]=new boolean[201];
        Queue <Integer> q= new LinkedList<>();
        q.add(0);
        q.add(0);
        check[0][0]=true;
        ans[water[2]]=true;
        while(!q.isEmpty()){
            int cur[]=new int [3];
            cur[0]=q.remove();
            cur[1]=q.remove();
            cur[2]=sum-cur[0]-cur[1];
            for(int k=0; k<6; k++){
                int next[]={cur[0], cur[1], cur[2]};
                next[to[k]]+=next[from[k]];
                next[from[k]]=0;
                if (next[to[k]]>=water[to[k]]){
                    next[from[k]]=next[to[k]]-water[to[k]];
                    next[to[k]]=water[to[k]];
                }
                if (!check[next[0]][next[1]]){
                    check[next[0]][next[1]]=true;
                    q.add(next[0]);
                    q.add(next[1]);
                    if (next[0]==0){
                        ans[next[2]]=true;
                    }
                }
            }
        }
        for(int i=0; i<=water[2]; i++){
            if (ans[i]){
                  System.out.print(i + " ");
            }
        }
        System.out.println();
    }
}

여기서 check라는 배열은 0번째 물통 값, 1번째 물통 값의 조합이 예전에 나온 조합임을 확인하는 boolean 배열이고 ans 배열은 문제의 조건을 만족시키는지 확인하는 배열이다. 

 

이런식으로 구하면 문제에서 요구하는 조건은 ans 배열을 0부터 2번 물통 최대 값(sum)까지 따라가면서 true인 값을 적어주면 된다.


4월 07, 2024

[백준] 17142번: 연구소 3문제 BFS로 풀기

1. 문제 

www.acmicpc.net/problem/17142

문제는 위 링크에 들어가서 볼 수 있다.


2. 풀이

이 문제는 먼저 바이러스를 놓을 수 있는 칸에서부터 시작해 바이러스를 m개 놓아야 한다. 그것을 우리는 재귀함수 move로 구현하겠다. 항상 그랬듯이, index와 count를 parameter로 받고 정해진 지점에 도달했을 때, 우리는 bfs를 돌리면서 검사를 하는 것이다. 

 

move의 함수는 아래와 같다.

    static ArrayList<Integer> xs = new ArrayList<>();
    static ArrayList<Integer> ys = new ArrayList<>();
 public static void move(int index,int count){
        if (index==xs.size()){
            if (count==m){
                bfs();
            }
        }else{
            int x = xs.get(index);
            int y = ys.get(index);
            a[x][y] = 3; //바이러스 놓기
            move(index+1, count+1);
            a[x][y] = 2; 
            move(index+1, count);
        }
    }

여기서 xs는 가능한 바이러스의 위치를 알려주는 ArrayList이다. 문제에서 입력받을 때 바이러스를 놓을 수 있는 위치의 행을 xs에, 열을 ys에 저장한다. 그러면 index의 위치가 xs의 size와 같다는 것은 전체 바이러스를 다 검사했다는 것이고, 그 중에서 count의 값이 m이라는 것은 문제의 조건과 일치한다는 뜻이므로 그 때 bfs 함수를 호출해 bfs 검사를 해주면 된다.


 static void bfs(){
        int d[][]=new int[n][n];
        Queue<Integer>q=new LinkedList<>();
         for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                d[i][j] = -1;
                if (a[i][j] == 3) {
                    q.add(i);q.add(j);
                    d[i][j] = 0;
                }
            }
        }
        while(!q.isEmpty()){
            int x=q.remove();
            int y=q.remove();
            for(int k=0; k<4; k++){
                int nx=x+dx[k];
                int ny=y+dy[k];
                if (0<=nx && nx<n && 0<=ny && ny<n){
                    if (a[nx][ny] != 1 && d[nx][ny] == -1) {
                        d[nx][ny] = d[x][y] + 1;
                        q.add(nx); q.add(ny);
                    }
                }
            }
        }
        int val = 0;
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                if (a[i][j] == 0) { //이미 빈칸이었던 애들만 검사
                    if (d[i][j] == -1) return;
                    if (val < d[i][j]) val = d[i][j];
                }
            }
        }
        if (ans == -1 || ans > val) {
            ans = val;
        }
    }

위 코드는 bfs 함수만 적어본 것이다. 일반적인 BFS 코드와 유사하나 다른 점은 문제에서 빈칸이 바이러스에 감염되는 시간을 검사하라고 했으므로 원래 칸이 빈칸이었는지를 하나 더 검사해주어야 한다. 

 


3. 풀이

전체 JAVA 코드를 살펴보면 아래와 같다.

import java.util.*;

public class Main{
    static int[][] a;
    static int[][] d;
    static int[] dx = {0,0,1,-1};
    static int[] dy = {1,-1,0,0};
    static int n, m;
    static ArrayList<Integer> xs = new ArrayList<>();;
    static ArrayList<Integer> ys = new ArrayList<>();;
    static int ans = -1;
    static void bfs(){
        d=new int[n][n];
        Queue<Integer>q=new LinkedList<>();
         for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                d[i][j] = -1;
                if (a[i][j] == 3) {
                    q.add(i);q.add(j);
                    d[i][j] = 0;
                }
            }
        }
        while(!q.isEmpty()){
            int x=q.remove();
            int y=q.remove();
            for(int k=0; k<4; k++){
                int nx=x+dx[k];
                int ny=y+dy[k];
                if (0<=nx && nx<n && 0<=ny && ny<n){
                    if (a[nx][ny] != 1 && d[nx][ny] == -1) {
                        d[nx][ny] = d[x][y] + 1;
                        q.add(nx); q.add(ny);
                    }
                }
            }
        }
        int val = 0;
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                if (a[i][j] == 0) { //이미 빈칸이었던 애들만 검사
                    if (d[i][j] == -1) return; //불가능
                    if (val < d[i][j]) {val = d[i][j];}
                }
            }
        }
        if (ans == -1 || ans > val) {
            ans = val;
        }
    }
    public static void move(int index,int count){
        if (index==xs.size()){
            if (count==m){
                bfs();
            }
        }else{
            int x = xs.get(index);
            int y = ys.get(index);
            a[x][y] = 3; //바이러스 놓기
            move(index+1, count+1);
            a[x][y] = 2; 
            move(index+1, count);
        }
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        a = new int[n][n];
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                a[i][j] = sc.nextInt();
                if (a[i][j] == 2) {
                    xs.add(i);
                    ys.add(j);
                }
            }
        }
        move(0,0);
        System.out.println(ans);
    }
}

3월 14, 2024

[백준] 16236번 아기상어 문제 BFS로 풀어보기

1. 문제

1) 링크

www.acmicpc.net/problem/16236

2) 문제

N×N 크기의 공간에 물고기 M마리와 아기 상어 1마리가 있다. 공간은 1×1 크기의 정사각형 칸으로 나누어져 있다. 한 칸에는 물고기가 최대 1마리 존재한다.

아기 상어와 물고기는 모두 크기를 가지고 있고, 이 크기는 자연수이다. 가장 처음에 아기 상어의 크기는 2이고, 아기 상어는 1초에 상하좌우로 인접한 한 칸씩 이동한다.

아기 상어는 자신의 크기보다 큰 물고기가 있는 칸은 지나갈 수 없고, 나머지 칸은 모두 지나갈 수 있다. 아기 상어는 자신의 크기보다 작은 물고기만 먹을 수 있다. 따라서, 크기가 같은 물고기는 먹을 수 없지만, 그 물고기가 있는 칸은 지나갈 수 있다.

아기 상어가 어디로 이동할지 결정하는 방법은 아래와 같다.

  • 더 이상 먹을 수 있는 물고기가 공간에 없다면 아기 상어는 엄마 상어에게 도움을 요청한다.
  • 먹을 수 있는 물고기가 1마리라면, 그 물고기를 먹으러 간다.
  • 먹을 수 있는 물고기가 1마리보다 많다면, 거리가 가장 가까운 물고기를 먹으러 간다.
    • 거리는 아기 상어가 있는 칸에서 물고기가 있는 칸으로 이동할 때, 지나야하는 칸의 개수의 최솟값이다.
    • 거리가 가까운 물고기가 많다면, 가장 위에 있는 물고기, 그러한 물고기가 여러마리라면, 가장 왼쪽에 있는 물고기를 먹는다.

아기 상어의 이동은 1초 걸리고, 물고기를 먹는데 걸리는 시간은 없다고 가정한다. 즉, 아기 상어가 먹을 수 있는 물고기가 있는 칸으로 이동했다면, 이동과 동시에 물고기를 먹는다. 물고기를 먹으면, 그 칸은 빈 칸이 된다.

아기 상어는 자신의 크기와 같은 수의 물고기를 먹을 때 마다 크기가 1 증가한다. 예를 들어, 크기가 2인 아기 상어는 물고기를 2마리 먹으면 크기가 3이 된다.

공간의 상태가 주어졌을 때, 아기 상어가 몇 초 동안 엄마 상어에게 도움을 요청하지 않고 물고기를 잡아먹을 수 있는지 구하는 프로그램을 작성하시오.

3) 입력

첫째 줄에 공간의 크기 N(2 ≤ N ≤ 20)이 주어진다.

둘째 줄부터 N개의 줄에 공간의 상태가 주어진다. 공간의 상태는 0, 1, 2, 3, 4, 5, 6, 9로 이루어져 있고, 아래와 같은 의미를 가진다.

  • 0: 빈 칸
  • 1, 2, 3, 4, 5, 6: 칸에 있는 물고기의 크기
  • 9: 아기 상어의 위치

아기 상어는 공간에 한 마리 있다.

4) 출력

첫째 줄에 아기 상어가 엄마 상어에게 도움을 요청하지 않고 물고기를 잡아먹을 수 있는 시간을 출력한다.

 

더 자세한 문제의 제한을 보기 위해서는 위의 링크에 들어가보자.


2. 풀이

이 문제는 BFS를 연속적으로 사용해야 한다는 점이 특이한 점이다. 왜냐하면 여러 번 이동을 해야 하고 먹이가 없을 때까지의 시간을 구해야 하는 문제이기 때문이다. 그래서 while 문을 사용해서 bfs 함수를 지속적으로 호출해주는 식으로 문제를 구현했다. 

 

그리고 또한 BFS 함수를 호출할 때마다 정렬을 해야 하는 과정이 추가된다. 왜냐하면 문제에서 먹을 수 있는 물고기가 2개 이상일 때에는 조건을 만족시키는 가장 가까운 물고기를 먼저 먹어야 하기 때문이다. 따라서 BFS를 호출해준 뒤 먹을 수 있는 물고기를 모두 ArrayList에 넣어주고 그 뒤에 정렬을 해주는 과정을 거쳐서 가장 가까이에 있는 물고기를 먹어준다. 그러면 그 위치를 다시 시작점으로 다시 bfs 함수를 호출해주는 것이다. 

 

BFS 함수를 구현한 코드 부분은 아래와 같다.

    static Pair bfs(int x, int y){
        ArrayList<Pair> fish=new ArrayList<>(); //먹은 것 저장
        int [][]d=new int[n][n];
        Queue<Integer> q=new LinkedList<>();
        for(int i=0; i<n; i++){
            Arrays.fill(d[i],-1);
        }
        q.add(x); 
        q.add(y);
        d[x][y]=0;
        while(!q.isEmpty()){
            int ccx=q.remove();
            int ccy=q.remove();
            for(int k=0; k<4; k++){
                int nx=ccx+dx[k];
                int ny=ccy+dy[k];
                if (nx>=0 && ny>=0 && nx<n && ny<n &&d[nx][ny]==-1){
                    boolean go=false; //지나갈 수 있는지
                    boolean eat=false; //먹을 수 있는지
                    if (a[nx][ny]==0){
                        go=true; //지나갈 수 있음
                    }
                    else if (a[nx][ny]<size){
                        eat=true;
                        go=true;
                    }
                    else if (a[nx][ny]==size){
                        go=true;
                    }
                    if (go){
                        q.add(nx);
                        q.add(ny);
                        d[nx][ny]=d[ccx][ccy]+1;
                        if (eat){
                            fish.add(new Pair(d[nx][ny], nx, ny));
                        }
                    }
                }
            }
        }
        if (fish.size()==0){ //먹은 게 없을 경우
            return null;
        }
        Pair close=fish.get(0);//정렬
        for(Pair p: fish){
            if (p.dist < close.dist){
                close=p;
            }
            else if (close.dist==p.dist && p.x<close.x){
                close=p;
            }
            else if (close.dist==p.dist && p.x==close.x &&p.y<close.y){
                close=p;
            }
        }
        a[close.x][close.y]=0;
        ans+=close.dist;
        count++;
        if (size==count){
            size++;
            count=0;
        }
        return close;
        
    }

여기서 가능한 물고기를 모두 다 fish ArrayList에 넣어주었고 이후 가장 처음 먹을 물고기를 찾아주었다. 그런 뒤에는 해당 칸의 물고기를 먹었으므로 숫자를 0으로 바꾸어놓고 ans(문제에서 구하려는 초)에다가 distance를 더해주었다. count가 size가 되면 size가 늘어난다는 문제의 조건을 처리해주었다. 

 

그리고 만약 물고기 fish ArrayList의 size가 0이라는 것은 먹을 수 있는 물고기가 더 이상 없어서 엄마상어에게 동무을 요청해야 할 경우를 의미하기 때문에 문제의 종료조건에 해당한다는 것을 알 수 있다. 즉 이 경우에는 null을 return하고 main 함수에서 while문을 종료하게 되는 조건이 된다.


3. 코드

이 모든 것을 코드화한 것은 아래와 같다.

import java.util.*;
class Pair{
    int dist; int x; int y;
    Pair(int dist, int x, int y){
        this.dist=dist;
        this.x=x;
        this.y=y;
    }
}
public class Main{
    static final int[] dx = {0,0,1,-1};
    static final int[] dy = {1,-1,0,0};
    static int ans=0;
    static int size=2;
    static int count=0;
    static int a[][];
    static int n;
    static Pair bfs(int x, int y){
        ArrayList<Pair> fish=new ArrayList<>(); //먹은 것 저장
        int [][]d=new int[n][n];
        Queue<Integer> q=new LinkedList<>();
        for(int i=0; i<n; i++){
            Arrays.fill(d[i],-1);
        }
        q.add(x); 
        q.add(y);
        d[x][y]=0;
        while(!q.isEmpty()){
            int ccx=q.remove();
            int ccy=q.remove();
            for(int k=0; k<4; k++){
                int nx=ccx+dx[k];
                int ny=ccy+dy[k];
                if (nx>=0 && ny>=0 && nx<n && ny<n &&d[nx][ny]==-1){
                    boolean go=false; //지나갈 수 있는지
                    boolean eat=false; //먹을 수 있는지
                    if (a[nx][ny]==0){
                        go=true; //지나갈 수 있음
                    }
                    else if (a[nx][ny]<size){
                        eat=true;
                        go=true;
                    }
                    else if (a[nx][ny]==size){
                        go=true;
                    }
                    if (go){
                        q.add(nx);
                        q.add(ny);
                        d[nx][ny]=d[ccx][ccy]+1;
                        if (eat){
                            fish.add(new Pair(d[nx][ny], nx, ny));
                        }
                    }
                }
            }
        }
        if (fish.size()==0){ //먹은 게 없을 경우
            return null;
        }
        Pair close=fish.get(0);//정렬
        for(Pair p: fish){
            if (p.dist < close.dist){
                close=p;
            }
            else if (close.dist==p.dist && p.x<close.x){
                close=p;
            }
            else if (close.dist==p.dist && p.x==close.x &&p.y<close.y){
                close=p;
            }
        }
        a[close.x][close.y]=0;
        ans+=close.dist;
        count++;
        if (size==count){
            size++;
            count=0;
        }
        return close;
        
    }
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
         n=sc.nextInt();
         a=new int[n][n];
        int cx=0; int cy=0; //현재 아기상어 위치
        for(int i=0; i<n; i++){
            for(int j=0; j<n; j++){
                a[i][j]=sc.nextInt();
                if (a[i][j]==9){
                    cx=i;
                    cy=j; 
                    a[i][j]=0;
                }
            }
        }
        while(true){
            Pair p=bfs(cx, cy);
            if (p==null){
                break;
            }
            cx=p.x;
            cy=p.y;
        }
        System.out.println(ans);
    }
}

Pair 클래스에 dist, x, y가 있는 것은 문제에서 가장 먼저 먹을 물고기를 결정할 때 거리, 행, 열의 정보가 모두 필요하기 때문이다. 


2월 26, 2024

[백준] 1260번 DFS와 BFS 구현해보기

1. 문제

1) 링크

www.acmicpc.net/problem/1260

2) 문제

그래프를 DFS로 탐색한 결과와 BFS로 탐색한 결과를 출력하는 프로그램을 작성하시오. 단, 방문할 수 있는 정점이 여러 개인 경우에는 정점 번호가 작은 것을 먼저 방문하고, 더 이상 방문할 수 있는 점이 없는 경우 종료한다. 정점 번호는 1번부터 N번까지이다.

3) 입력

첫째 줄에 정점의 개수 N(1 ≤ N ≤ 1,000), 간선의 개수 M(1 ≤ M ≤ 10,000), 탐색을 시작할 정점의 번호 V가 주어진다. 다음 M개의 줄에는 간선이 연결하는 두 정점의 번호가 주어진다. 어떤 두 정점 사이에 여러 개의 간선이 있을 수 있다. 입력으로 주어지는 간선은 양방향이다.

4) 출력

첫째 줄에 DFS를 수행한 결과를, 그 다음 줄에는 BFS를 수행한 결과를 출력한다. V부터 방문된 점을 순서대로 출력하면 된다.
 
위와 같이 DFS와 BFS 구현 문제를 풀어 볼 것이다. 더 자세한 문제의 제한사항이 궁금하면 위 링크를 클릭하여 들어가보자.

2. DFS BFS 

문제를 풀기 전에 아직 DFS와 BFS가 익숙하지 않은 사람들은 
https://www.programmingstory.com/2024/02/dfs-bfs.html
위 포스트에서 개념과 구현방식을 다루었으니 살펴보자.
 

3. 풀이 

먼저 DFS (깊이 우선 탐색)부터 코드를 작성해보자

public static void dfs(int x){
if (c[x]) return;
c[x]=true;
System.out.print(x+" ");
for(int y: a[x]){
if (!c[y]){
dfs(y);
}
}
}
인접리스트의 방법으로 푼 것이다. 가능한 인접리스트 중에서 이미 사용되지 않은 것이 있다면 재귀를 활용해서 dfs 함수를 한 번 더 호출해준다.
 
다음으로 BFS( 너비 우선 탐색) 코드를 살펴보자

public static void bfs(int start){
Queue <Integer> q=new LinkedList<Integer>();
q.add(start);
c[start]=true;
while(!q.isEmpty()){
int x=q.remove();
System.out.print(x+" ");
for(int y: a[x]){
if (c[y]==false){
q.add(y);
c[y]=true;
}
}
}
}

이것은 Queue라는 자료구조를 활용한다. 
queue에 인접한 것들을 넣고 하나씩 remove를 하면서 queue에 들어있는 것이 없어질 때까지 탐색을 하는 것이다. 

전체 코드를 살펴보면 다음과 같다.

import java.util.*;
public class Main{
static ArrayList<Integer>[] a;
static boolean[] c;
public static void dfs(int x){
if (c[x]) return;
c[x]=true;
System.out.print(x+" ");
for(int y: a[x]){
if (!c[y]){
dfs(y);
}
}
}
public static void bfs(int start){
Queue <Integer> q=new LinkedList<Integer>();
q.add(start);
c[start]=true;
while(!q.isEmpty()){
int x=q.remove();
System.out.print(x+" ");
for(int y: a[x]){
if (c[y]==false){
q.add(y);
c[y]=true;
}
}
}
}
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int m=sc.nextInt();
int start=sc.nextInt();
a=(ArrayList<Integer>[] )new ArrayList[n+1];
for(int i=1; i<=n;i++){
a[i]=new ArrayList<Integer>();
}
for(int i=0; i<m;i++){
int from=sc.nextInt();
int to=sc.nextInt();
a[from].add(to);
a[to].add(from);
}
for(int i=1; i<=n; i++){
Collections.sort(a[i]);
}
c=new boolean[n+1];
dfs(start);
System.out.println();
c=new boolean[n+1];
bfs(start);
System.out.println();
}
}

인접리스트를 구현할 ArrayList들의 배열 a와 해당 숫자가 사용되었는지를 판단하는 boolean c 배열이 필요하다.
dfs를 한번 시작한 이후에는 c=new boolean[n+1]을 한번 해줌으로써 모든 것을 초기화해주는 작업이 필요하다. 

2월 25, 2024

[그래프의 탐색] DFS(깊이 우선 탐색), BFS(너비 우선탐색) 알아보기

1. 그래프 탐색의 필요성

굉장히 인기있는 그래프의 탐색이라는 주제,
우리는 왜 공부해야 하는 것일까?
목적은 임의의 정점에서 시작하여 연결되어 있는 모든 정점을 1번씩 방문하기 위해서이다. 
 

2. 그래프 탐색의 종류

그래프의 탐색에는 크게 두 가지 방법이 있다.
DFS (Depth First Search: 깊이 우선 탐색)BFS(Breadth First Search: 너비 우선 탐색) 이다.
 
각각의 방법을 하나씩 알아보자.
 

1) DFS (Depth First Search) : 깊이 우선 탐색

깊이 우선 탐색은 갈 수 있는 만큼 최대한 많이 가고, 갈 수 없으면 이전의 정점으로 돌아가는 방식의 알고리즘이다.
깊이 우선탐색은 stack을 사용한다. 
 

우선 탐색 Flow 

예를 들어 아래와 같은 그래프를 깊이 우선 탐색으로 탐색해보자


[1]  현재 정점: 1 

    해당 정점을 탐색한 후 check[i]을 true로 만들어준다. (stack에 집어넣는 순간)
 
i 1 2 3 4 5
check[i] true false false false false
 
현재 스택 1        
 
[2] 1을 탐색했으니 이제 1와 인접해있는 2와 3 중 하나를 탐색해본다.

i 1 2 3 4 5
check[i] true true false false false
 
현재 스택 1      
 
[3] 2를 탐색했으니 2와 인접해있는 것중 아직 check가 false인 4를 탐색해보자

i 1 2 3 4 5
check[i] true true false true false
 
현재 스택 1 2 4    
 
[4] 다음으로 4와 인접해있고 아직 check[i]가 false인 5를 탐색해보자.
 
i 1 2 3 4 5
check[i] true true false true true
 
현재 스택 1 2 4 5  
 
[5] 그 다음으로는 5에서 갈 수 있는 것이 없기 때문에 stack에서 5를 pop해주고 4로 돌아간다.

   5를 pop하더라도 check[5]는 false로 바뀌는 것이 아니다.
 
i 1 2 3 4 5
check[i] true true false true true
 
현재 스택 1 2 4    
 
[6] 4에서는 인접한 것 중에서 check 가 false인 3을 방문할 수 있기 때문에 3을 탐색한다.
 
i 1 2 3 4 5
check[i] true true true true true
 
현재 스택 1 2 4 3  
 
[7] 이제 더 이상 방문할 수 있는 것이 없기 때문에 스택에서 하나씩 pop해준다. 
 
현재 스택 1 2 4    
 
현재 스택 1 2      
 
현재 스택 1        
 
현재 스택 1        
 
[8] 스택에 아무것도 없는 상태가 되면 탐색이 끝난 것이다. 
 
그렇다면 깊이 우선 탐색을 코드로 구현하면 어떻게 구현할 수 있을까? 
stack을 명시적으로 사용하지 않고 재귀함수 호출로 구현할 수 있다.
전에 인접행렬과 인접리스트를 사용하여서 그래프를 구현할 수 있다고 했는데 깊이 우선탐색도 두가지를 사용하여 모두 구현할 수 있다.
 

1) 인접행렬을 사용하여 깊이 우선탐색 구현하기

  public static void dfs(int x) {
if (c[x]) {
return;
}
c[x] = true;
System.out.print(x + " ");
for (int i=1; i<=n; i++) {
if (c[i] == false && a[x][i]==1) {
dfs(i);
}
}
}
해당 행렬 (a[x][i])가 1이라는 것은 인접해 있다는 뜻이고 check[i]가 false라는 것은 아직 탐색되지 않았다는 뜻이기 때문이다.
이 경우 시간복잡도는 O(V^2) 라고 할 수 있다 (V는 정점의 개수) 
모든 정점을 돌면서 확인하고 총 V번 호출되기 때문이다.
 

2) 인접리스트를 사용하여 깊이 우선탐색 구현하기

  public static void dfs(int x) {
if (c[x]) {
return;
}
c[x] = true;
System.out.print(x + " ");
for (int y : a[x]) {
if (c[y] == false) {
dfs(y);
}
}
}
이 경우 a[x] 즉 리스트에 저장되어 있는 인접한 항들을 하나씩 돌면서 check가 false일 경우 재귀로 함수를 다시 불러 구하는 것이다.
이 경우 시간복잡도는 O(|V|+|E|)라고 할 수 있다. 여기서 E는 간선의 개수이다. 모든 정점을 한 번씩 방문하고 간선 E개에 대해서 확인을 해주기 때문이다. 
대체로 V<=E이기 때문에 시간복잡도는 O(E)라고 할 수 있다.
 

 

2. BFS (Breadth First Search) : 너비 우선 탐색

 
깊이 우선탐색에서 스택을 사용했다면, 너비 우선탐색에서는 큐를 사용한다.
큐를 이용해서 지금 위치에서 갈 수 있는 것을 모두 큐에 넣는 방식으로 탐색이 진행된다. 
 
이것도 위의 깊이 우선탐색처럼 같은 그림을 사용해 큐로 생각해보자


[1] 1부터 탐색을 시작해보자
 
i 1 2 3 4 5
check[i] true false false false false
 
현재 queue 1        
 
[2] 1이 인접할 수 있는 2와 3을 queue에 넣어주고 해당 check를 true로 바꾸어준다
 
i 1 2 3 4 5
check[i] true true true false false
 
현재 queue 1 2 3    
 
[3] 1에 인접한 것들을 queue에 넣었으니 1을 pop 해준다.
 
현재 queue 2 3      
 
[4] 다음으로 2와 인접해 있는 것들중 해당 check 배열이 false인 것들을 queue에 넣어주고 true로 바꾸어준다.
 
i 1 2 3 4 5
check[i] true ture true true true
 
현재 queue 2 3 4 5  
 
[5] 다음으로 2를 pop해준다.
 
현재 queue 3 4 5    
 
[6] 다음 3과 인접해있으면서 해당 check가 false인 것들을 queue에 넣어준다. 여기부터는 이제 모든 check가 true이게 되므로 queue에 넣어줄 것들이 없다. 즉, 하나씩 pop을 해주면 되는 것이다.
 
현재 queue 4 5      
 
현재 queue 5        
 
현재 queue          
 
queue는 이렇게 다 빌 때까지 pop을 해주면 된다. 
 

너비 우선탐색 또한 인접행렬과 인접그래프를 사용하여 모두 다 구현할 수 있다.
 

1) 인접행렬을 사용하여 너비 우선탐색 구현하기

 public static void bfs(int start) {
Queue<Integer> q = new LinkedList<Integer>();
q.add(start);
c[start] = true;
while (!q.isEmpty()) {
int x = q.remove();
System.out.print(x + " ");
for(int i=1; i<=n; i++){
if (c[i] == false && a[x][i]==1) {
c[i] = true;
q.add(i);
}
}
}
}
이것은 위에 DFS에서 본 것처럼 시간복잡도가 O(V^2)라는 것을 알 수 있음. 모든 정점마다 다른 모든 정점을 구해보기 때문
 

2) 인접리스트를 사용하여 너비 우선탐색 구현하기

 public static void bfs(int start) {
Queue<Integer> q = new LinkedList<Integer>();
q.add(start);
c[start] = true;
while (!q.isEmpty()) {
int x = q.remove();
System.out.print(x + " ");
for (int y : a[x]) {
if (c[y] == false) {
c[y] = true;
q.add(y);
}
}
}
}
이것 또한 위의 DFS와 마찬가지로 시간복잡도는 O(V+E)가 나온다는 것을 알 수 있다.