호석이 두 마리 치킨

조소복·2022년 10월 12일
0

문제

컴공 출신은 치킨집을 하게 되어있다. 현실을 부정하지 말고 받아들이면 마음이 편하다. 결국 호석이도 2050년에는 치킨집을 하고 있다. 치킨집 이름은 "호석이 두마리 치킨"이다.

이번에 키친 도시로 분점을 확보하게 된 호석이 두마리 치킨은 도시 안에 2개의 매장을 지으려고 한다. 도시는 N 개의 건물과 M 개의 도로로 이루어져 있다. 건물은 1번부터 N번의 번호를 가지고 있다. i 번째 도로는 서로 다른 두 건물 Ai 번과 Bi 번 사이를 1 시간에 양방향으로 이동할 수 있는 도로이다.

키친 도시에서 2개의 건물을 골라서 치킨집을 열려고 한다. 이 때 아무 곳이나 열 순 없어서 모든 건물에서의 접근성의 합을 최소화하려고 한다. 건물 X 의 접근성은 X 에서 가장 가까운 호석이 두마리 치킨집까지 왕복하는 최단 시간이다. 즉, "모든 건물에서 가장 가까운 치킨집까지 왕복하는 최단 시간의 총합"을 최소화할 수 있는 건물 2개를 골라서 치킨집을 열려고 하는 것이다.

컴공을 졸업한 지 30년이 넘어가는 호석이는 이제 코딩으로 이 문제를 해결할 줄 모른다. 알고리즘 퇴물 호석이를 위해서 최적의 위치가 될 수 있는 건물 2개의 번호와 그 때의 "모든 건물에서 가장 가까운 치킨집까지 왕복하는 최단 시간의 총합"을 출력하자. 만약 이러한 건물 조합이 여러 개라면, 건물 번호 중 작은 게 더 작을수록, 작은 번호가 같다면 큰 번호가 더 작을수록 좋은 건물 조합이다.

입력

첫 번째 줄에 건물의 개수 N과 도로의 개수 M 이 주어진다. 이어서 M 개의 줄에 걸쳐서 도로의 정보 Ai , Bi 가 공백으로 나뉘어서 주어진다. 같은 도로가 중복되어 주어지는 경우는 없다. 어떤 두 건물을 잡아도 도로를 따라서 오고 가는 방법이 존재함이 보장된다.

출력

한 줄에 건물 2개가 지어질 건물 번호를 오름차순으로 출력하고, 그때 모든 도시에서의 왕복 시간의 합을 출력한다.

만약 건물 조합이 다양하게 가능하면, 작은 번호가 더 작은 것을, 작은 번호가 같다면 큰 번호가 더 작은 걸 출력한다.

제한

2 ≤ N ≤ 100
N-1 ≤ M ≤ N×(N - 1)/2
1 ≤ Ai , Bi​ ≤ N (Ai ≠ Bi)

예제 입력 1

5 4
1 3
4 2
2 5
3 2

예제 출력 1

1 2 6

위의 그림과 같이 1번과 2번 건물에 치킨집을 짓게 되면 1번부터 5번 건물에 대해 치킨집까지의 왕복 시간이 0, 0, 2, 2, 2 로 최소가 된다. 2번과 3번 건물에 지어도 동일한 왕복 시간이 나오지만 더 작은 건물 번호를 출력해야 함을 유의하자.

문제 풀이

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class BJ21278 {
    static int[][] map;
    static int N,M;
    static boolean[] visited;
    static int tmp,a,b;

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

        N=Integer.parseInt(st.nextToken());
        M=Integer.parseInt(st.nextToken());

        map=new int[N][N];

        for(int i=0;i<N;i++){
            for(int j=0;j<N;j++){
                map[i][j]=(int)1e9;
            }
        }

        for(int i=0;i<M;i++){
            st=new StringTokenizer(br.readLine());
            int a=Integer.parseInt(st.nextToken())-1;
            int b=Integer.parseInt(st.nextToken())-1;

            map[a][b]=1;
            map[b][a]=1;
        }


        for(int k=0;k<N;k++){
            for(int i=0;i<N;i++){
                for(int j=0;j<N;j++){
                    if(i==j || j==k || k==i) continue;
                    if(map[i][j]>map[i][k]+map[k][j]){
                        map[i][j]=map[i][k]+map[k][j];
                    }
                }
            }
        }

        int result=Integer.MAX_VALUE;
        // 치킨 집 두 곳 지정
        for(int i=0;i<N;i++){
            for(int j=i+1;j<N;j++){
                tmp=0;
                for(int k=0;k<N;k++){
                    if(i==k || j==k) continue;
                    int min=Math.min(map[i][k],map[j][k]);
                    tmp+=min;
                }

                if(tmp<result){
                    result=tmp;
                    a=i+1;
                    b=j+1;
                }
            }
        }

        System.out.println(a+" "+b+" "+result*2);
    }

}

치킨집 두 군데를 지정한 다음 근처 건물에서 왕복하는 시간 합의 최소값을 구하는 문제였다.

플로이드 워셜을 이용하여 각 노드에서 다른 노드까지의 거리값을 미리 구해준 다음 치킨집 두 곳을 지정하여 왕복하는데 걸리는 최소시간을 구하는 방식을 이용한다.


플로이드 워셜을 이용하여 최소 거리값 구해주기

map=new int[N][N];

for(int i=0;i<N;i++){
    for(int j=0;j<N;j++){
        map[i][j]=(int)1e9;
    }
}

