프로그래머스의 네트워크 문제를 함께 풀어보도록 하겠습니다.
문제
네트워크란 컴퓨터 상호 간에 정보를 교환할 수 있도록 연결된 형태를 의미합니다. 예를 들어, 컴퓨터 A와 컴퓨터 B가 직접적으로 연결되어있고, 컴퓨터 B와 컴퓨터 C가 직접적으로 연결되어 있을 때 컴퓨터 A와 컴퓨터 C도 간접적으로 연결되어 정보를 교환할 수 있습니다. 따라서 컴퓨터 A, B, C는 모두 같은 네트워크 상에 있다고 할 수 있습니다.
컴퓨터의 개수 n, 연결에 대한 정보가 담긴 2차원 배열 computers가 매개변수로 주어질 때, 네트워크의 개수를 return 하도록 solution 함수를 작성하시오.
- 컴퓨터의 개수 n은 1 이상 200 이하인 자연수입니다.
- 각 컴퓨터는 0부터 n-1인 정수로 표현합니다.
- i번 컴퓨터와 j번 컴퓨터가 연결되어 있으면 computers[i][j]를 1로 표현합니다.
- computer[i][i]는 항상 1입니다.
예제
Input:
n: 3
computers: [[1, 1, 0], [1, 1, 0], [0, 0, 1]]
Output: 2
Input:
n: 3
computers: [[1, 1, 0], [1, 1, 1], [0, 1, 1]]
Output: 1
풀이 1
문제를 풀기 전에 주어진 첫 번째 예제에서 네트워크의 수가 왜 2인지를 먼저 같이 생각해볼까요?
주어진 이차원 배열을 좀 더 알아보기 쉽게 여러 줄에 표시해보겠습니다.
[1, 1, 0]
[1, 1, 0]
[0, 0, 1]
그리고 이 것을 시각화해 보면 다음과 같은 모습이 될 것입니다.
0 - 1 👉 첫 번째 네트워크
2 👉 두 번째 네트워크
각 컴퓨터를 정점(Vertex, Node)으로 보고 컴퓨터 간의 연결을 간선(Edge)으로 보면 이 문제는 전형적인 그래프(Graph) 문제라는 것을 알 수 있는데요.
computers
입력 배열은 자연스럽게 그래프를 표현하는 인접 행렬(adjacency matrix)이 되고요.
각 컴퓨터를 출발점으로 해서 그래프를 탐색을 하다가 더 이상 연결된 컴퓨터가 없으면 네트워크를 하나를 발견한 것이라고 볼 수 있겠습니다.
여기서 주의할 부분은 한 번 방문한 컴퓨터는 절대 재방문해서는 안 된다는 것입니다. 그렇지 않으면 네트워크 안에서 무한 루프에 빠질 수 있기 때문입니다. 예를 들어, 위의 첫 번째 네트워크를 보시면 컴퓨터 0과 컴퓨터 1 사이를 계속 왕복할 것입니다.
이럴 때는 집합(Set) 자료구조를 사용하면 효과적으로 재방문을 방지할 수 있습니다.
컴퓨터를 추가하거나 어떤 컴퓨터가 있는지 확인하는데 모두 O(1)
의 시간이 걸리기 때문입니다.
그럼 설명드린 깊이 우선 탐색(DFS, Depth First Search) 알고리즘을 재귀 함수를 사용해서 파이썬으로 구현해보겠습니다.
def solution(n, computers):
count = 0
visited = set()
def dfs(node):
visited.add(node)
for nei in range(n):
if nei not in visited and computers[node][nei]:
dfs(nei)
for start in range(n):
if start not in visited:
dfs(start)
count += 1
return count
첫 번째 예제를 기준으로 위 코드를 돌려보면 다음과 같은 구조로 재귀 함수가 실행이 되어 결과로 2
를 반환하게 될 것 입니다.
start = 0
dfs(0)
node = 0
visited = {0}
nei = 0
nei = 1
dfs(1)
node = 1
visited = {0, 1}
nei = 0
nei = 1
nei = 2
nei = 2
count = 1
start = 1
start = 2
dfs(2)
node = 2
visited = {0, 1, 2}
nei = 0
nei = 1
nei = 2
count = 2
n
을 컴퓨터의 개수라고 했을 때, 이 풀이의 시간 복잡도는 O(n^2)
입니다.
중복 방문을 막고 있기 때문에 재귀 함수가 각 컴퓨터를 상대로 딱 한 번씩만 호출되는 것을 볼 수 있고요.
각 재귀 함수 안에서 n
번의 반복이 일어나는 루프가 돌기 때문입니다.
반면에 공간 복잡도는 집합에는 n
개의 컴퓨터를 저장해야 하기 때문에 O(n)
이 됩니다.
풀이 2
조금 더 생각을 해보면 메모리 사용량을 좀 줄일 수 있지 않을까요?
문제의 제한 사항 중에서 computer[i][i]
은 항상 1
이 저장되어 있다고 했으니까, 그 위치에 컴퓨터 방문 여부를 저장한는 것입니다.
그러면 굳이 추가적으로 집합 자료구조를 사용하지 않고도 입력 배열만으로 이 문제를 해결할 수 있을 것 같습니다.
컴퓨터를 방문했을 경우, 그 위치에 1
대신에 0
을 저장하고, 탐색 전에 computer[i][i]
를 확인하도록 코드를 수정해보겠습니다.
def solution(n, computers):
count = 0
def dfs(node):
computers[node][node] = 0
for nei in range(n):
if computers[nei][nei] and computers[node][nei]:
dfs(nei)
for start in range(n):
if computers[start][start]:
dfs(start)
count += 1
return count
그런데 이렇게 메모리 최적화를 해주면 과연 공간 복잡도가 향상될까요?
아쉽게도 재귀 함수의 호출 스택의 깊이가 최대 n
이 되므로 빅오 계신법을 따르는 공간 복잡도에는 변화가 없습니다.
그럼에도 불구하고 코드가 좀 더 간단해지고, 추가 자료구조를 쓰지 않기 때문에 더 좋은 풀이이라고 볼 수 있겠습니다.
풀이 3
깊이 우선 탐색은 재귀 알고리즘 대신에 스택(Stack) 자료구조를 사용해서 많이 구현하죠? 반복 알고리즘을 사용하면 스택 오버플로우(Stack Overflow) 문제를 피할 수 있기 때문에 실무에서 선호합니다.
그럼 동일한 알고리즘을 스택을 사용해서 반복 알고리즘으로 구현해보겠습니다.
def solution(n, computers):
count = 0
for start in range(n):
if computers[start][start]:
count += 1
stack = [start]
while stack:
node = stack.pop()
computers[node][node] = 0
for nei in range(n):
if computers[nei][nei] and computers[node][nei]:
stack.append(nei)
return count
최대 n
개의 컴퓨터가 스택에 저장될 수있기 때문에 역시 공간 복잡도는 O(n)
으로 유지됩니다.
마치면서
이 문제는 그래프 탐색의 기초를 다치는데 매우 도움이 되는 문제라고 생각합니다. 코딩 테스트에서 그래프를 어떻게 다루는지에 대해서 좀 더 공부하고 싶으시다면 관련 게시물을 참고 바랄께요.