Network Delay Time
Một mạng có n nút được đánh nhãn từ 1 đến n. Liên kết times[i] = [u, v, w] truyền tín hiệu từ nút u đến nút v trong w đơn vị thời gian, chỉ theo chiều đó.
Một tín hiệu rời nút k vào thời điểm 0 và truyền dọc theo mọi liên kết mà nó có thể đi qua. Hãy trả về thời điểm nút cuối cùng nhận được tín hiệu, hoặc -1 nếu có nút không bao giờ nhận được tín hiệu.
Hàm
- timesinteger-2d-array
- các liên kết có hướng, mỗi liên kết có dạng [u, v, w]
- ninteger
- số lượng nút
- kinteger
- nút gửi tín hiệu
- Trả vềinteger
- thời điểm nút cuối cùng nhận được tín hiệu, hoặc -1
Ràng buộc
2 ≤ n ≤ 30001 ≤ times.length ≤ 4000times[i].length = 31 ≤ u, v ≤ nvàu ≠ v0 ≤ w ≤ 100- Không có hai liên kết nào cùng có
uvàv. 1 ≤ k ≤ n
Ví dụ
- Đầu vào
- times = [[1, 2, 4], [1, 3, 1], [3, 2, 2], [2, 4, 1]]n = 4k = 1
- Đầu ra
- 4
- Giải thích
- Liên kết trực tiếp đến nút 2 sau 4 đơn vị thời gian, nhưng đường đi qua nút 3 đến nơi sau 1 + 2 = 3 đơn vị thời gian. Nút 4 nhận tín hiệu cuối cùng, sau 3 + 1 = 4 đơn vị thời gian.
- Đầu vào
- times = [[1, 2, 3], [3, 1, 2]]n = 3k = 1
- Đầu ra
- -1
- Giải thích
- Nút 2 nhận được tín hiệu tại thời điểm 3, nhưng liên kết duy nhất nối với nút 3 chạy từ 3 đến 1. Nút 3 không bao giờ nhận được tín hiệu, vì vậy đáp án là -1.
- Đầu vào
- times = [[2, 1, 5], [2, 3, 0], [3, 1, 2]]n = 3k = 2
- Đầu ra
- 2
- Giải thích
- Đường dẫn đến nút 3 mất 0, nên nút 3 nhận được tín hiệu tại thời điểm 0. Sau đó, nút 1 nhận được tín hiệu tại thời điểm 0 + 2 = 2, sớm hơn thời điểm 5 của đường dẫn trực tiếp đến nút này.
Câu hỏi mở rộng
Giả sử tín hiệu yếu dần sau khi truyền qua m liên kết. Làm thế nào để tìm thời điểm nút cuối cùng nhận được tín hiệu trong giới hạn đó?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Một nút nhận được tín hiệu sau khoảng thời gian bằng độ dài của tuyến đường nhanh nhất từ
kđến nút đó. Vì vậy, đáp án là thời gian nhanh nhất lớn nhất trong số đó, hoặc -1 nếu có nút không có tuyến đường nào.Không có liên kết nào mất thời gian âm. Vì vậy, trong số các nút chưa có thời gian cuối cùng, nút có thời gian tạm tính nhỏ nhất không thể đến nhanh hơn: mọi tuyến đường khác đến nút đó đều đi qua một nút khác trong số các nút này, mà nút đó cũng không thể được đến sớm hơn. Chốt các nút theo thứ tự đến.
Duy trì một min-heap gồm các cặp
(time, node). Lấy phần tử nhỏ nhất ra, bỏ qua nếu nút đó đã có thời gian nhỏ hơn, và thêm mọi nút lân cận có thời gian được cải thiện vào heap. Khi heap trống, lấy thời gian lớn nhất.
Lời giải
Mỗi nút nhận tín hiệu sau khoảng thời gian bằng độ dài của tuyến đường nhanh nhất từ k, vì vậy bài toán này tìm đường đi ngắn nhất từ một nguồn rồi lấy giá trị lớn nhất, hoặc trả về -1 nếu một nút không có tuyến đường nào. Thời gian của các liên kết không bao giờ âm, nên thuật toán Dijkstra chốt mỗi nút một lần. Trong phần dưới, E là times.length.
Tìm kiếm theo chiều sâu, duyệt lại mỗi khi tìm thấy đường đi nhanh hơn
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Tìm kiếm theo chiều sâu từ k, mang theo thời gian đã trôi qua và ghi lại thời gian đến nhanh nhất của mỗi nút. Một tuyến đường đến nút vào lúc không sớm hơn thời gian đã ghi sẽ dừng lại; tuyến đường đến sớm hơn sẽ cập nhật thời gian đã ghi và tiếp tục tìm kiếm. Mỗi thời gian đã ghi cuối cùng đều bằng thời gian nhanh nhất thực sự, vì tuyến đường nhanh nhất sẽ cập nhật nó vào một thời điểm nào đó.
Một nút có thể được cập nhật nhiều lần. Xếp 18 cổng thành một hàng, mỗi cặp được nối với nhau bằng một liên kết miễn phí và một đường vòng có thời gian lần lượt là 65,536, 32,768, rồi giảm dần đến 1. Nếu thử các đường vòng trước, quá trình tìm kiếm đến cổng cuối cùng vào 131,072 thời điểm khác nhau và mỗi lần đều đi qua toàn bộ phần phía sau cổng đó: khối lượng công việc tăng theo hàm mũ của n.
Thuật toán
- Đặt mọi thời gian tốt nhất thành vô cực và đẩy
(k, 0)vào ngăn xếp. - Lấy
(node, t)ra. Bỏ qua nếu không thỏat < best[node]; nếu không, đặtbest[node] = t. - Đẩy
(next, t + w)cho mỗi liên kết từnodecó thời điểm đến tốt hơnbest[next]. - Khi ngăn xếp trống, trả về -1 nếu có thời gian tốt nhất nào là vô cực, nếu không thì trả về giá trị lớn nhất.
def networkDelayTime(times, n, k):
graph = [[] for _ in range(n + 1)]
for u, v, w in times:
graph[u].append((v, w))
INF = float("inf")
best = [INF] * (n + 1) # the fastest arrival found so far at each node
stack = [(k, 0)]
while stack:
node, t = stack.pop()
# A route that is not faster than one already found adds nothing.
if t >= best[node]:
continue
best[node] = t
# Push in reverse so the first listed edge is explored first.
for nxt, w in reversed(graph[node]):
if t + w < best[nxt]:
stack.append((nxt, t + w))
worst = 0
for node in range(1, n + 1):
if best[node] == INF:
return -1
worst = max(worst, best[node])
return worstBellman-Ford: nới lỏng mọi cạnh tối đa n-1 lần
Ý tưởng
Giữ một thời gian tạm tính dist cho mỗi nút, bằng 0 cho k. Khi nới lỏng một liên kết u → v, giảm dist[v] xuống dist[u] + w nếu giá trị đó nhỏ hơn. Bellman-Ford nới lỏng mọi liên kết trong mỗi lượt.
Chọn đường đi nhanh nhất đến một nút, chẳng hạn k → a → b → v. Lượt đầu tiên chốt dist[a], lượt thứ hai chốt dist[b], lượt thứ ba chốt dist[v]. Đường đi nhanh nhất không bao giờ lặp lại nút, nên có nhiều nhất n-1 liên kết và n-1 lượt đủ để chốt mọi nút.
Mỗi lượt có độ phức tạp O(E). Một chuỗi gồm 3,000 nút được liệt kê từ đầu cuối trở về mất 2,999 lượt trên 4,000 liên kết: khoảng 12 triệu lần nới lỏng.
Thuật toán
- Đặt
dist[k] = 0và đặt mọidistkhác thành vô cực. - Lặp lại tối đa n-1 lần: với mỗi liên kết
[u, v, w], nếudist[u] + w < dist[v], hãy đặtdist[v] = dist[u] + w. - Dừng sớm nếu có một lượt duyệt không làm thay đổi gì.
- Trả về -1 nếu bất kỳ
distnào vẫn là vô cực, nếu không thì trả về giá trị lớn nhất.
def networkDelayTime(times, n, k):
INF = float("inf")
dist = [INF] * (n + 1)
dist[k] = 0
# A fastest route visits each node at most once, so it has at most n-1 edges,
# and n-1 passes over every edge are enough to find it.
for _ in range(n - 1):
changed = False
for u, v, w in times:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
changed = True
if not changed: # a pass that improves nothing means every time is final
break
worst = 0
for node in range(1, n + 1):
if dist[node] == INF:
return -1
worst = max(worst, dist[node])
return worstThuật toán Dijkstra với đống tối thiểu
Ý tưởng
Trong số các nút chưa được chốt, nút có thời gian tạm thời nhỏ nhất đã là nút cuối cùng. Mọi tuyến đường khác đến nút đó đều phải rời các nút đã chốt qua một nút chưa được chốt có thời gian ít nhất cũng lớn bằng, và các liên kết sau đó chỉ làm tăng thời gian vì không liên kết nào có trọng số âm. Vì vậy, bạn chốt nút đó, nới lỏng các liên kết của nó một lần rồi lặp lại.
Heap tối thiểu tìm thời gian nhỏ nhất. Khi thời gian của một nút được cải thiện, hãy đẩy một mục mới vào heap và giữ nguyên mục cũ; mục được lấy ra có thời gian lớn hơn dist là mục đã lỗi thời, nên bạn bỏ qua. Mỗi liên kết đẩy vào nhiều nhất một mục, vì vậy heap chứa tối đa E + 1 mục và tổng thời gian là O(E log E).
Thuật toán
- Đặt
dist[k] = 0, đặt mọi giá trịdistkhác bằng vô cực và đưa(0, k)vào hàng đợi ưu tiên nhỏ nhất. - Lấy
(t, node)nhỏ nhất ra; nếut > dist[node], bỏ qua vì giá trị đã cũ. - Với mỗi liên kết
node → nextcó thời gianw, nếut + w < dist[next], đặtdist[next] = t + wvà đưa(t + w, next)vào hàng đợi. - Khi hàng đợi trống, trả về -1 nếu có bất kỳ giá trị
distnào là vô cực, nếu không thì trả về giá trị lớn nhất.
import heapq
def networkDelayTime(times, n, k):
graph = [[] for _ in range(n + 1)]
for u, v, w in times:
graph[u].append((v, w))
INF = float("inf")
dist = [INF] * (n + 1)
dist[k] = 0
heap = [(0, k)] # (arrival time, node), smallest time on top
while heap:
t, node = heapq.heappop(heap)
# A stale entry: this node was already reached sooner.
if t > dist[node]:
continue
# t is now final: every other route reaches node later.
for nxt, w in graph[node]:
if t + w < dist[nxt]:
dist[nxt] = t + w
heapq.heappush(heap, (t + w, nxt))
worst = 0
for node in range(1, n + 1):
if dist[node] == INF:
return -1
worst = max(worst, dist[node])
return worst
Lỗi thường gặp và trường hợp biên
- Coi các liên kết là hai chiều.
[3, 1, 2]chỉ truyền tín hiệu từ 3 đến 1, nên nút 3 không bao giờ nhận được tín hiệu trong ví dụ thứ hai. - Dùng thuật toán tìm kiếm theo chiều rộng (breadth-first search) thông thường. Thuật toán này tìm lộ trình có ít liên kết nhất: trong ví dụ đầu tiên, nó cho nút 2 thời gian là 4 thay vì 3.
- Quên giá trị -1. Lấy giá trị lớn nhất mà không kiểm tra sẽ trả về vô cực hoặc ẩn nút chưa bao giờ nhận được tín hiệu.
- Dùng giá trị int lớn nhất làm vô cực. Khi đó,
dist[u] + wsẽ tràn thành số âm nếuuchưa được tiếp cận; hãy bỏ qua các nút chưa được tiếp cận hoặc dùng 10^9.
Câu hỏi thường gặp3
Độ phức tạp thời gian của Network Delay Time là bao nhiêu?
Với thuật toán Dijkstra và đống nhị phân, độ phức tạp là O(E log E), trong đó E là số liên kết. Độ phức tạp này bằng O(E log n), vì E tối đa là n². Độ phức tạp không gian là O(n + E). Bellman-Ford mất O(n · E) thời gian.
Tại sao thuật toán Dijkstra cần trọng số không âm?
Dijkstra không bao giờ duyệt lại một nút đã chốt, điều này chỉ an toàn khi không có tuyến đường nào xuất hiện sau đó ngắn hơn. Với các liên kết A → B mất 2 đơn vị thời gian, A → C mất 3 đơn vị thời gian và C → B mất -2 đơn vị thời gian, thuật toán chốt B ở thời điểm 2, nhưng tuyến đường qua C đến nút này ở thời điểm 1. Bellman-Ford xử lý được thời gian âm.
Bạn có thể giải bài toán Thời gian trễ mạng mà không dùng heap không?
Đúng vậy. Dijkstra dùng mảng thông thường sẽ quét mọi nút chưa được chốt để tìm thời gian nhỏ nhất, tổng cộng tốn O(n²). Cách này hiệu quả hơn trên đồ thị dày, nơi E gần bằng n². Trên đồ thị thưa, chẳng hạn như đồ thị có 3.000 nút và 4.000 cạnh, phiên bản dùng heap thực hiện ít thao tác hơn nhiều.
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 networkDelayTime(times, n, k):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
times = [[1, 2, 4], [1, 3, 1], [3, 2, 2], [2, 4, 1]] n = 4 k = 1
Đầu ra mong đợi
4