Network Delay Time
Sieć ma n węzłów oznaczonych liczbami od 1 do n. Połączenia są podane jako lista times, gdzie times[i] = [u, v, w] oznacza, że sygnał wysłany z węzła u dociera do węzła v po w jednostkach czasu. Połączenia działają tylko w jednym kierunku.
Sygnał opuszcza węzeł k w chwili 0 i rozchodzi się wszystkimi dostępnymi połączeniami. Zwróć czas, po którym otrzyma go ostatni węzeł, lub -1, jeśli jakiś węzeł nigdy go nie otrzyma.
Funkcja
- timesinteger-2d-array
- skierowane krawędzie, każda w postaci [u, v, w]
- ninteger
- liczba węzłów
- kinteger
- węzeł, który wysyła sygnał
- Zwracainteger
- czas, w którym ostatni węzeł otrzymuje sygnał, lub -1
Ograniczenia
2 ≤ n ≤ 30001 ≤ times.length ≤ 4000times[i].length = 31 ≤ u, v ≤ niu ≠ v0 ≤ w ≤ 100- Żadne dwa łącza nie mają jednocześnie takiej samej wartości
ui takiej samej wartościv. 1 ≤ k ≤ n
Przykłady
- Wejście
- times = [[1, 2, 4], [1, 3, 1], [3, 2, 2], [2, 4, 1]]n = 4k = 1
- Wyjście
- 4
- Wyjaśnienie
- Węzeł 3 odbiera sygnał w chwili 1. Węzeł 2 mógłby odebrać go w chwili 4 przez bezpośrednie łącze, ale trasą przez węzeł 3 dociera on w chwili 1 + 2 = 3, a węzeł 4 odbiera go w chwili 3 + 1 = 4. Węzeł 4 odbiera sygnał jako ostatni, w chwili 4.
- Wejście
- times = [[1, 2, 3], [3, 1, 2]]n = 3k = 1
- Wyjście
- -1
- Wyjaśnienie
- Węzeł 2 odbiera sygnał w chwili 3, ale jedyne łącze, które łączy się z węzłem 3, biegnie od 3 do 1, czyli w złym kierunku. Węzeł 3 nigdy go nie odbiera, więc odpowiedź to -1.
- Wejście
- times = [[2, 1, 5], [2, 3, 0], [3, 1, 2]]n = 3k = 2
- Wyjście
- 2
- Wyjaśnienie
- Łącze do węzła 3 ma wartość 0, więc węzeł 3 odbiera sygnał w chwili 0, razem z węzłem 2. Węzeł 1 odbiera go wtedy w chwili 0 + 2 = 2, wcześniej niż po 5 jednostkach czasu przez bezpośrednie łącze, więc każdy węzeł ma sygnał w chwili 2.
+16 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Załóżmy, że sygnał zanika po przejściu przez m łączy. Jak znaleźć czas, w którym ostatni węzeł go usłyszy, przy tym ograniczeniu?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Węzeł otrzymuje sygnał po czasie równym długości najszybszej trasy do niego z
k. Odpowiedzią jest największy z tych najszybszych czasów albo -1, jeśli do któregoś węzła nie ma żadnej trasy.Żadne połączenie nie zajmuje ujemnego czasu. Dlatego spośród węzłów, których czas nie jest jeszcze ostateczny, węzeł z najmniejszym czasem tymczasowym nie może zostać osiągnięty szybciej: każda inna droga do niego prowadzi przez inny z tych węzłów, do którego nie można dotrzeć wcześniej. Ustalaj węzły w kolejności przybycia.
Utrzymuj kopiec minimalny par
(time, node). Zdejmij najmniejszą parę, pomiń ją, jeśli węzeł ma już mniejszy czas, i dodaj każdego sąsiada, którego czas ulegnie poprawie. Gdy kopiec będzie pusty, wybierz największy czas.
Rozwiązanie
Każdy węzeł odbiera sygnał po czasie równym długości najszybszej trasy z k, więc zadanie polega na znalezieniu najkrótszych ścieżek z jednego źródła w grafie skierowanym, a następnie wyznaczeniu maksimum. Odpowiedzią jest największy z najkrótszych czasów albo -1, jeśli do któregoś węzła nie prowadzi żadna trasa. Czasy połączeń nigdy nie są ujemne, dzięki czemu algorytm Dijkstry może ustalać położenie każdego węzła tylko raz, w kolejności dotarcia, używając kopca minimum. Poniżej E oznacza liczbę połączeń, times.length.
Przeszukiwanie w głąb, które ponownie odwiedza węzły przy każdej szybszej trasie
Poprawne, ale nie kończy się na największych testach
Intuicja
Rozpocznij wyszukiwanie w węźle k z czasem 0 i podążaj każdym łączem, uwzględniając dotychczasowy czas. Zapisuj dla każdego węzła najkrótszy czas dotarcia, jaki udało się uzyskać. Gdy wyszukiwanie dociera do węzła nie szybciej niż odnotowany czas, zatrzymaj się: wszystko, co ta trasa mogłaby zaoferować dalej, oferowała już szybsza trasa. Gdy dociera do węzła szybciej, rekord się poprawia, a wszystko za tym węzłem również może się poprawić, więc wyszukiwanie jest kontynuowane od tego węzła.
To zawsze działa poprawnie. Wyszukiwanie zatrzymuje się tylko wtedy, gdy żadna trasa nie poprawia żadnego rekordu, a najszybsza trasa do każdego węzła w pewnym momencie poprawia jego rekord, więc każdy rekord kończy z prawdziwym najkrótszym czasem.
Problemem jest to, ile razy może poprawić się rekord węzła. Wyszukiwanie w głąb podąża pierwszym napotkanym łączem aż do końca, więc może dotrzeć do węzła wolną trasą, potem nieco szybszą, a następnie jeszcze szybszą, za każdym razem przechodząc przez wszystko za tym węzłem. Wyobraź sobie 18 bramek ustawionych w rzędzie. Między każdą parą bramek możesz wybrać bezpłatne łącze albo objazd — łańcuch łączy, których czasy sumują się do 65,536, 32,768 i tak dalej, aż do 1. Próbując najpierw objazdów, wyszukiwanie dociera do ostatniej bramki 131,072 razy, za każdym razem szybciej niż poprzednio, i za każdym razem przechodzi przez 1,700 węzłów znajdujących się za nią: około 220 milionów kroków. Dwa z dużych testów są skonstruowane w ten sposób: w jednym każdy objazd jest wymieniony przed bezpłatnym łączem, a w drugim po nim, więc wyszukiwanie potknie się na jednym z nich, niezależnie od kolejności, w jakiej próbuje łączy.
Algorytm
- Utwórz listę sąsiedztwa: dla każdego węzła zapisz wychodzące z niego połączenia wraz z ich czasami.
- Ustaw najlepszy czas dla każdego węzła na nieskończoność i umieść
(k, 0)na stosie. - Zdejmij ze stosu
(node, t). Jeślitnie jest mniejsze niżbest[node], pomiń ten element; w przeciwnym razie ustawbest[node] = t. - Umieść na stosie
(next, t + w)dla każdego połączenia znode, którego czas dotarcia jest lepszy niżbest[next]. - Gdy stos będzie pusty, zwróć -1, jeśli którykolwiek najlepszy czas nadal jest nieskończony; w przeciwnym razie zwróć największy z nich.
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: rozluźnij każdą krawędź maksymalnie n-1 razy
Intuicja
Przechowuj wstępny czas dist dla każdego węzła: 0 dla k, a nieskończoność dla pozostałych. Poluzowanie połączenia u → v o czasie w oznacza: jeśli dist[u] + w jest mniejsze niż dist[v], połączenie oferuje szybszą drogę do v, więc zmniejsz dist[v] do tej wartości. Bellman-Ford wykonuje przebiegi po całej liście i relaksuje każde połączenie w każdym przebiegu.
Dlaczego na końcu otrzymujemy poprawne czasy? Weźmy najszybszą trasę do pewnego węzła, na przykład k → a → b → v. W pierwszym przebiegu relaksowane jest k → a, gdy dist[k] wynosi już 0, więc po nim dist[a] jest ostateczne. Drugi przebieg ustala dist[b], a trzeci dist[v]. Najszybsza trasa nigdy nie musi odwiedzać węzła dwukrotnie, ponieważ pętla nigdy nie zajmuje ujemnego czasu, więc ma co najwyżej n-1 połączeń, a n-1 przebiegów ustala wartości dla każdego węzła. Przebieg, który niczego nie zmienia, dowodzi, że żaden późniejszy przebieg też niczego nie zmieni, więc możesz na nim zakończyć.
Każdy przebieg kosztuje O(E), więc w najgorszym przypadku złożoność wynosi O(n · E). Kolejność elementów na liście decyduje o tym, jak źle będzie: jeśli połączenia na jednej długiej trasie są podane od jej dalszego końca w kierunku początku, każdy przebieg ustala wartość dla jeszcze jednego węzła. Jeden duży test wygląda dokładnie tak: łańcuch z 3 000 węzłów podany w odwrotnej kolejności, co wymaga 2 999 przebiegów po 4 000 połączeń: około 12 milionów relaksacji. Przy takim rozmiarze nadal działa to wystarczająco szybko, ale nakład pracy rośnie proporcjonalnie do n razy E. Zaletą Bellmana-Forda jest coś innego: algorytm pozostaje poprawny, gdy czasy niektórych połączeń są ujemne, a Dijkstra wtedy zawodzi.
Algorytm
- Ustaw
dist[k] = 0, a każdej innej wartościdistprzypisz nieskończoność. - Powtórz maksymalnie n-1 razy: dla każdego łącza
[u, v, w], jeślidist[u] + w < dist[v], ustawdist[v] = dist[u] + w. - Zakończ wcześniej, jeśli po przebiegu nic się nie zmieni.
- Zwróć -1, jeśli którakolwiek wartość
distjest nadal nieskończona; w przeciwnym razie zwróć największą z nich.
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 worstAlgorytm Dijkstry z kopcem minimum
Intuicja
Bellman-Ford wykonuje zbędne przebiegi, ponieważ relaksuje połączenia wychodzące z węzłów, których czasy nie są jeszcze ostateczne, i musi wracać do tych węzłów. Algorytm Dijkstry relaksuje połączenia każdego węzła dokładnie raz, w chwili, gdy jego czas staje się ostateczny. Pytanie brzmi: skąd wiadomo, kiedy to następuje?
Odpowiedzią jest zachłanna reguła: spośród węzłów, które nie zostały jeszcze ustalone, ten o najmniejszym czasie tymczasowym ma już czas ostateczny. Każda inna trasa do niego musi w pewnym momencie opuścić ustalone węzły przez nieustalony węzeł, którego czas jest co najmniej tak duży, a dalsze połączenia mogą tylko zwiększyć czas, ponieważ żadne z nich nie ma ujemnej wartości. Ustalasz więc ten węzeł, relaksujesz jego połączenia i powtarzasz. W pierwszym przykładzie węzeł 1 zostaje ustalony w chwili 0 i oferuje węzłowi 2 czas 4, a węzłowi 3 czas 1. Węzeł 3 ma najmniejszy czas, zostaje ustalony w chwili 1 i obniża czas węzła 2 do 3. Węzeł 2 zostaje ustalony w chwili 3 i oferuje węzłowi 4 czas 4; ten zostaje ustalony jako ostatni. Odpowiedź to 4.
Kopiec minimalny szybko znajduje najmniejszy czas tymczasowy. Dodawaj (time, node) za każdym razem, gdy czas węzła ulega poprawie, a starszy wpis pozostaw w kopcu zamiast go wyszukiwać. Gdy starszy wpis zostanie później zdjęty z kopca, jego czas będzie większy od bieżącego czasu węzła, więc go pomijasz. Każde połączenie dodaje najwyżej jeden wpis, dlatego kopiec nigdy nie zawiera więcej niż E + 1 wpisów, a każde dodanie lub zdjęcie z kopca kosztuje O(log E). Łącznie daje to O(E log E), przy zużyciu O(n + E) pamięci na listę sąsiedztwa, czasy i kopiec.
Nieujemne czasy sprawiają, że zachłanna reguła jest bezpieczna. Rozważ połączenia A → B o czasie 2, A → C o czasie 3 oraz C → B o czasie -2. Algorytm Dijkstry ustala węzeł B w chwili 2, a jednak trasa przez C dociera do niego w chwili 1. Sygnał nigdy nie może dotrzeć przed wysłaniem, więc każdy czas w tym przypadku jest co najmniej równy 0 i reguła jest spełniona.
Algorytm
- Utwórz listę sąsiedztwa par
(next, w)dla każdego węzła. - Ustaw
dist[k] = 0, każdą pozostałą wartośćdistna nieskończoność i umieść(0, k)w kopcu minimum. - Usuń najmniejszą wartość
(t, node). Jeślit > dist[node], wpis jest nieaktualny: pomiń go. - W przeciwnym razie
tjest wartością końcową. Dla każdego połączenia znodedonexto czasiew, jeślit + w < dist[next], ustawdist[next] = t + wi umieść(t + w, next)w kopcu. - Gdy kopiec będzie pusty, zwróć -1, jeśli którakolwiek wartość
distjest nieskończona; w przeciwnym razie zwróć największą z nich.
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
Pułapki i przypadki brzegowe
Większość błędów ignoruje kierunek połączeń, uznaje pierwszy czas zaproponowany dla wierzchołka za ostateczny albo gubi wierzchołki, do których sygnał nigdy nie dociera.
- Traktowanie połączeń jako dwukierunkowych.
[3, 1, 2]przenosi sygnał tylko z 3 do 1, dlatego w drugim przykładzie wierzchołek 3 nigdy go nie odbiera. - Używanie zwykłego przeszukiwania wszerz. Znajduje ono trasę z najmniejszą liczbą połączeń, a nie najszybszą: w pierwszym przykładzie przypisuje wierzchołkowi 2 czas 4 przez bezpośrednie połączenie zamiast 3 przez wierzchołek 3.
- Oznaczanie wierzchołka jako ostatecznego w chwili dodania go do kopca, a nie usunięcia z niego. Pierwszy zaproponowany czas dla wierzchołka nie zawsze jest najlepszy; ostateczna jest dopiero najmniejsza wartość zdjęta z kopca.
- Zapominanie o -1. Wyznaczenie maksimum bez sprawdzenia albo zwróci nieskończoność, albo poda największy skończony czas, ukrywając wierzchołek, który nigdy nie odebrał sygnału.
- Liczenie wierzchołków od 0. Wierzchołki są oznaczone liczbami od 1 do n, więc utwórz tablice o rozmiarze
n+1i pomiń nieużywany indeks 0 przy wyznaczaniu maksimum. - Przepełnienie przy nieskończoności. Jeśli nieskończoność jest największą wartością typu int,
dist[u] + wzawija się do liczby ujemnej dla nieosiągniętegou. Pomijaj nieosiągnięte wierzchołki albo użyj wartości takiej jak 10^9, która pozostawia zapas.
Najczęstsze pytania4
Jaka jest złożoność czasowa problemu Network Delay Time?
W przypadku algorytmu Dijkstry i kopca binarnego złożoność czasowa wynosi O(E log E), gdzie E to liczba połączeń. To tyle samo co O(E log n), ponieważ E wynosi co najwyżej n². Lista sąsiedztwa, czasy i kopiec zajmują O(n + E) pamięci. Algorytm Bellmana-Forda działa w czasie O(n · E) i zajmuje O(n) pamięci.
Dlaczego algorytm Dijkstry wymaga nieujemnych wag?
Algorytm Dijkstry ustala węzeł o najmniejszym szacowanym czasie i już do niego nie wraca. Jest to bezpieczne tylko wtedy, gdy żadna późniejsza trasa nie może być krótsza, a przy nieujemnych wagach każda trasa prowadząca przez nieustalony węzeł kosztuje co najmniej tyle, ile wynosi czas tego węzła. Ujemna krawędź podważa to rozumowanie: trasa prowadząca przez węzeł, który wydawał się droższy, może jednak okazać się tańsza. W przypadku ujemnych wag użyj algorytmu Bellmana-Forda.
Dlaczego nie użyć BFS do rozwiązania problemu z opóźnieniem sieci?
BFS odwiedza węzły w kolejności zależnej od liczby połączeń dzielących je od początku, co odpowiada kolejności przybycia tylko wtedy, gdy każde połączenie zajmuje tyle samo czasu. Tutaj czasy przejścia są różne, więc trasa z większą liczbą połączeń może pozwolić dotrzeć szybciej. Gdyby każdy czas był równy, BFS by wystarczył, a przy czasach wynoszących tylko 0 i 1 sprawdza się algorytm 0-1 BFS oparty na deque.
Czy potrafisz rozwiązać problem czasu opóźnienia sieci bez kopca?
Tak. Dijkstra z użyciem zwykłej tablicy przegląda wszystkie nierozstrzygnięte węzły, aby znaleźć najmniejszy czas, co łącznie wymaga O(n²) operacji i nie wymaga kopca. To lepszy wybór w przypadku grafów gęstych, w których E jest bliskie n². W grafach rzadkich, takich jak duże testy tutaj, z 3,000 węzłami i 4,000 połączeniami, wersja z kopcem wykonuje znacznie mniej pracy.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def networkDelayTime(times, n, k):
# Wpisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
times = [[1, 2, 4], [1, 3, 1], [3, 2, 2], [2, 4, 1]] n = 4 k = 1
Oczekiwane
4