Is Graph Bipartite
Một đồ thị có n nút, được gắn nhãn từ 0 đến n-1. Mỗi cặp [a, b] trong edges là một cạnh vô hướng nối nút a với nút b. Đồ thị có thể gồm nhiều phần tách biệt, và một số nút có thể không có cạnh nào.
Đồ thị là đồ thị hai phía (bipartite) khi bạn có thể chia các nút thành hai nhóm sao cho mỗi cạnh nối một nút thuộc nhóm này với một nút thuộc nhóm kia. Trả về true nếu đồ thị là đồ thị hai phía và false nếu không phải.
Hàm
- ninteger
- số nút, được đánh số từ 0 đến n-1
- edgesinteger-2d-array
- các cạnh vô hướng, mỗi cạnh là một cặp [a, b]
- Trả vềboolean
- true nếu có thể chia các nút thành hai nhóm sao cho mọi cạnh đều nối hai nhóm, false nếu không
Ràng buộc
2 ≤ n ≤ 1041 ≤ edges.length ≤ 5000edges[i] = [a, b]với0 ≤ a, b ≤ n-1vàa ≠ b- Không có cạnh nào xuất hiện hai lần, dù theo hướng nào.
Ví dụ
- Đầu vào
- n = 4edges = [[0, 1], [1, 2], [2, 3], [3, 0]]
- Đầu ra
- true
- Giải thích
- Đặt các nút 0 và 2 vào một nhóm, các nút 1 và 3 vào nhóm còn lại. Cả bốn cạnh đều nối hai nhóm.
- Đầu vào
- n = 3edges = [[0, 1], [1, 2], [2, 0]]
- Đầu ra
- false
- Giải thích
- Nút 0 và 1 phải thuộc các nhóm khác nhau, và nút 2 phải khác cả hai nút đó, nhưng chỉ có hai nhóm. Không thể chia một chu trình gồm ba cạnh.
- Đầu vào
- n = 6edges = [[0, 1], [2, 3], [3, 4], [4, 2]]
- Đầu ra
- false
- Giải thích
- Cạnh
[0, 1]được chia đúng cách và nút 5 không có cạnh nào, nhưng các nút 2, 3 và 4 tạo thành một tam giác. Chỉ cần một phần không đạt là đủ để đáp án làfalse.
Câu hỏi mở rộng
Khi đồ thị không phải là đồ thị hai phía, bạn có thể trả về một chu trình có độ dài lẻ làm bằng chứng, liệt kê các nút theo thứ tự không?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Đặt nút 0 vào nhóm đầu tiên. Mỗi nút láng giềng của nó phải nằm ở đâu? Còn các nút láng giềng của những nút đó thì sao?
Khi một nút của một phần liên thông đã được đặt, mọi nút khác của phần đó không còn lựa chọn nào: hai đầu của một cạnh phải nằm ở hai nhóm khác nhau. Đồ thị chỉ thất bại khi cách đặt bắt buộc này khiến cả hai đầu của một cạnh nào đó nằm trong cùng một nhóm, điều này xảy ra chính xác khi phần đó có một chu trình với độ dài lẻ.
Tô màu bằng thuật toán tìm kiếm theo chiều rộng: gán màu 0 cho nút đầu tiên của một phần, gán cho mỗi nút lân cận chưa được tô màu màu đối lập với nút hiện tại, và trả về
falsekhi một nút lân cận đã có cùng màu. Bắt đầu một lượt tìm kiếm mới từ mỗi nút vẫn chưa được tô màu để không bỏ sót phần nào.
Lời giải
Chọn một nhóm cho một nút sẽ buộc các nút còn lại trong phần liên thông của nó vào nhóm tương ứng: một nút lân cận sẽ thuộc nhóm kia. Tô màu mỗi phần bằng tìm kiếm theo chiều rộng, luân phiên hai màu; đồ thị là hai phía khi và chỉ khi không có cạnh nào có hai đầu cùng màu. Đồ thị có thể không liên thông, vì vậy mỗi nút chưa được tô màu sẽ bắt đầu một lượt tìm kiếm mới.
Thử mọi cách tách
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Thử mọi cách chia. Xem nhóm của mỗi nút như một bit, vậy n bit tạo thành một số nhị phân, và đếm từ 0 đến 2^n-1 sẽ lần lượt xét mọi cách chia đúng một lần. Với mỗi cách chia, kiểm tra mọi cạnh và trả về true nếu có cách chia khiến tất cả các cạnh đều nối giữa hai nhóm. Số cách chia tăng gấp đôi theo mỗi nút: 20 nút cho khoảng một triệu cách.
Thuật toán
- Với mỗi số từ 0 đến 2^n-1, đưa nút
ivào nhóm được xác định bởi biti. - Nếu mọi cạnh đều nối hai nhóm, trả về
true. - Nếu không có cách chia nào phù hợp, trả về
false.
def isBipartite(n, edges):
side = [0] * n # side[x] is 0 or 1: the group of node x in the current split
while True:
# Does every edge cross between the two groups?
if all(side[u] != side[v] for u, v in edges):
return True
# Move to the next split, counting in binary over side.
i = 0
while i < n and side[i] == 1:
side[i] = 0
i += 1
if i == n:
return False # all 2^n splits have been tried
side[i] = 1Union-find trên các nút lân cận của mỗi nút
Ý tưởng
Dù một nút được xếp vào nhóm nào, tất cả các nút kề với nó đều được xếp vào nhóm còn lại, vì vậy chúng cùng một nhóm. Với mỗi nút, hãy gộp tất cả các nút kề với nó vào một tập hợp union-find; khi đó, một tập hợp chứa các nút buộc phải nằm cùng một phía. Một cạnh có cả hai đầu mút trong cùng một tập hợp thì buộc phải nằm trong một nhóm, vì vậy đồ thị không phải là đồ thị hai phía.
Phép kiểm tra này phát hiện mọi trường hợp không thỏa mãn. Xung quanh một chu trình lẻ, cứ gộp hai cạnh một lần thì nút đầu tiên sẽ được nối với nút cuối cùng, còn cạnh khép kín sẽ nối hai nút đó với nhau. Nếu không có chu trình lẻ, mỗi lần gộp đều nối hai nút cách nhau một khoảng cách chẵn, nên chúng nằm cùng một phía. Nút kề đầu tiên của một nút, gọi là nút neo của nó, xác định tập hợp gồm các nút kề với nó, vì vậy bạn gộp các nút khi các cạnh lần lượt xuất hiện, không cần danh sách kề. Với phép gộp theo kích thước và kỹ thuật rút ngắn đường đi, 2m lần gộp và m lần kiểm tra có độ phức tạp O(m α(n)).
Thuật toán
- Đặt
parent[x] = xvàanchor[x] = -1cho mọi nút. - Với mỗi cạnh
[u, v], đặt đầu còn lại làm điểm neo cho mỗi đầu nếu nó chưa có điểm neo. - Gộp
vvào tập hợp chứaanchor[u], vàuvào tập hợp chứaanchor[v]. - Trả về
falsenếu có cạnh mà cả hai đầu thuộc cùng một tập hợp, nếu không thì trả vềtrue.
def isBipartite(n, edges):
parent = list(range(n)) # every node starts as its own group
size = [1] * n
anchor = [-1] * n # the first neighbour seen for each node
def find(x):
# Walk up to the root, halving the path on the way.
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(a, b):
a, b = find(a), find(b)
if a == b:
return
# Hang the smaller group under the larger one.
if size[a] < size[b]:
a, b = b, a
parent[b] = a
size[a] += size[b]
# All neighbours of a node must sit on one side: the side opposite the node.
for u, v in edges:
if anchor[u] == -1:
anchor[u] = v
if anchor[v] == -1:
anchor[v] = u
union(anchor[u], v) # v joins the other neighbours of u
union(anchor[v], u) # u joins the other neighbours of v
# An edge whose two ends were forced onto one side cannot cross.
for u, v in edges:
if find(u) == find(v):
return False
return TrueTô hai màu theo chiều rộng cho mọi phần
Ý tưởng
Mâu thuẫn là kết luận cuối cùng. Màu đầu tiên trong một phần được chọn tự do, và khi đổi màu đó thì toàn bộ phần cũng đổi màu; mọi màu khác đều do một cạnh quyết định. Tìm kiếm tô màu các nút dựa trên việc khoảng cách từ điểm bắt đầu là chẵn hay lẻ, vì vậy hai nút kề nhau chỉ có cùng màu khi phần đó có chu trình với độ dài lẻ, và khi đó không tồn tại cách chia.
Một lượt tìm kiếm chỉ xét phần của chính nó, vì vậy hãy duyệt qua tất cả các nút và bắt đầu một lượt tìm kiếm mới từ mỗi nút chưa được tô màu. Trong ví dụ thứ ba, lượt tìm kiếm từ nút 0 tô màu các nút 0 và 1. Lượt tìm kiếm từ nút 2 tô nút 2 màu 0 và các nút 3 và 4 màu 1, sau đó tìm thấy [3, 4] có màu 1 ở cả hai đầu: false. Mỗi nút được tô màu và đưa vào hàng đợi một lần, và mỗi mục trong số 2m mục của danh sách được so sánh một lần: thời gian và bộ nhớ O(n + m).
Thuật toán
- Tạo danh sách kề với mỗi cạnh được thêm vào cả hai đầu, rồi đặt màu của mọi nút thành -1.
- Lần lượt xét từng nút chưa được tô màu, gán màu 0 cho nút đó rồi đưa nó vào hàng đợi.
- Lấy một nút ra khỏi hàng đợi. Gán màu đối lập cho từng nút kề chưa được tô màu rồi đưa nút đó vào hàng đợi; nếu một nút kề có cùng màu, trả về
false. - Sau khi tô màu tất cả các phần, trả về
true.
from collections import deque
def isBipartite(n, edges):
graph = [[] for _ in range(n)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
color = [-1] * n # -1: not colored yet, otherwise 0 or 1
for start in range(n):
if color[start] != -1:
continue
# A new component: its first node can take either color.
color[start] = 0
queue = deque([start])
while queue:
node = queue.popleft()
for nxt in graph[node]:
if color[nxt] == -1:
# A neighbour must take the other color.
color[nxt] = 1 - color[node]
queue.append(nxt)
elif color[nxt] == color[node]:
return False # this edge stays inside one group
return True
Lỗi thường gặp và trường hợp biên
- Chỉ tìm kiếm từ nút 0. Ví dụ thứ ba hoạt động tốt quanh nút 0 nhưng thất bại giữa các nút 2, 3 và 4.
- Chỉ thêm mỗi cạnh vào một danh sách. Với các cạnh
[1, 0]và[2, 0], nút 0 nhận màu 0 và không thấy nút lân cận nào; sau đó nút 1 cũng nhận màu 0, và cạnh nối nó với nút 0 trông như một xung đột, dù đường đi vẫn phân tách ổn. - Tô màu một nút khi lấy nút đó ra khỏi hàng đợi. Khi đó, một nút có thể được đưa vào hàng đợi hai lần, và lần thăm thứ hai ghi đè lần đầu. Hãy tô màu nút khi thêm nó vào hàng đợi.
- Nhầm lẫn giữa đồ thị hai phía và đồ thị không chu trình. Hình vuông có chu trình và là đồ thị hai phía; chỉ các chu trình lẻ mới phá vỡ sự phân tách.
Câu hỏi thường gặp3
Độ phức tạp thời gian của Is Graph Bipartite là bao nhiêu?
Thuật toán tô màu theo chiều rộng hoặc chiều sâu chạy trong O(n + m) với m cạnh: mỗi nút được tô màu một lần, và mỗi cạnh được so sánh một lần từ mỗi đầu. Danh sách kề và màu sắc chiếm O(n + m) bộ nhớ. Cấu trúc union-find cho cùng kết quả trong O(n + m α(n)).
Tại sao một chu trình có độ dài lẻ khiến đồ thị không phải là đồ thị hai phía?
Đi quanh chu trình, các nhóm phải luân phiên: nhóm thứ nhất, nhóm thứ hai, nhóm thứ nhất, nhóm thứ hai. Với chu trình có số cạnh lẻ, sau một số lần chuyển nhóm lẻ, bạn sẽ quay lại điểm xuất phát, nên điểm xuất phát phải thuộc cả hai nhóm. Một đồ thị là hai phía khi và chỉ khi nó không có chu trình với độ dài lẻ.
Bạn nên dùng BFS hay DFS để kiểm tra xem một đồ thị có hai phía hay không?
Cả hai cách đều được: quy tắc tô màu giống nhau, chỉ khác thứ tự duyệt. Với đồ thị lớn, hãy viết DFS bằng một ngăn xếp tường minh, vì phiên bản đệ quy có thể tạo độ sâu một lần gọi hàm cho mỗi 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 isBipartite(n, edges):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
n = 4 edges = [[0, 1], [1, 2], [2, 3], [3, 0]]
Đầu ra mong đợi
true