Thief of Wealth
article thumbnail
Published 2020. 10. 6. 23:05
17472 다리만들기2 개발/알고리즘

문제

섬으로 이루어진 나라가 있고, 모든 섬을 다리로 연결하려고 한다. 이 나라의 지도는 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

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

입력

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

출력

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

 

 

 

그림이 짤리지만 문제를 읽으면 모두 이해갈 것이다.

 

처음에는 접근 방법을 몰라서 고민하다가 풀이를 잠깐 보았다.

 

핵심은 다음과 같다.

섬을 찾고 -> 라벨링을하고 -> 각 좌표에서 상,하,좌,우로 쭉 뻗어서 다리를 만들고 -> 크루스칼 알고리즘을 돌린다.

 

1. 섬을 찾는다.

bfs를 이용해서 찾는다.

 

2. 라벨링

섬의 좌표들에 같은 섬끼리 번호를 붙여준다.

 

3. 다리는 1개의 방향이므로 각 섬의 좌표에서 상,하,좌,우로 쭉 뻗는다.

이때 다른섬이면 다리를 추가하고 (길이는 2이상)

맵밖으로 나가면 종료하고

아니면 다리를 연장한다. (이때 자신의 섬을 가로질러갈 수 있으므로 자신의 섬일때는 길이를 0으로 초기화해줘야 한다)

 

4. 크루스칼 알고리즘을 돌린다.

이때, 모든 스패닝트리가 연결되었는지 확인하기 위해서, 모든 섬의 루트노드를 확인하는 과정이 꼭 필요하다.

 

자잘한 구현상의 실수 떄문에 반례를 찾아가면서 힘겹게 풀었다.

그래도 힌트를 조금씩 얻어가면서 스스로 문제를 해결해나가는 이러한 기분이 좋은것 같다.

 

import sys
from collections import deque

input = sys.stdin.readline

n, m = map(int, input().rstrip().split(" "))

board = []
for _ in range(n):
    board.append(list(map(int, input().rstrip().split(" "))))

# 상,하,좌,우
direction_x = [-1, 1, 0, 0]
direction_y = [0, 0, -1, 1]

# 순회하면서 섬의 영역을 찾고 좌표를 저장한다.


def find_islands():
    global board, direction_x, direction_y
    visited = [[0]*(m) for _ in range(n)]

    q = deque()

    islands = []

    for row in range(n):
        for col in range(m):
            # 순회하다가 섬의 시작점으로 추정되는 곳에서 시작
            if(board[row][col] == 1 and visited[row][col] == 0):
                islands.append([])  # 새로운 섬 넣기
                q.append((row, col))
                visited[row][col] = 1

                while(len(q) != 0):
                    x, y = q.popleft()

                    islands[-1].append((x, y))  # 섬 좌표 1개씩 넣기
                    for i in range(4):
                        new_x = x+direction_x[i]
                        new_y = y+direction_y[i]

                        if(new_x >= 0 and new_x <= n-1 and new_y >= 0 and new_y <= m-1):
                            # 바다거나 방문한곳이면 패쓰
                            if(visited[new_x][new_y] == 0
                               and board[new_x][new_y]):
                                q.append((new_x, new_y))
                                visited[new_x][new_y] = 1

    return islands


# board의 섬좌표에 각각의 섬의 이름을 대입한다.
def naming_islands(islands):
    for idx, points in enumerate(islands):
        for point in points:
            x, y = point
            board[x][y] = idx+1  # 1부터 섬이름 붙임

# 섬의 각 좌표에서 상,하,좌,우로 직선을 그어 섬에 도달하는 다리를 만들고 저장한다.


def make_bridge(islands):
    global board, direction_x, direction_y
    graph = []

    for points in islands:
        for point in points:
            # 상,하,좌,우 로 직진

            for i in range(4):
                new_x, new_y = point
                curr_x, curr_y = point
                dist = 0
                while(1):
                    new_x = new_x+direction_x[i]
                    new_y = new_y+direction_y[i]

                    # 맵 벗어나면 종료
                    if(
                        new_x < 0 or new_x > n-1 or new_y < 0 or new_y > m-1
                    ):
                        break
                    # 다른 섬에 도착하면 종료
                    elif(
                        board[new_x][new_y] != 0 and  # 육지면서
                        (new_x, new_y) not in points  # 현재 섬에 포함되지 않는 => 다른섬
                    ):
                        # 다리의 최소길이는 2이상이어야함
                        if(dist >= 2):
                            graph.append(
                                (board[curr_x][curr_y],
                                 board[new_x][new_y], dist)
                            )
                        break
                    else:
                        # 바다면 다리길이 추가
                        if(board[new_x][new_y] == 0):
                            dist += 1

                        elif(board[new_x][new_y] >= 1):
                            # '''
                            # 아래처럼 0을 2번지나가는 경우까지 고려
                            # 1 1 1
                            # 1 0 1
                            # 0 1 1
                            # 0 0 0
                            # 0 1 0
                            # '''
                            dist = 0

    return graph


# 루트노드를 찾는 함수(재귀)


def find_root_nodes(x):
    global parents
    if(parents[x] != x):
        parents[x] = find_root_nodes(parents[x])
    return parents[x]


# union


def union(a, b):
    global parents
    a = find_root_nodes(a)
    b = find_root_nodes(b)
    if(a < b):
        parents[b] = a
    else:
        parents[a] = b


islands = find_islands()
naming_islands(islands)
graph = make_bridge(islands)
graph = sorted(graph, key=lambda x: x[2])

# 크루스칼 용 parent
parents = [0]*(1 + len(islands))
# 자기자신을 부모로
for i in range(1+len(islands)):
    parents[i] = i

answer = 0

for edge in graph:
    start, end, d = edge
    if(find_root_nodes(start) != find_root_nodes(end)):
        union(start, end)
        answer += d

# 모든 섬의 루트노드가 같은지 체크
allConnected = True
temp = find_root_nodes(1)
for i in range(1, len(islands)+1):
    if(temp != find_root_nodes(i)):  # 부모가 다른 노드가 있다면 스패닝트리로 합쳐지지 않은 것
        allConnected = False

if(allConnected == False or answer == 0):
    print(-1)
else:
    print(answer)

# for i in board:
#     print(" ".join(map(str, i)))
# print(graph)

# print(parents)
# print(answer)

'개발 > 알고리즘' 카테고리의 다른 글

10815 숫자 카드  (0) 2020.10.07
6497 전력난  (0) 2020.10.07
2887 행성 터널  (0) 2020.10.05
1647 도시 분할 계획  (0) 2020.10.05
1197 최소 스패닝 트리  (0) 2020.10.05
profile on loading

Loading...