Number of Provinces
Có n thành phố, được đánh số từ 0 đến n-1. isConnected[i][j] là 1 khi có đường nối trực tiếp thành phố i với thành phố j, và là 0 trong trường hợp còn lại. Đường đi theo cả hai chiều, nên ma trận đối xứng, và mỗi thành phố đều nối với chính nó.
Tỉnh là một nhóm thành phố mà tất cả đều có thể đi đến nhau, trực tiếp hoặc qua các thành phố khác, và không có con đường nào dẫn ra khỏi nhóm. Hãy trả về số tỉnh.
Hàm
- isConnectedinteger-2d-array
- ma trận n × n, giá trị 1 khi có đường nối trực tiếp hai thành phố
- Trả vềinteger
- số tỉnh
Ràng buộc
1 ≤ n ≤ 150, trong đón = isConnected.lengthisConnected[i].length = nisConnected[i][j]là0hoặc1isConnected[i][i] = 1isConnected[i][j] = isConnected[j][i]
Ví dụ
- Đầu vào
- isConnected = [[1, 0, 0, 1], [0, 1, 1, 0], [0, 1, 1, 0], [1, 0, 0, 1]]
- Đầu ra
- 2
- Giải thích
- Thành phố 0 có đường đến thành phố 3, còn thành phố 1 có đường đến thành phố 2. Không có con đường nào nối hai cặp thành phố, nên có 2 tỉnh.
- Đầu vào
- 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]]
- Đầu ra
- 3
- Giải thích
- Thành phố 0 và 2 không có đường nối trực tiếp, nhưng cả hai đều nối với thành phố 1, nên các thành phố 0, 1 và 2 tạo thành một tỉnh. Thành phố 3 và 4 không có đường nối và mỗi thành phố tạo thành một tỉnh riêng: tổng cộng có 3 tỉnh.
Câu hỏi mở rộng
Mỗi con đường giờ đây mở vào một ngày nhất định. Bạn có thể tìm ngày đầu tiên mà tất cả các thành phố đều thuộc cùng một tỉnh không?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Biểu diễn mỗi thành phố bằng một dấu chấm và mỗi số
1nằm ngoài đường chéo bằng một đường nối hai dấu chấm. Một tỉnh sẽ trông như thế nào trong hình đó?Một tỉnh là một thành phần liên thông: số 0 nằm giữa hai thành phố không có nghĩa là chúng tách rời, vì một thành phố thứ ba có thể nối chúng với nhau. Hãy đếm số lần bạn phải bắt đầu một lượt tìm kiếm mới từ một thành phố mà các lượt tìm kiếm trước đó chưa chạm tới.
Một cách khác: bắt đầu với
nnhóm, mỗi thành phố một nhóm, rồi gộp nhóm củaivàjvới mỗi giá trị 1 phía trên đường chéo. Gộp hai nhóm khác nhau làm giảm số nhóm đi một. Cấu trúc union-find có nén đường đi giúp mỗi lần gộp gần như mất thời gian hằng số.
Lời giải
Ma trận mô tả một đồ thị vô hướng: các thành phố là nút và mỗi số 1 là một cạnh. Một tỉnh là một thành phần liên thông, và số 0 giữa hai thành phố không tách chúng ra nếu có thành phố thứ ba nối cả hai. Tìm kiếm từ mỗi thành phố chưa được thăm, hoặc dùng cấu trúc union-find cho mọi cạnh, sẽ đếm số thành phần trong O(n²).
Tìm kiếm theo chiều sâu từ mọi thành phố chưa được thăm
Ý tưởng
Một thành phố chưa được đánh dấu không thể thuộc về một tỉnh mà bạn đã tính, vì mỗi lần tìm kiếm đều đánh dấu toàn bộ tỉnh đó. Vì vậy, nó bắt đầu một tỉnh mới. Lần tìm kiếm lần lượt xét các hàng của ma trận: trong ví dụ thứ hai, lần tìm kiếm bắt đầu từ thành phố 0 đến được thành phố 1, rồi hàng của thành phố 1 dẫn đến thành phố 2, dù hàng 0 có giá trị 0 ở cột thành phố 2.
Mỗi thành phố được lấy ra một lần và đọc hàng gồm n phần tử, nên bạn đọc ma trận một lần: thời gian O(n²). Các dấu đánh dấu và ngăn xếp chứa tối đa n thành phố, nên bộ nhớ phụ là O(n).
Thuật toán
- Tạo một cờ đã thăm cho mỗi thành phố và đặt số đếm bằng 0.
- Duyệt các thành phố theo thứ tự và bỏ qua thành phố đã được đánh dấu là đã thăm.
- Với một thành phố chưa được thăm, tăng số đếm lên 1, đánh dấu thành phố đó và đẩy nó vào ngăn xếp.
- Khi ngăn xếp còn thành phố, lấy một thành phố ra rồi đẩy mọi thành phố trong hàng tương ứng của nó có giá trị 1 và chưa được thăm vào ngăn xếp, đồng thời đánh dấu từng thành phố khi đẩy vào.
- Trả về số đếm.
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 provincesUnion-find với nén đường đi và hợp nhất theo hạng
Ý tưởng
Bắt đầu với n tỉnh, mỗi tỉnh tương ứng với một thành phố. Mỗi số 1 phía trên đường chéo cho biết hai thành phố thuộc cùng một nhóm: nếu hai thành phố thuộc các nhóm khác nhau, hãy gộp chúng và giảm số lượng đi một. Trong ví dụ thứ hai, số lượng bắt đầu là 5, (0, 1) gộp thành phố 0 và 1, (1, 2) đưa thành phố 2 vào nhóm của họ, và còn lại 3 tỉnh.
Cấu trúc union-find lưu mỗi nhóm dưới dạng một cây: parent[c] trỏ lên một cấp, và hai thành phố thuộc cùng một nhóm khi find lần theo cả hai đến cùng một nút gốc. Nếu không tối ưu, các lần gộp có thể tạo thành một đường đi dài mà mỗi lần find phải đi qua O(n) bước. Gộp theo hạng treo cây thấp hơn bên dưới cây cao hơn, nhờ đó không có đường đi nào dài hơn log n. Nén đường đi trỏ thẳng mỗi thành phố mà find đi qua đến nút gốc. Kết hợp lại, chúng giúp mỗi lần find có độ phức tạp khấu hao O(α(n)), và trong thực tế α(n) không vượt quá 4.
Thuật toán
- Đặt
parent[c] = cvàrank[c] = 0cho mỗi thành phố, rồi đặt số lượng làn. - Với mỗi cặp
i < jthỏa mãnisConnected[i][j] = 1, tìm gốc củaivàj. - Trong
find, đi lên đến gốc, rồi đi lại theo đường đi và trỏ mỗi thành phố trên đường đó đến gốc. - Nếu hai gốc khác nhau, gắn gốc có hạng thấp hơn bên dưới gốc còn lại, tăng hạng thêm 1 nếu hai hạng bằng nhau, rồi giảm số lượng đi 1.
- Trả về số lượng.
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
Lỗi thường gặp và trường hợp biên
- Chỉ dựa vào các con đường trực tiếp. Thành phố 0 và 2 trong ví dụ thứ hai có giá trị 0 giữa chúng nhưng vẫn thuộc cùng một tỉnh.
- Đếm các giá trị 1 rồi chia cho hai. Ba thành phố đều nối với nhau có ba con đường và tạo thành một tỉnh.
- Trong cấu trúc union-find, giảm số đếm mỗi khi gặp giá trị 1 thay vì chỉ giảm khi hai gốc khác nhau.
- So sánh
parent[i]vớiparent[j]. Hai thành phố trong cùng một nhóm có thể có giá trị parent khác nhau khi một thành phố nằm sâu hơn; hãy so sánhfind(i)vớifind(j). - Gắn chính thành phố
j, như trongparent[j] = find(i). Nếujđã thuộc một nhóm, những thành phố còn lại trong nhóm đó sẽ không được gộp.
Câu hỏi thường gặp3
Độ phức tạp thời gian của bài toán Number of Provinces là gì?
O(n²) với tìm kiếm trên đồ thị hoặc cấu trúc hợp nhất tìm kiếm, vì cả hai đều đọc mỗi phần tử của ma trận n × n một lần. Cấu trúc hợp nhất tìm kiếm thêm hệ số α(n), hàm Ackermann nghịch đảo, trong thực tế tối đa là 4. Bộ nhớ phụ là O(n).
Bạn nên dùng DFS, BFS hay union-find cho bài toán số tỉnh thành?
Cả ba cách đều trả về cùng một số lượng trong thời gian O(n²). DFS hoặc BFS là cách viết ngắn nhất. Union-find là công cụ phù hợp hơn khi bạn cần trả lời nhiều câu hỏi dạng “hai thành phố này có cùng tỉnh không”, vì mỗi câu trả lời chỉ mất gần như thời gian hằng số.
Số tỉnh khác với số đảo như thế nào?
Cả hai đều đếm các thành phần liên thông. Trong bài Number of Islands, đồ thị là một lưới, mỗi ô vuông có nhiều nhất bốn ô lân cận và độ phức tạp là O(rows × cols). Ở đây, đồ thị là một ma trận kề: bất kỳ thành phố nào cũng có thể kết nối với thành phố khác, và để liệt kê các thành phố lân cận của một thành phố, ta phải đọc toàn bộ một hàng gồm n phần tử.
Bài tập tương tự
Các bài tập dùng cùng ý tưởng. Giải hai hoặc ba bài là bạn sẽ nhớ được mẫu này.
def findCircleNum(isConnected):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Đối số
isConnected = [[1, 0, 0, 1], [0, 1, 1, 0], [0, 1, 1, 0], [1, 0, 0, 1]]
Đầu ra mong đợi
2