ML/AI/SW Developer

BAEK17472 다리 만들기 2(python)

1. 문제 설명

  • 문제 링크
  • N×M 이차원 격자
    • 격자는 땅(1)이나 바다(0)로 이루어져 있다
  • 다리 건설 조건
    1. 바다에만 건설 가능
    2. 다리의 길이: 격자에서 다리가 차지하는 칸수
    3. 중간에 방향이 바뀌면 안됨, 수직, 수평으로만 설치 가능
      • 수직, 수평 양 끝은 바다가 아니라 섬이어야 한다.
    4. 다리 길이는 2이상이어야 한다.
    5. 모든 섬을 연결해야 한다.
첫째 줄에 지도의 세로 크기 N과 가로 크기 M이 주어진다. 
둘째 줄부터 N개의 줄에 지도의 정보가 주어진다. 
각 줄은 M개의 수로 이루어져 있으며, 수는 0 또는 1이다. 0은 바다, 1은 땅을 의미한다.

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

2. 풀이

  1. BFS를 활용해 섬에 고유번호 부여하기
    • make_island: grid 배열에 고유 번호 표시
  2. 브루트포스 활용 그래프 생성
    • 섬: node, 후보 다리: edge
    • make_graph: 2차원 배열로 graph 표현
      • 각 값은 최소 다리 길이
      • 굳이 긴 다리를 선택할 필요가 없음
  3. kruskal(크루스칼) 알고리즘 활용, 최소신장트리 생성
    • 그래프에서 최소비용의 edge만 선택해서 모든 노드 연결
      • 수행전 edge를 비용 기준으로 오름차순 정렬
      • 작은것 부터 선택 (부모 섬이 다를 때만 선택 됨으로 자연스럽게 최소 비용으로 간선들이 연결 됨)
    • find: 부모 찾기, 재귀를 활용해 root로 이동
    • union: 다리 선택시, 동일한 부모 섬으로 표시
    • kruskal: 다리 선택
  4. 모든 섬이 연결되어있는지 확인
    • find를 활용해 모든 섬의 root 섬이 같은지 확인
    • 다르면 answer = -1로 변경

3. 코드

  • python
from collections import deque

# 섬에 번호 부여하기
def make_island(x, y, grid, number, visited):
    global N
    global M
    # 탐색 방향
    dx = [1,-1,0,0]
    dy = [0,0,1,-1]

    q = deque()
    q.append([x, y])
    grid[x][y] = number
    visited[x][y] = True

    while q:
        cx, cy = q.popleft()
        
        for dir in range(4):
            nx = cx + dx[dir]
            ny = cy + dy[dir]

            # 맵밖을 벗어나지 않고
            if 0 <= nx < N and 0 <= ny < M:
                # 방문한적이 없는 곳이면
                if not visited[nx][ny] and grid[nx][ny] != 0:
                    visited[nx][ny] = True
                    grid[nx][ny] = number
                    q.append([nx, ny])

# 섬을 노드로, 다른 섬으로 갈 수 있는 다리 후보를 edge로 만들기
    # 같은 섬을 연결하는 다리의 경우에는 최소 값으로 설정
def make_graph(grid, num_of_island):
    global N
    global M
    
    # 탐색 방향 하, 상, 우, 좌
    dx = [1,-1,0,0] 
    dy = [0,0,1,-1]
    
    graph = [[100 for _ in range(num_of_island)] for __ in range(num_of_island)]

    # 탐색
    for x in range(N):
        for y in range(M):
            # 바다가 아니면
            if grid[x][y] != 0:
                # 4방위 탐색
                for dir in range(4):
                    # 연결가능 여부, 노드번호, 다리길이 반환
                    check, node, value = check_dir(dir, grid, x, y)
                    # 연결 가능하고
                    if check:
                        # 다리기리가 더 작으면 최소값으로 바꿔주기
                        if graph[grid[x][y]][node] > value:
                            graph[grid[x][y]][node] = value
                            graph[node][grid[x][y]] = value

    return graph

