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);
    }
}

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


3월 05, 2024

[백준] 2146번 다리만들기 BFS로 빠르게 풀어보기

1. 문제

1) 링크

www.acmicpc.net/problem/2146

2) 문제

여러 섬으로 이루어진 나라가 있다. 이 나라의 대통령은 섬을 잇는 다리를 만들겠다는 공약으로 인기몰이를 해 당선될 수 있었다. 하지만 막상 대통령에 취임하자, 다리를 놓는다는 것이 아깝다는 생각을 하게 되었다. 그래서 그는, 생색내는 식으로 한 섬과 다른 섬을 잇는 다리 하나만을 만들기로 하였고, 그 또한 다리를 가장 짧게 하여 돈을 아끼려 하였다.

이 나라는 N×N크기의 이차원 평면상에 존재한다. 이 나라는 여러 섬으로 이루어져 있으며, 섬이란 동서남북으로 육지가 붙어있는 덩어리를 말한다.

지도가 주어질 때, 가장 짧은 다리 하나를 놓아 두 대륙을 연결하는 방법을 찾으시오.

3) 입력

첫 줄에는 지도의 크기 N(100이하의 자연수)가 주어진다. 그 다음 N줄에는 N개의 숫자가 빈칸을 사이에 두고 주어지며, 0은 바다, 1은 육지를 나타낸다. 항상 두 개 이상의 섬이 있는 데이터만 입력으로 주어진다.

4) 출력

첫째 줄에 가장 짧은 다리의 길이를 출력한다.

 

위의 링크로 들어가보면 문제의 예시가 그림을 통해 더 자세히 설명되어 있으니 들어가서 확인해보자


2. 풀이

이 문제를 풀기 위해 필요한 요소를 알아보자

  • a[][] : 문제에서 해당 자리가 섬인지 아닌지를 배열 a 로 받는 것이다. 0 또는 1
  • group[][] : 섬끼리 grouping을 하고 난 뒤에 섬이 어느 섬에 속해 있는 섬인지 group number를 붙여주게 된다
  • distance[][]: 거리를 계산하기 위한 배열. 섬인 자리는 0부터 시작하고 인접해있는 자리는 1을 더하는 식으로 계산해준다. 

이 문제는 먼저 섬이 있는 곳을 찾아내어 group 번호를 붙어준다. 이는 dfs로 해주어도 되고 bfs로 해주어도 되는데 여기서는 dfs 로 계산해주었다. 

public static void dfs(int x, int y, int cnt){
        group[x][y]=cnt;
        for(int i=0; i<4; i++){
            int nx=x+dx[i];
            int ny=y+dy[i];
            if (nx>=0 && nx<n &&ny>=0 &&ny<n){
                if (a[nx][ny]==1 &&group[nx][ny]==0){
                    dfs(nx, ny, cnt);
                }
            }
        }
    }

위와 같이 dfs 함수를 사용하고 나면 섬이 있는 자리에는 그룹 번호를 붙일 수 있게 되었다. 즉, 섬이 있는 자리에 group 배열에는 해당 섬의 그룹 숫자가 적혀있다.


다음으로는 섬이 아닌 부분의 거리도 계산해주고, 섬이 아닌 부분도 나중을 위해 group 번호를 붙여준다. 거리의 경우 인접한 거리에 1을 더해주면 되고 group 번호는 인접한 group의 번호를 그대로 가져다 쓰면 된다. 여기서 섬이 아닌 부분도 group 번호를 붙여주는 이유는 나중에 최종으로 다리를 놓는 경우를 생각할 때 인접한 두 칸의 그룹번호가 다르면 그 두 칸의 거리를 더한 것이 다리를 놓는 최종 거리가 되기 때문이다. 만약 섬이 없는 부분에 그룹번호를 입력해놓지 않으면 이런 방식으로 구할 수 없다.

 

따라서 queue에 처음 섬인 부분을 넣고 섬인 부분의 distance는 0, 그렇지 않은 부분의 distance는 -1로 초기화하고 시작한다. 이후, 섬이 아닌 부분의 distance는 인접한 것에 1을 더하는 식으로 코드를 구성해보았다. 

 

Queue<Pair> q=new LinkedList<>();


        for(int i=0; i<n;i++){
            for(int j=0; j<n; j++){
                distance[i][j]=-1;
                if (a[i][j]==1){
                    distance[i][j]=0;
                    q.add(new Pair(i,j));
                }
            }
        }
        while(!q.isEmpty()){
            Pair a=q.remove();
            int x=a.x;
            int y=a.y;
           for(int i=0; i<4; i++){
               int nx=x+dx[i];
               int ny=y+dy[i];
               if (nx>=0 && ny>=0 && nx<n && ny<n){
                   if (distance[nx][ny]==-1){
                       distance[nx][ny]=distance[x][y]+1;
                       group[nx][ny]=group[x][y];
                       q.add(new Pair(nx, ny));
                   }
               }
           }
        }

