Number of Provinces
n개의 도시가 있으며, 0부터 n-1까지 번호가 매겨져 있습니다. 행의 목록으로 주어진 n × n 행렬 isConnected가 있습니다. isConnected[i][j]는 도로가 도시 i와 도시 j를 직접 연결하면 1이고, 연결하지 않으면 0입니다. 도로는 양방향으로 통행할 수 있으므로 행렬은 대칭이며, 모든 도시는 자기 자신과 연결된 것으로 간주합니다.
성(省)이란 서로 직접 또는 다른 도시를 거쳐 도달할 수 있고, 그룹 밖으로 이어지는 도로가 없는 도시들의 집합입니다. 성의 개수를 반환하세요.
함수
- isConnectedinteger-2d-array
- n × n 행렬로, 두 도시를 직접 연결하는 도로가 있으면 1
- 반환값integer
- 주의 개수
제약 조건
1 ≤ n ≤ 150, 여기서n = isConnected.lengthisConnected[i].length = nisConnected[i][j]는0또는1입니다isConnected[i][i] = 1isConnected[i][j] = isConnected[j][i]
예제
- 입력
- isConnected = [[1, 0, 0, 1], [0, 1, 1, 0], [0, 1, 1, 0], [1, 0, 0, 1]]
- 출력
- 2
- 설명
- 도시 0은 도시 3으로 이어지는 도로가 있고, 도시 1은 도시 2로 이어지는 도로가 있습니다. 두 쌍 사이를 연결하는 도로가 없으므로, 지방은 2개입니다.
- 입력
- isConnected = [[1, 1, 0, 0, 0], [1, 1, 1, 0, 0], [0, 1, 1, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 1]]
- 출력
- 3
- 설명
- 도시 0과 2 사이에는 도로가 없지만, 두 도시 모두 도시 1로 연결된 도로가 있으므로 도시 0, 1, 2는 하나의 주를 이룹니다. 도시 3과 4에는 도로가 전혀 없으며, 각각 하나의 주를 이루므로 총 3개입니다.
제출 시 숨은 테스트 +15개
후속 질문
이제 각 도로는 정해진 날짜에 개통됩니다. 모든 도시가 하나의 주에 속하게 되는 첫날을 찾을 수 있나요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
각 도시를 점으로 그리고 대각선에서 벗어난 각
1을 두 점 사이의 선으로 그려 보세요. 그 그림에서 하나의 성은 어떤 모습일까요?주는 연결 요소입니다. 두 도시 사이에 0이 있다고 해서 두 도시가 연결되어 있지 않다는 뜻은 아닙니다. 제3의 도시가 두 도시를 연결할 수 있기 때문입니다. 이전 검색이 도달하지 못한 도시에서 새 검색을 시작해야 하는 횟수를 세세요.
또 다른 방법: 도시마다 하나씩,
n개의 그룹으로 시작하고 대각선 위에 있는 모든 1에 대해i와j의 그룹을 합칩니다. 서로 다른 두 그룹을 합치면 그룹 수가 하나 줄어듭니다. 경로 압축을 사용하는 유니온 파인드 자료 구조를 사용하면 각 병합의 시간 복잡도를 거의 상수 시간으로 만들 수 있습니다.
풀이
이 행렬은 무방향 그래프의 인접 행렬입니다. 도시는 노드이며, 행 i와 열 j에 있는 1은 간선을 나타냅니다. 하나의 주(province)는 연결 요소이므로, 정답은 연결 요소의 개수입니다. 주의할 점은 세 번째 도시를 거쳐 연결될 수 있다는 것입니다. 두 도시 사이의 값이 0이라고 해서 서로 다른 주에 속하는 것은 아닙니다. 아직 방문하지 않은 각 도시에서 탐색을 시작하거나, 모든 간선의 양 끝을 합치는 유니온 파인드를 사용하면 행렬 자체의 크기인 O(n²) 시간에 연결 요소의 개수를 셀 수 있습니다.
방문하지 않은 모든 도시에서 시작하는 깊이 우선 탐색
핵심 아이디어
도시를 순서대로 살펴보세요. 이전 탐색에서 표시하지 않은 도시를 만나면, 각 탐색이 해당 성의 모든 도시를 표시하므로 이미 센 성에 속할 수 없습니다. 따라서 개수를 1 늘린 다음, 이 도시에서 도달할 수 있는 모든 도시를 표시하세요.
도시들을 찾으려면 스택을 사용하세요. 도시를 꺼내 행렬에서 해당 도시의 행을 읽고, 그 행에서 값이 1이며 아직 표시되지 않은 모든 도시를 스택에 넣으면서 표시하세요. 두 번째 예제에서는 도시 0에서 시작한 탐색이 도시 1을 스택에 넣고, 이어서 도시 1의 행이 도시 2를 추가합니다. 행 0에서 도시 2의 값이 0이어도 마찬가지입니다. 이런 식으로 각 행을 따라가야 다른 도시를 거쳐서만 연결된 도시도 찾을 수 있습니다.
각 도시는 한 번씩 꺼내지며, 도시를 꺼낼 때마다 n개의 항목이 있는 해당 행을 읽으므로 전체 시간 복잡도는 O(n²)입니다. 행렬을 한 번 읽기 때문입니다. 표시 정보와 스택에는 최대 n개의 도시가 들어가므로 추가 공간 복잡도는 O(n)입니다.
재귀 탐색은 더 간결하게 읽히지만, 하나의 긴 선 모양으로 된 성에서는 호출이 도시마다 한 단계씩 중첩됩니다. n = 150일 때는 안전하지만, 노드가 10^5개인 그래프에서 같은 코드를 실행하면 호출 스택이 넘칩니다. 그러므로 명시적인 스택을 사용하는 습관을 들이는 것이 좋습니다.
알고리즘
- 각 도시에 대한 방문 여부 플래그를 만들고 개수를 0으로 설정합니다.
- 도시들을 순서대로 확인하고 이미 방문한 도시는 건너뜁니다.
- 방문하지 않은 도시에서는 개수에 1을 더하고, 해당 도시를 방문한 것으로 표시한 다음 스택에 넣습니다.
- 스택에 도시가 있는 동안 하나를 꺼내고, 해당 도시의 행에서 값이 1이며 아직 방문하지 않은 도시를 모두 스택에 넣습니다. 넣으면서 방문한 것으로 표시합니다.
- 개수를 반환합니다.
def findCircleNum(isConnected):
n = len(isConnected)
seen = [False] * n
provinces = 0
for start in range(n):
if seen[start]:
continue
# Nobody reached this city from an earlier province, so it starts a new one.
provinces += 1
seen[start] = True
stack = [start]
while stack:
city = stack.pop()
row = isConnected[city]
for other in range(n):
# Mark a city when you push it, so it is never pushed twice.
if row[other] == 1 and not seen[other]:
seen[other] = True
stack.append(other)
return provinces경로 압축과 랭크 기반 합치기를 사용하는 유니온 파인드
핵심 아이디어
질문을 뒤집어 생각해 보세요. 도시마다 하나씩, n개의 지역에서 시작합니다. 행렬의 1은 두 도시가 같은 그룹에 속한다는 뜻입니다. 두 도시가 아직 서로 다른 그룹에 있다면 그룹을 합치고, 개수는 1 감소합니다. 마지막 도로를 처리한 뒤의 개수가 답입니다. 행렬은 대칭이고 대각선은 도시를 자기 자신과 연결하므로 대각선 위쪽의 항목만 확인하면 됩니다. 두 번째 예시에서는 개수가 5에서 시작합니다. (0, 1)의 1은 도시 0과 1을 합칩니다(4개 남음). (1, 2)의 1을 처리할 때는 도시 1이 도시 0의 그룹에 속한다는 것을 확인하고 도시 2를 그 그룹에 합칩니다(3개 남음). 도시 3과 4는 대각선 위쪽에 1이 없으므로 답은 3입니다.
서로소 집합 합집합이라고도 하는 유니온 파인드는 각 그룹을 트리로 저장합니다. parent[c]는 한 단계 위를 가리키며, 부모가 자기 자신인 꼭대기의 도시는 해당 그룹의 루트입니다. find가 두 도시를 각각 위로 따라가 같은 루트에 도달할 때, 두 도시는 같은 그룹에 속합니다. 두 그룹을 합치려면 한쪽 루트가 다른 쪽 루트를 가리키게 합니다.
두 가지 규칙으로 트리를 평평하게 유지합니다. 랭크에 따른 합치기는 짧은 트리를 긴 트리 아래에 붙입니다. 따라서 높이가 h인 트리에는 최소 2^h개의 도시가 있고, 어떤 경로도 log n보다 길지 않습니다. 경로 압축은 한 단계 더 나아갑니다. find가 루트를 찾으면, 거쳐 간 모든 도시가 그 루트를 곧바로 가리키도록 하므로 다음에는 그 도시들 중 어느 곳에서 조회하더라도 한 단계만 걸립니다. 두 규칙을 모두 적용하지 않으면, 운이 나쁜 순서로 긴 사슬의 도시들을 합칠 때 트리가 하나의 경로가 되어 모든 find가 O(n)단계를 거칩니다.
두 규칙을 모두 적용하면 각 find의 분할 상환 비용은 O(α(n))입니다. α는 역 아커만 함수이며, 컴퓨터가 저장할 수 있는 어떤 n에 대해서도 4를 넘지 않습니다. 행렬을 읽는 데 여전히 O(n²)이 걸리므로 이것이 전체 시간 복잡도이며, 부모 배열과 랭크 배열은 O(n) 공간을 차지합니다. 도로가 한 번에 하나씩 추가될 때 이 자료 구조의 장점이 드러납니다. 새 도로가 추가될 때마다 다시 탐색하지 않고도 개수를 최신 상태로 유지합니다.
알고리즘
- 모든 도시에 대해
parent[c] = c와rank[c] = 0으로 설정하고, 개수는n으로 설정합니다. isConnected[i][j] = 1인 모든i < j쌍에 대해i와j의 루트를 찾습니다.find에서는 루트까지 올라간 다음, 같은 경로를 다시 따라가며 경로상의 각 도시가 루트를 직접 가리키도록 합니다.- 루트가 서로 다르면 순위가 낮은 루트를 다른 루트 아래에 연결하고, 순위가 같으면 순위에 1을 더한 다음 개수에서 1을 뺍니다.
- 개수를 반환합니다.
def findCircleNum(isConnected):
n = len(isConnected)
parent = list(range(n))
rank = [0] * n
def find(city):
root = city
while parent[root] != root:
root = parent[root]
# Path compression: point every city on the way straight at the root.
while parent[city] != root:
up = parent[city]
parent[city] = root
city = up
return root
provinces = n
for i in range(n):
for j in range(i + 1, n): # the matrix is symmetric, so the upper half is enough
if isConnected[i][j] == 1:
a, b = find(i), find(j)
if a == b:
continue
# Union by rank: hang the shorter tree under the taller one.
if rank[a] < rank[b]:
a, b = b, a
parent[b] = a
if rank[a] == rank[b]:
rank[a] += 1
# Two provinces just became one.
provinces -= 1
return provinces
함정과 경계 사례
오답 중 대부분은 0을 두 도시가 서로 연결되어 있지 않다는 증거로 취급하거나, 연결 요소가 아닌 다른 것을 셉니다.
- 직접 연결된 도로만 확인하기. 두 번째 예제의 도시 0과 2 사이에는 0이 있지만, 도시 1을 통해 여전히 같은 주에 속합니다. 직접 연결된 도로만으로 계산하면 이를 놓칩니다. 예를 들어 서로 다른 행의 수를 세면, 이 경우 3이 아니라 5가 됩니다.
- 1의 개수를 세고 2로 나누기. 이는 주가 아니라 도로의 개수를 셉니다. 서로 모두 연결된 도시 3개에는 도로가 3개 있지만 주는 1개입니다.
- 유니온 파인드에서 두 루트가 서로 다를 때만이 아니라 1이 나올 때마다 개수를 줄이기. 이미 병합된 그룹 내의 도로는 개수를 바꾸면 안 됩니다.
- 루트가 아니라 부모를 비교하기. 트리에서 한 도시의 깊이가 더 깊으면 같은 그룹에 속한 두 도시라도
parent[i] == parent[j]는 거짓일 수 있습니다. 항상find(i)와find(j)를 비교하세요. parent[j] = find(i)처럼 도시j자체를 연결하고 루트를 연결하지 않기.j가 이미 그룹에 속해 있었다면, 나머지 그룹은 병합에서 분리됩니다.- 큰 그래프에서 재귀 사용하기. 재귀 탐색이나 유니온 바이 랭크를 사용하지 않는 재귀
find는 사슬 모양 그래프에서 도시마다 한 단계씩 내려갑니다. 도시가 150개일 때는 괜찮지만 10^5개일 때는 스택 오버플로가 발생합니다.
자주 묻는 질문4
Number of Provinces의 시간 복잡도는 무엇인가요?
그래프 탐색이나 유니온 파인드 모두 n × n 행렬의 모든 항목을 한 번씩 읽으므로 O(n²)입니다. 유니온 파인드에는 역 아커만 함수인 α(n)이라는 계수가 추가되며, 실제 입력에서는 항상 4 이하입니다. 추가 공간은 방문 여부 플래그에 O(n)을 사용하거나, 부모 및 랭크 배열에 O(n)을 사용합니다.
Number of Provinces 문제에는 DFS, BFS, 유니온 파인드 중 무엇을 사용해야 할까요?
세 가지 모두 O(n²) 시간에 같은 개수를 반환합니다. 행렬 전체가 한 번에 주어질 때는 DFS나 BFS를 작성하는 것이 가장 간단합니다. 도로가 하나씩 추가되거나 두 도시가 같은 주에 속하는지도 답해야 하는 경우에는 유니온 파인드가 더 적합합니다. 새로운 탐색을 하지 않고도 각 도로와 각 질문을 거의 상수 시간에 처리하기 때문입니다.
union-find에서 경로 압축과 랭크 기반 합치기는 어떤 역할을 하나요?
랭크에 의한 합집합은 두 그룹을 합칠 때 더 짧은 트리를 더 높은 트리 아래에 붙여, 모든 트리의 높이를 최대 log n으로 유지합니다. 경로 압축은 find가 지나가는 모든 노드가 루트를 직접 가리키게 하므로, 이후 해당 노드에서 조회할 때는 한 단계만 거치면 됩니다. 두 기법을 함께 사용하면 m번의 연산으로 이루어진 모든 시퀀스의 비용은 O(m α(n))이며, 이는 선형 시간과 비슷하게 작동합니다.
프로빈스의 수는 섬의 수와 어떻게 다른가요?
둘 다 연결 요소의 개수를 셉니다. Number of Islands에서는 그래프가 격자이며 각 칸에는 최대 네 개의 이웃이 있고, 작업량은 O(rows × cols)입니다. 여기서는 그래프가 인접 행렬로 주어집니다. 어떤 도시든 다른 어떤 도시와 연결될 수 있으며, 한 도시의 이웃을 나열하려면 n개의 항목으로 이루어진 전체 행을 읽습니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def findCircleNum(isConnected):
# 여기에 코드를 작성하세요케이스 1
케이스 2
입력
isConnected = [[1, 0, 0, 1], [0, 1, 1, 0], [0, 1, 1, 0], [1, 0, 0, 1]]
기대값
2