for(int i=0;i<M;i++){
    st=new StringTokenizer(br.readLine());
    int a=Integer.parseInt(st.nextToken())-1;
    int b=Integer.parseInt(st.nextToken())-1;
    
    map[a][b]=1;
    map[b][a]=1;
}


for(int k=0;k<N;k++){
    for(int i=0;i<N;i++){
        for(int j=0;j<N;j++){
            if(i==j || j==k || k==i) continue;
            if(map[i][j]>map[i][k]+map[k][j]){
                map[i][j]=map[i][k]+map[k][j];
            }
        }
    }
}

map 배열에 1e9라는 큰 값으로 초기화해준다.

입력값을 통해 이어진 노드들은 1로 설정해주고 플로이드워셜을 통해 최소거리값을 갱신한다.

k 변수는 거쳐갈 정점을 나타내는 것으로

예를 들어, 1번 -> 2번 노드로 향하는 값이 5이고, 1번 -> 3번 -> 2번 처럼 3번 노드를 거쳐서 가는 경우 거리값이 3이라고 한다면 1번에서 2번으로 향하는 최소거리값은 3이 된다.

위 예시를 그대로 코드로 구현한 것이 플로이드 워셜이다.

k변수를 3번과 같은 거쳐갈 정점, i는 1번과 같이 출발하는 정점, j는 2번과 같이 도착할 정점을 나타낸다.

만약 1번에서 2번으로 향하는 거리값인 map[i][j]보다 3번을 거쳐서 도달하는 map[i][k]+map[k][j] 거리값이 더 적다면 map[i][j]값을 갱신해주는 것이다.

모든 작업을 마무리하면 각 노드로 향하는 최소거리값이 구해진다.


치킨가게 두 곳 지정하여 최소값 구하기

int result=Integer.MAX_VALUE;
// 치킨 집 두 곳 지정
for(int i=0;i<N;i++){
    for(int j=i+1;j<N;j++){
        tmp=0;
        for(int k=0;k<N;k++){
            if(i==k || j==k) continue;
            int min=Math.min(map[i][k],map[j][k]);
            tmp+=min;
        }
        
        if(tmp<result){
            result=tmp;
            a=i+1;
            b=j+1;
        }
    }
}

치킨집은 두 곳으로 정해져있기 때문에 완전탐색을 통해 치킨집 두 곳을 지정한다.

(1번, 2번), (2번, 1번) 을 치킨집으로 지정하는것은 결국 같은 결과이기 때문에 j=i+1으로 반복문 변수를 초기화해준다.

치킨집 두 곳을 지정했다면 모든 노드에서 치킨집까지 도달하는 거리값을 합해야하기 때문에 반복문을 한 번 더 사용해준다.

첫번째 치킨집과 두번째 치킨집 중 더 가까운 거리값을 tmp 변수에 추가해주고

모든 노드의 거리값이 추가되었다면 최종 결과 출력을 위해 result 변수와 비교해주고 값을 갱신해준다.

위 로직들을 반복해주면 답을 출력할 수 있다.



처음엔 치킨집 두 곳을 먼저 지정한 다음 BFS를 통해 모든 노드에 대해 최소거리값을 구할 생각이었는데 시간초과가 발생했다.

시간초과 발생 풀이

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {
    static int[][] map;
    static int N,M;
    static boolean[] visited;
    static int tmp;
    static int a,b,min=Integer.MAX_VALUE;

    public static class Node{
        int node,dist;

        public Node(int node, int dist) {
            this.node = node;
            this.dist = dist;
        }
    }

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

        N=Integer.parseInt(st.nextToken());
        M=Integer.parseInt(st.nextToken());

        map=new int[N][N];

        for(int i=0;i<M;i++){
            st=new StringTokenizer(br.readLine());
            int a=Integer.parseInt(st.nextToken())-1;
            int b=Integer.parseInt(st.nextToken())-1;

            map[a][b]=1;
            map[b][a]=1;
        }

        // 치킨 집 두 곳 지정
        for(int i=0;i<N;i++){
            for(int j=i;j<N;j++){
                if(i==j) continue;

                tmp=0;
                for(int n=0;n<N;n++) {
                    tmp+=bfs(i, j,n);
                }

                if(tmp<min){
                    min=tmp;
                    a=i+1;
                    b=j+1;
                }
            }
        }

        System.out.println(a+" "+b+" "+min);

    }

    private static int bfs(int i, int j,int node) {
        Queue<Node> queue=new ArrayDeque<>();
        queue.offer(new Node(node,0));
        visited=new boolean[N];
        visited[node]=true;

        int result=0;

        while(!queue.isEmpty()){
            Node n=queue.poll();

            result=Math.max(result,n.dist);

            if(n.node==i || n.node==j) break;

            visited[n.node]=true;

            for(int a=0;a<N;a++){
                if(map[n.node][a]==1 && !visited[a]){
                    queue.offer(new Node(a,n.dist+1));
                }
            }
        }

        return result*2;
    }

}

시간초과를 없애기 위해 구글링을 하던 중 치킨집 두 곳을 먼저 구하지 말고 거리값을 먼저 구한 뒤 치킨집 위치를 구하는 방식이 시간초과가 발생하지 않는다는 힌트를 얻고

플로이드 워셜을 이용하여 문제를 풀었고 빠른 속도로 정답처리가 되었다.

profile
개발을 꾸준히 해보자

0개의 댓글