본문 바로가기
알고리즘

[백준] 17472번: 다리 만들기2 Java

by 랼랼 2022. 4. 28.

문제

섬으로 이루어진 나라가 있고, 모든 섬을 다리로 연결하려고 한다. 이 나라의 지도는 N×M 크기의 이차원 격자로 나타낼 수 있고, 격자의 각 칸은 땅이거나 바다이다.

섬은 연결된 땅이 상하좌우로 붙어있는 덩어리를 말하고, 아래 그림은 네 개의 섬으로 이루어진 나라이다. 색칠되어있는 칸은 땅이다.

다리는 바다에만 건설할 수 있고, 다리의 길이는 다리가 격자에서 차지하는 칸의 수이다. 다리를 연결해서 모든 섬을 연결하려고 한다. 섬 A에서 다리를 통해 섬 B로 갈 수 있을 때, 섬 A와 B를 연결되었다고 한다. 다리의 양 끝은 섬과 인접한 바다 위에 있어야 하고, 한 다리의 방향이 중간에 바뀌면 안된다. 또, 다리의 길이는 2 이상이어야 한다.

다리의 방향이 중간에 바뀌면 안되기 때문에, 다리의 방향은 가로 또는 세로가 될 수 밖에 없다. 방향이 가로인 다리는 다리의 양 끝이 가로 방향으로 섬과 인접해야 하고, 방향이 세로인 다리는 다리의 양 끝이 세로 방향으로 섬과 인접해야 한다.

섬 A와 B를 연결하는 다리가 중간에 섬 C와 인접한 바다를 지나가는 경우에 섬 C는 A, B와 연결되어있는 것이 아니다. 

아래 그림은 섬을 모두 연결하는 올바른 2가지 방법이고, 다리는 회색으로 색칠되어 있다. 섬은 정수, 다리는 알파벳 대문자로 구분했다.

 

다리의 총 길이: 13
D는 2와 4를 연결하는 다리이고, 3과는 연결되어 있지 않다.
다리의 총 길이: 9 (최소)

다음은 올바르지 않은 3가지 방법이다

C의 방향이 중간에 바뀌었다 D의 길이가 1이다. 가로 다리인 A가 1과 가로로 연결되어 있지 않다.

다리가 교차하는 경우가 있을 수도 있다. 교차하는 다리의 길이를 계산할 때는 각 칸이 각 다리의 길이에 모두 포함되어야 한다. 아래는 다리가 교차하는 경우와 기타 다른 경우에 대한 2가지 예시이다.

A의 길이는 4이고, B의 길이도 4이다.
총 다리의 총 길이: 4 + 4 + 2 = 10
다리 A: 2와 3을 연결 (길이 2)
다리 B: 3과 4를 연결 (길이 3)
다리 C: 2와 5를 연결 (길이 5)
다리 D: 1과 2를 연결 (길이 2)
총 길이: 12

나라의 정보가 주어졌을 때, 모든 섬을 연결하는 다리 길이의 최솟값을 구해보자.

 

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

 

17472번: 다리 만들기 2

첫째 줄에 지도의 세로 크기 N과 가로 크기 M이 주어진다. 둘째 줄부터 N개의 줄에 지도의 정보가 주어진다. 각 줄은 M개의 수로 이루어져 있으며, 수는 0 또는 1이다. 0은 바다, 1은 땅을 의미한다.

www.acmicpc.net

입력

첫째 줄에 지도의 세로 크기 N과 가로 크기 M이 주어진다. 둘째 줄부터 N개의 줄에 지도의 정보가 주어진다. 각 줄은 M개의 수로 이루어져 있으며, 수는 0 또는 1이다. 0은 바다, 1은 땅을 의미한다.

출력

모든 섬을 연결하는 다리 길이의 최솟값을 출력한다. 모든 섬을 연결하는 것이 불가능하면 -1을 출력한다.

제한

  • 1 ≤ N, M ≤ 10
  • 3 ≤ N×M ≤ 100
  • 2 ≤ 섬의 개수 ≤ 6

접근

여러 풀이 전략이 결합된 문제이다.

1) 먼저 깊이/너비 탐색을 통해 구역번호를 지정하고, 같은 섬은 같은 구역번호로 지정한다.

2) 주변 섬과의 거리를 측정하고, 거리가 1 이상이면 간선 정보(섬1, 섬2, 거리)를 저장하고 list에 넣는다.

3) MST를 이용하여 최소 신장 트리의 길이를 출력한다.

   만약 선택된 간선이 섬 개수 -1 보다 작으면 적절하지 않음으로 -1을 출력한다.

구현

1) 입력