이런식으로 queue에 섬인 부분을 넣어두고, 이후 pop과 add를 하면서 거리와 그룹넘버를 채워넣게 된다. 


다음으로는 이제 모든 n*n 칸을 다 돌면서 양 옆칸의 그룹넘버가 다르다면 두 개의 거리를 더해서 ans 에 넣어준다. 이 ans를 최소화시키는 방향으로 코드를 구성해주면 된다. 

  int ans = -1;
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                for (int k=0; k<4; k++) {
                    int nx = i+dx[k];
                    int ny = j+dy[k];
                    if (0 <= nx && nx < n && 0 <= ny && ny < n) {
                        if (group[i][j] != group[nx][ny]) {
                            if (ans == -1 || ans > distance[i][j] + distance[nx][ny]) {
                                ans = distance[i][j] + distance[nx][ny];
                            }
                        }
                    }
                }
            }
        }

조건문에 ans==-1이 들어간 이유는 맨 처음 ans 를 업데이트 해주는 경우를 고려해준것이다.


이 문제는 각각의 섬에서 각각 bfs를 사용해도 되지만 그러면 시간이 오래 걸리기 때문에 모든 섬을 queue에 넣어놓고 bfs를 한꺼번에 사용했다.

 

3. 코드

모든 것을 종합적으로 고려하여, 전체코드는 아래와 같다. 

import java.util.*;
class Pair{
    int x, y;
    Pair(int x, int y){
        this.x=x;
        this.y=y;
    }
}
public class Main{
     public static final int[] dx={0,0,1,-1};
    public static final int[] dy={1,-1,0,0};
    
    static int group[][];
    static int n;
    static int a[][];
  public static void dfs(int x, int y, int cnt){
        group[x][y]=cnt;
        for(int i=0; i<4; i++){
            int nx=x+dx[i];
            int ny=y+dy[i];
            if (nx>=0 && nx<n &&ny>=0 &&ny<n){
                if (a[nx][ny]==1 &&group[nx][ny]==0){
                    dfs(nx, ny, cnt);
                }
            }
        }
    }
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
         n=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();
            }
        }
        group=new int[n][n];
        int cnt=0;
        for(int i=0; i<n; i++){
            for(int j=0; j<n; j++){
                if (a[i][j]==1 &&group[i][j]==0){
                    dfs(i,j,++cnt );
                }
            }
        }
        Queue<Pair> q=new LinkedList<>();
        int distance[][]=new int [n][n];
        for(int i=0; i<n;i++){
            for(int j=0; j<n; j++){
                distance[i][j]=-1;
                if (a[i][j]==1){
                    distance[i][j]=0;
                    q.add(new Pair(i,j));
                }
            }
        }
        while(!q.isEmpty()){
            Pair a=q.remove();
            int x=a.x;
            int y=a.y;
           for(int i=0; i<4; i++){
               int nx=x+dx[i];
               int ny=y+dy[i];
               if (nx>=0 && ny>=0 && nx<n && ny<n){
                   if (distance[nx][ny]==-1){
                       distance[nx][ny]=distance[x][y]+1;
                       group[nx][ny]=group[x][y];
                       q.add(new Pair(nx, ny));
                   }
               }
           }
        }
        
         int ans = -1;
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                for (int k=0; k<4; k++) {
                    int nx = i+dx[k];
                    int ny = j+dy[k];
                    if (0 <= nx && nx < n && 0 <= ny && ny < n) {
                        if (group[i][j] != group[nx][ny]) {
                            if (ans == -1 || ans > distance[i][j] + distance[nx][ny]) {
                                ans = distance[i][j] + distance[nx][ny];
                            }
                        }
                    }
                }
            }
        }
        System.out.println(ans);
        
         }
}

2월 16, 2024

[그래프의 표현] 인접행렬과 인접리스트

 그래프(Graph) 란 자료구조의 일종으로

크게 정점 (Node, Vertex) 와 간선(Edge) 두가지의 구성요소를 가지고 있다.

정점과 간선으로 표현된 그래프

 

위 그림에서 1,2,3으로 표시된 원은 정점을 나타내는 것이고, 파란색 화살표는 간선을 표현하는 것이다. 

 

우리는 이러한 그래프를 코드화하여서 표현해야 하는데, 크게 그래프를 코드화하여 표현하는데는 두가지 방법이 있다. 첫 번째는 인접 항렬 (Adjacency-matrix)이고 두 번째는 인접 리스트(Adjacency-list) 이다.

 

1) 인접 항렬 (Adjacency-matrix)

 