# 연결 가능한 섬이 있는지 검사
def check_dir(dir, grid, x, y):
    global N
    global M

    curr_island = grid[x][y] # 현재 섬 번호
    start_x = x # 탐색 시작 좌표
    start_y = y # 탐색 시작 좌표

    # 탐색 방향
    dx = [1,-1,0,0] 
    dy = [0,0,1,-1]

    check, node, value = False, -1, -1

    while True:
        # 정해진 방향으로 쭉 진행
        x = x + dx[dir]
        y = y + dy[dir]

        # 맵을 안벗어 나면 검사
        if 0 <= x < N and 0 <= y < M:  
            # 바다가 아닌 다른 섬이 나올 경우 값들을 계산 및 저장 하고 탈출!
            if grid[x][y] != 0 and grid[x][y] != curr_island:
                if abs(x - start_x) - 1 >= 2 or abs(y - start_y) - 1 >= 2:
                    check = True # 연결 가능!
                    node = grid[x][y] # 연결 될 섬 번호
                    # 다리 길이!
                        # 수평 또는 수직 으로만 움직이기 때문에 x쪽, y쪽 둘중 하나는 0이다. 그래서 큰 값으로 선택하면 다리길이
                    value = abs(x - start_x) if abs(x - start_x) > abs(y - start_y) else abs(y - start_y) 
                    return check, node, value - 1   
                else: # 길이가 2가 안되면 다리 건설 불가
                    return check, node, value
            # 섬 내부, 겹치는 방향 좌표일 경우
            elif grid[x][y] == curr_island:
                return check, node, value
        else: # 맵을 벗어나는 경우
            return check, node, value

# 부모 찾기
def find(x, parents):
    if x == parents[x]:
        return x
    else:
        parents[x] = find(parents[x], parents)
        return parents[x]

# 부모 합치기
def union(x, y, parents):
    x = find(x, parents)
    y = find(y, parents)

    # 같으면 변경 필요 없음
    if x==y: return

    # 낮은 값을 부모 노드로
    if x<y: parents[y] = x
    else: parents[x] = y

def kruskal(parents, edges):
    length = 0

    # 간선 연결
    for edge in edges:
        w, s, e = edge
        # 연결이 안되어 있는 섬이면
        if find(s, parents) != find(e, parents):
            # 연결
            union(s, e, parents)
            # 다리 길이 더해주기
            length += w
    # 다리길이 반환
    return length

def Print_grid(grid):
    print('')
    for _ in range(len(grid)):
        print(grid[_])
    print('')

# main
N, M = map(int, input().split())
grid = [list(map(int, input().split())) for _ in range(N)]

# 1. island 정보구성
visited = [[False for _ in range(M)] for __ in range(N)] # 섬 방문 여부
number = 1 # 섬 고유 번호 1~
for x in range(N):
    for y in range(M):
        # 방문하지 않았고 바다가 아니면 탐색
        if not visited[x][y] and grid[x][y] != 0:
            make_island(x, y, grid, number, visited)
            number += 1

# 2. 각 섬을 그래프 형태로 만들기(노드, 엣지 만들기)
num_of_island = number
    # 1 ~ N을 번호로 사용
graph = make_graph(grid, num_of_island)

# 3. 그래프를 이용해 최소 신장 트리 만들기
    # 크루스칼 알고리즘
parents = [_ for _ in range(num_of_island)] # 부모(그룹) 표시
edges = []
for s in range(num_of_island):
    for e in range(s+1, num_of_island):
        if graph[s][e] != 100:
            edges.append([graph[s][e], s, e])
    # edge 가중치 순으로 정렬
edges = sorted(edges, key=lambda x: x[0])
length = kruskal(parents, edges)
answer = length if length != 0 else -1

# 4. 모든 섬을 연결하지 못했으면 -1
root = find(parents[1], parents)
for idx in range(2, len(parents)):
    temp = find(parents[idx], parents)
    if root != temp:
        answer = -1
        break

print(answer)

4. 결과 및 회고


4번인 “모든 섬을 연결하지 못했으면” 조건을 처음에는 빠트리고 맞왜틀을 하며 헤맸었다. 맞왜틀 중에 혹시나 싶어 parents를 print해서 확인 해보니 모든 섬이 연결되었을 때에도 부모노드가 바로 상위 노드로만 표현되고, 최종 root로 표현되지 않는 다는 것을 알 수 있었다. Union-Find 알고리즘을 조금더 심도있게 탐구해봐야할 것 같다. 그리고 graph를 만드는 부분에서 어느정도 벡트랙킹을 하기는 했지만, 섬 내부땅(테두리가 아닌 땅) 까지 검사하기 위한 함수를 호출한다. 또한 양방향을 검사하기 때문에 동일한 엣지가 2번씩 검사가되는 문제가 존재한다. 이부분을 어떻게 개선하면 더 효율적일지 생각해봐야 할 것 같다. 또, graph를 만들고 나서 다시 edge 정보를 추출하는데, graph를 만들면서 바로 edge정보만을 저장해도 되기 때문에, 이차원 배열이 아닌 인접리스트로 표현하는 방법을 사용하면, 약간 더 효율적인 알고리즘이 될 것 같다.