static int N,M; //행,열 크기
static int[][] Board; //주어진 데이터를 저장할 2차원 배열

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()); //열 크기
    Board=new int[N][M]; //데이터 저장을 위한 배열 선언
    //주어진 값 입력
    for(int i=0;i<N;i++) {
        st=new StringTokenizer(br.readLine());
        for(int j=0;j<M;j++) {
            Board[i][j]=Integer.parseInt(st.nextToken());
        }
    }
    //최소 신장 트리의 간선 합 길이 출력
    System.out.println(minBridge());
    br.close();
}

주어진 N과 M, 섬의 정보를 입력한다.

 

2) 최소 간선의 길이

public static int minBridge() {
    //각 구역에 구역번호 지정하고 최대 구역번호 리턴(구역은 2부터 시작)
    int maxDivision = islandDivision();
    //division 개수
    int countDivision = maxDivision-1;
    //kruskal 알고리즘으로 최소 신장 트리 길이 출력
    return kruskalMst(maxDivision, countDivision);
}

 

최소 간선 길이 출력을 위해

먼저 섬의 구역을 지정하고, 최대 구역 번호와 구역 개수를 이용하여 최소 신장 길이를 구한다.

 

3) 섬 구역 지정

//방향
static int[][] D = {{0,1},{-1,0},{1,0},{0,-1}};

//섬 구역 지정
public static int islandDivision() {
    //구역 번호, 1은 미지정, 2부터 시작하여 증가함
    int division = 2;
    //bfs 를 위한 큐
    Queue<int[]> q = new LinkedList<int[]>();

    for(int i=0;i<N;i++) {
        for(int j=0;j<M;j++) {
            //1이면 섬이 존재
            if(Board[i][j]==1) {
                //큐에 추가하여 주변 탐색
                q.add(new int[] {i,j});
                while(!q.isEmpty()) {
                    int[] curr = q.poll();
                    int r = curr[0];
                    int c = curr[1];
                    //구역 표시
                    Board[r][c]=division;
                    //상하좌우 탐색
                    for(int d=0;d<4;d++) {
                        int nr = r+D[d][0];
                        int nc = c+D[d][1];
                        //범위 이탈시 continue
                        if(nr<0||nr>=N||nc<0||nc>=M) continue;
                        //1이면 아직 미지정이므로 큐에 넣어 탐색
                        if(Board[nr][nc]==1) {
                            q.add(new int[] {nr,nc});
                        }
                    }
                }
                //다른 구역 지정을 위해 1 증가
                division++;
            }
        }
    }

    //최대 division 출력
    return division-1;
}

너비 우선 탐색을 통해 섬의 구역마다 번호를 지정한다.

같은 섬이라면 같은 구역 번호를 가지게 되며

1은 미지정된 섬이라는 의미로 사용하여 2부터 구역번호를 지정하였다.

 

4) 크루스칼 MST

 4-1) Edge 클래스

//간선 정보 저장
static class Edge implements Comparable<Edge>{
    int node1,node2;
    int length;
    //생성자
    public Edge(int node1, int node2, int length) {
        this.node1=node1;
        this.node2=node2;
        this.length=length;
    }
    //pq를 이용하기 위한 정렬, length가 적은 순
    @Override
    public int compareTo(Edge o) {
        if(this.length<o.length) return -1;
        if(this.length>o.length) return 1;
        return 0;
    }

    @Override
    public String toString() {
        return node1+" "+node2+" "+length;
    }
}

간선 정보를 저장할 클래스이다.

node(섬) 과 다른 node의 정보와 이를 연결하는 간선 길이 정보를 가지고 있다.

정렬을 위해 Comparable을 상속받고 길이가 짧은 순으로 정렬하도록 하였다.

 

 4-2) Union Find를 위한 메서드

static int[] Parents; //부모 배열

//부모 찾기
public static int getParent(int node) {
    if(Parents[node]==node) return node;
    return Parents[node]=getParent(Parents[node]);
}

//부모 세팅, 작은 수가 부모
public static void setParent(int node1, int node2) {
    int p1 = getParent(node1);
    int p2 = getParent(node2);
    if(p1<p2) Parents[p2]=p1;
    else Parents[p1]=p2;
}

//같은 부모 판별
public static boolean isSameParent(int node1, int node2) {
    return getParent(node1)==getParent(node2);
}

간선의 부모를 지정/탐색하기 위한 세트 메서드이다.

 

4-3) 크루스칼 MST (1)