인접항렬은 정점의 개수를 V라고 했을 때 V*V 크기의 배열을 이용하는 방법이다. 

정점 i에서 정점 j 로 가는 간선이 있을 경우 A[i][j]=1로 표현하고 간선이 존재하지 않을 경우 A[i][j]=0으로 표현한다. 

여기서 중요한 점은 그래프에 방향이 있는지, 없는지 방향의 유무이다. 방향이 없는 그래프도 많이 존재하는데 그 경우에는 정점 i에서 정점 j로 가는 간선과 정점 j에서 정점 i로 가는 간선이 같은 간선을 의미하게 된다.

따라서 A[i][j]=A[j][i] 라는 식이 세워지게 된다. 즉 matrix 상으로 봤을 때 대칭구조를 가지고 있다는 것이다. 

 

조금 더 예시를 들어서 설명해보자면,



위와 같은 그래프가 있다고 해보자. 그렇다면 정점이 5개 이므로 5*5의 배열을 사용하면 되고 간선이 (1,2), (1,3), (2,3), (2,4), (2,5), (3,4), (4,5) 사이에 존재하는 것이라고 할 수 있다. 

 

따라서 해당 칸의 행렬을 생각해보면,

위와 같이 만들 수 있다. x축은 node i를 나타내고 y축은 node j를 나타내며, i와 j가 연결되어 있다면 1, 아니면 0으로 표현이 된다고 할 수 있다. 양방향 그래프의 경우 (i, j)나 (j, i)가 같기 때문에 해당 표에서도 주황색으로 표시된 대각선 칸을 기준으로 대각선 대칭을 이루는 것을 알 수 있다. 

 

시간복잡도와 공간복잡도는 어떻게 될까?

우선 공간복잡도의 경우에는 V*V 만큼이 필요하다는 것을 알 수 있다 (정점의 개수를 V라고 했을때). 2차원 배열이 필요하기 때문이다. 

그렇다면 한 정점과 연결된 모든 간선을 파악하는데 걸리는 시간복잡도는 얼마일까?

이것은 O(V)라고 할 수 있다. 왜냐하면 만약 node 3과 연결된 간선을 파악하기 위해서는

빨간색으로 표시된 것처럼 node 1부터 node 5까지 V를 검사해주어야 하기 때문이다. 

 


2) 인접 리스트(Adjacency-list)

 

인접리스트는 리스트를 이용해서 그래프를 구현하는 방식이다.

A[i]= i와 연결된 정점을 리스트로 포함하고 있는 것을 나타낸다고 할 수 있다. 

 

따라서 위의 그래프를 동일하게 적용시킨다면, 인접리스트의 경우

A[1]: 2, 3

A[2]: 1, 3, 4, 5

A[3]: 1, 2, 4

A[4]: 2, 3, 5

A[5]: 2, 4

이런 식으로 표현할 수 있는 것이다. 

 

그렇다면, 인접리스트의 공간복잡도와 시간복잡도는 얼마일까?

우선 간선의 길이를 E라고 했을 때 인접리스트의 공간복잡도는 O(E)이다. 간선이 있는 경우에만 저장을 하기 때문이다.

한 정점과 연결된 모든 간선을 찾는데 걸리는 시간복잡도는 O(차수)라고 할 수 있다. 

여기서 차수란 정점과 연결되어 있는 간선의 개수를 의미한다. 

인접리스트의 경우 한 정점에 해당하는 배열에 연결되어 있는 간선을 저장해놓기 때문이다. 


3) 인접항렬와 인접리스트의 비교

위의 공간복잡도와 시간복잡도에서 알 수 있었듯, 대부분의 경우 인접리스트가 인접항렬을 사용한 것보다 효과적인 모습을 보인다. 따라서 알고리즘을 풀 때, 인접리스트를 사용하는 경우가 많다고 한다.

 

그러면 인접항렬을 사용하기 좋을 때는 언제일짜?

바로 i와 j node를 이어주는 간선이 있는지 없는지의 유무를 판단할때이다.

해당 경우에 인접항렬을 쓰면 A[i][j]의 값을 검사하기 때문에 O(1)의 시간복잡도를 보이기 때문이다. 반면 인접리스트를 사용하게 되면 앞에서도 언급했지만 O(차수)만큼의 시간복잡도가 걸리기 때문에 인접항렬보다 느리게 된다. 

 

하지만 특수경우을 제외하고는 알고리즘을 푸는 일반적인 경우라면 인접리스트를 쓰는 것을 더 추천하는 바이다.


 

4) 간선리스트

 

오늘 소개했던 두 가지 방법이외에도 간선리스트라는 방법이 존재한다. 간선리스트는 말 그대로 간선을 저장하고 있는 경우인데, 매우 특수한 방법이니 우선 위 두 가지 방법만 알아두어도 충분할 것 같다.