//최소 신장 길이 탐색, kruskal MST 사용
public static int kruskalMst(int maxDivision, int countDivision) {
    //부모 초기화, 자기 자신이 부모
    Parents = new int[maxDivision+1];
    for(int i=2;i<=maxDivision;i++) {
        Parents[i]=i;
    }
    //length가 적은순으로 poll되는 우선순위 큐
    PriorityQueue<Edge> pq = new PriorityQueue<>();

    for(int i=0;i<N;i++) {
        for(int j=0;j<M;j++) {
            //0이면 비어있음으로 continue
            if(Board[i][j]==0) continue;
            //현재 구역 번호
            int no = Board[i][j];
            //완전 탐색할 것임으로 오른쪽과 아래만 탐색
            for(int d=0;d<2;d++) {
                //구역과의 거리
                int length = 0;
                //이동한 행과 열
                int r = i+D[d][0];
                int c = j+D[d][1];
                //범위를 벗어날때까지 반복
                while(r>=0&&c>=0&&r<N&&c<M) {
                    //본인 번호를 만나면 break
                    if(Board[r][c]==no) break;
                    //다른 섬에 다다르면
                    if(Board[r][c]!=0) {
                        //거리가 1 이상일 경우에만 간선 정보를 pq에 추가
                        if(length>1) {
                            pq.add(new Edge(no, Board[r][c], length));
                        }
                        //더이상 탐색할 필요가 없음으로 종료
                        break;
                    }
                    //아직 섬을 만나지 못했음으로 한칸 더 이동
                    r+=D[d][0];
                    c+=D[d][1];
                    //거리 1 증가
                    length++;
                }
            }
        }
    }
//----------------------------크루스칼 MST (2)----------------------------//

먼저 부모를 자신의 번호로 초기화한다. 이 때, 구역번호는 2부터 사용하였음으로 2부터 초기화하였다.

완전탐색을 통해 인접 섬과의 거리를 측정하였다.

이 때 방향은 오른쪽과 아래방향만 탐색하였다.

(위와 왼쪽 방향을 포함시키면 완전탐색이기에 같은 간선 정보가 2번 탐색하게 된다.)

거리 측정은 자신과 같은 번호를 만나거나 배열 범위를 벗어나면 break하고,

다른 섬을 만났을 때, 거리가 2 이상일 경우에만 우선순위 큐에 추가한다.

 

 4-3) 크루스칼 MST (2)

//----------------------------크루스칼 MST (1)----------------------------//
    //트리 간선 개수
    int count = 0;
    //간선의 길이 합
    int sumLength = 0;

    //간선 탐색
    while(!pq.isEmpty()) {
        Edge edge = pq.poll();
        //같은 부모가 아니면 간선 채택
        if(!isSameParent(edge.node1, edge.node2)) {
            //두 구역의 부모 설정
            setParent(edge.node1, edge.node2);
            //간선 개수 1 증가
            count++;
            //간선 길이 추가
            sumLength+=edge.length;
            //간선의 개수가 N-1이라면 리턴
            if(count==countDivision-1) return sumLength;
        }
    }
    //간선 개수가 충분하지 않음으로 구성 불가
    return -1;
}

크루스칼 알고리즘을 통해

길이가 짧은 간선순으로 검사를 한다.

간선끼리 부모가 다른 경우,

간선을 선택하고 (count 증가), 부모를 동일하게 세팅하고 (setParent()), 간선 길이를 추가한다.

만약 선택한 간선 개수가 섬의 개수-1 이라면 간선 길이 합을 출력하고,

모두 탐색했음에도 간선 개수가 부족하면 모두 연결할 수 없음으로 -1을 출력한다.

 

5) 전체 코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {
	static int N,M; //행,열 크기
	static int[][] Board; //주어진 데이터를 저장할 2차원 배열
	static int[][] D = {{0,1},{-1,0},{1,0},{0,-1}}; //방향
	static int[] Parents; //부모 배열
	
	//간선 정보 저장
	static class Edge implements Comparable<Edge>{
		int node1,node2;
		int length;
		//생성자
		public Edge(int node1, int node2, int length) {
			this.node1=node1;
			this.node2=node2;
			this.length=length;
		}
		//pq를 이용하기 위한 정렬, length가 적은 순
		@Override
		public int compareTo(Edge o) {
			if(this.length<o.length) return -1;
			if(this.length>o.length) return 1;
			return 0;
		}
		
		@Override
		public String toString() {
			return node1+" "+node2+" "+length;
		}
	}
	
	//부모 찾기
	public static int getParent(int node) {
		if(Parents[node]==node) return node;
		return Parents[node]=getParent(Parents[node]);
	}
	
	//부모 세팅, 작은 수가 부모
	public static void setParent(int node1, int node2) {
		int p1 = getParent(node1);
		int p2 = getParent(node2);
		if(p1<p2) Parents[p2]=p1;
		else Parents[p1]=p2;
	}
	
	//같은 부모 판별
	public static boolean isSameParent(int node1, int node2) {
		return getParent(node1)==getParent(node2);
	}
	
	//섬 구역 지정
	public static int islandDivision() {
		//구역 번호, 1은 미지정, 2부터 시작하여 증가함
		int division = 2;
		//bfs 를 위한 큐
		Queue<int[]> q = new LinkedList<int[]>();
		
		for(int i=0;i<N;i++) {
			for(int j=0;j<M;j++) {
				//1이면 섬이 존재
				if(Board[i][j]==1) {
					//큐에 추가하여 주변 탐색
					q.add(new int[] {i,j});
					while(!q.isEmpty()) {
						int[] curr = q.poll();
						int r = curr[0];
						int c = curr[1];
						//구역 표시
						Board[r][c]=division;
						//상하좌우 탐색
						for(int d=0;d<4;d++) {
							int nr = r+D[d][0];
							int nc = c+D[d][1];
							//범위 이탈시 continue
							if(nr<0||nr>=N||nc<0||nc>=M) continue;
							//1이면 아직 미지정이므로 큐에 넣어 탐색
							if(Board[nr][nc]==1) {
								q.add(new int[] {nr,nc});
							}
						}
					}
					//다른 구역 지정을 위해 1 증가
					division++;
				}
			}
		}
		
		//최대 division 출력
		return division-1;
	}
	
	//최소 신장 길이 탐색, kruskal MST 사용
	public static int kruskalMst(int maxDivision, int countDivision) {
		//부모 초기화, 자기 자신이 부모
		Parents = new int[maxDivision+1];
		for(int i=2;i<=maxDivision;i++) {
			Parents[i]=i;
		}
		//length가 적은순으로 poll되는 우선순위 큐
		PriorityQueue<Edge> pq = new PriorityQueue<>();
		
		for(int i=0;i<N;i++) {
			for(int j=0;j<M;j++) {
				//0이면 비어있음으로 continue
				if(Board[i][j]==0) continue;
				//현재 구역 번호
				int no = Board[i][j];
				//완전 탐색할 것임으로 오른쪽과 아래만 탐색
				for(int d=0;d<2;d++) {
					//구역과의 거리
					int length = 0;
					//이동한 행과 열
					int r = i+D[d][0];
					int c = j+D[d][1];
					//범위를 벗어날때까지 반복
					while(r>=0&&c>=0&&r<N&&c<M) {
						//본인 번호를 만나면 break
						if(Board[r][c]==no) break;
						//다른 섬에 다다르면
						if(Board[r][c]!=0) {
							//거리가 1 이상일 경우에만 간선 정보를 pq에 추가
							if(length>1) {
								pq.add(new Edge(no, Board[r][c], length));
							}
							//더이상 탐색할 필요가 없음으로 종료
							break;
						}
						//아직 섬을 만나지 못했음으로 한칸 더 이동
						r+=D[d][0];
						c+=D[d][1];
						//거리 1 증가
						length++;
					}
				}
			}
		}
		//트리 간선 개수
		int count = 0;
		//간선의 길이 합
		int sumLength = 0;
		
		//간선 탐색
		while(!pq.isEmpty()) {
			Edge edge = pq.poll();
			//같은 부모가 아니면 간선 채택
			if(!isSameParent(edge.node1, edge.node2)) {
				//두 구역의 부모 설정
				setParent(edge.node1, edge.node2);
				//간선 개수 1 증가
				count++;
				//간선 길이 추가
				sumLength+=edge.length;
				//간선의 개수가 N-1이라면 리턴
				if(count==countDivision-1) return sumLength;
			}
		}
		//간선 개수가 충분하지 않음으로 구성 불가
		return -1;
	}
	
	
	public static int minBridge() {
		//각 구역에 구역번호 지정하고 최대 구역번호 리턴(구역은 2부터 시작)
		int maxDivision = islandDivision();
		//division 개수
		int countDivision = maxDivision-1;
		//kruskal 알고리즘으로 최소 신장 트리 길이 출력
		return kruskalMst(maxDivision, countDivision);
	}
	
	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());
		Board=new int[N][M];
		for(int i=0;i<N;i++) {
			st=new StringTokenizer(br.readLine());
			for(int j=0;j<M;j++) {
				Board[i][j]=Integer.parseInt(st.nextToken());
			}
		}
		System.out.println(minBridge());
		br.close();
	}
}

 

반응형

댓글