[PYTHON] Das Finden des kürzesten Pfades eines Diagramms mit einem geschlossenen Pfad nach Breitenprioritätssuche ist manchmal schneller als die Dyxtra-Methode, im schlimmsten Fall jedoch langsamer.

Es gibt einen geschlossenen Pfad (= kein Baum) Der kürzeste Pfad im Diagramm wird auch mit hoher Geschwindigkeit mithilfe der Suche nach Breitenpriorität gefunden ...!?

https://atcoder.jp/contests/soundhound2018-summer-qual/tasks/soundhound2018_summer_qual_d

Als ich die frühere Frage dieses AtCoders gelöst habe, konnte ich AC mit der Dyxtra-Methode ausführen, aber mit Python (PyPy) ist das Ausführungszeitlimit (2000 ms) gerade noch erreicht. Als ich nach früheren Einsendungen suchte, fand ich eine schnelle Antwort von 789 ms, während es viele über 1000 ms gab.

https://atcoder.jp/contests/soundhound2018-summer-qual/submissions/7721902

Es führt so etwas wie die kürzeste Routenberechnung nach Breitenprioritätssuche für einen Baum durch. Wenn der Graph jedoch einen geschlossenen Pfad anstelle eines Baums hat, scheint die Zeitberechnung $ O (V ^ 2) $ zu sein (wenn es sich um einen Baum handelt). $ O (V) = O (E) $). Die Antwort ist jedoch tatsächlich schnell. In Anbetracht der Möglichkeit, dass der Berechnungsbetrag falsch geschätzt wird, habe ich die folgenden drei Methoden verglichen.

Referenz: <a href = https://ja.wikipedia.org/wiki/%E3%83%80%E3%82%A4%E3%82%AF%E3%82%B9%E3%83%88%E3 % 83% A9% E6% B3% 95> Wikipedia-Artikel Pseudocode

Wenn man nur den Zeitaufwand berücksichtigt, scheint dijkstra1 </ b> in vielen Fällen die schnellste zu sein.

Problemstellung

Verwenden Sie ein verkettetes ungerichtetes Diagramm ($ V $ für Eckpunkte, $ E $ für Seiten, keine Mehrfachseiten, keine Selbstschleife). Erstellen Sie zunächst eine gerade Straße, die alle Punkte einmal in der Reihenfolge vom Startpunkt aus durchläuft, wie in der folgenden Abbildung gezeigt, um die Verbindung sicherzustellen (die Reihenfolge der Scheitelpunkte auf dem Weg ist zufällig). Die anderen Seiten werden zufällig zwischen Eckpunkten gestreckt, die noch keine Seiten haben. Das Gewicht der Seiten wird zufällig von $ 1 $ bis $ 10 ^ 7 $ für jede Seite bestimmt.

$V \in \{ 10^3,~ 10^4,~ 10^5,~ 10^6,~ 10^7 \} $ $E \in \{ 10^3,~ 10^4,~ 10^5,~ 3\cdot 10^5,~ 10^6,~ 10^7 \} $ (Der Grund, warum ich $ 3 \ cdot 10 ^ 5 $ in die Anzahl der Seiten gesetzt habe, ist, dass ich die Einschränkungen des Wettbewerbsprofis übernommen habe)

Die Zeit wurde für $ (V, E) $ gemessen, das $ V-1 \ leqq E \ leqq \ frac {V (V-1)} {2} $ erfüllt.

Code

worstcase.py


def dijkstra0(a, in_connect_list, v, money_kind): 
    out_shortest_list = [10**15 for i in range(v)]   #Abstand von der Spitze a
    out_shortest_list[a] = 0
    searching_list = [a]
    
    while searching_list != []:
        new_search_list = []
        for i in searching_list:
            for j in in_connect_list[i]:
                c = j[0]
                p = j[money_kind] 
                if out_shortest_list[c] > p + out_shortest_list[i]:
                    out_shortest_list[c] = p + out_shortest_list[i]
                    new_search_list.append(c)
        searching_list = list(set(new_search_list))  #Änderung vom ursprünglichen Code
    return out_shortest_list


def dijkstra1(s, M):
    D = [10**15] * v  #Abstand von Eckpunkten s
    D[s] = 0
    V = [0] * v  #D an dieser Spitze[i]If wird als kürzeste Entfernung bestimmt 1
    Q = []  #Prioritätswarteschlange
    for v in range(v):
        heapq.heappush(Q, [D[v], v])

    le = len(Q)
    while le > 0:
        q = heapq.heappop(Q)
        u = q[1]
        du = q[0]
        if V[u] == 0:
            V[u] = 1
            le -= 1
            for i in range(len(M[u])):
                v = M[u][i][0]
                luv = M[u][i][1]
                if V[v] == 0:
                    alt = du + luv
                    if D[v] > alt:
                        D[v] = alt
                        heapq.heappush(Q, [alt, v])
    return D


def dijkstra2(s, M):
    D = [10**15] * v  #Abstand von Eckpunkten s
    D[s] = 0
    Q = [i for i in range(v)]
    
    while len(Q) > 0:
        mi_ind = -1
        mi = 10**15
        for i in Q:
            if D[i] < mi:
                mi = D[i]
                mi_ind = i
        Q.pop(Q.index(mi_ind))
        for i in range(len(M[mi_ind])):
            v = M[mi_ind][i][0]
            luv = M[mi_ind][i][1]
            D[v] = min(D[v], D[mi_ind] + luv)
    return D


from random import randint, random, sample
import heapq
import time

V = [10**3, 10**4, 10**5, 10**6, 10**7]
E = [10**3, 10**4, 10**5, 3 * 10**5, 10**6, 10**7] 

for v in V:
    for e in E:
        if v-1 <= e <= v*(v-1)//2:  #Bedingungen für die Verkettung und keine Mehrfachseiten
            T = []
            for _ in range(10):  #Nehmen Sie den 10-mal gemessenen Durchschnitt
                
                N_shuf = [0] + sample([i for i in range(1, v)], v-1)
                M = [[] for i in range(v)]

                A = [min(N_shuf[i],N_shuf[i+1]) * v
                     + max(N_shuf[i],N_shuf[i+1]) for i in range(v-1)]
                while len(A) < e:
                    e_add = e - len(A)
                    ADD = [0] * e_add
                    i = 0
                    while i < e_add:
                        v0 = randint(0, v-1)
                        v1 = randint(0, v-1)
                        if v0 != v1:
                            ADD[i] = min(v0,v1) * v + max(v0,v1)
                            i += 1
                    A += ADD
                    A = list(set(A))  #Entfernen Sie mehrere Seiten

                for ii in A:
                    i = ii // v
                    j = ii % v
                    M[i].append([j, randint(1, 10**7)])
                    M[j].append([i, randint(1, 10**7)])

                t0 = time.time()
                
                D = dijkstra0(0, M, v, 1)
                # D = dijkstra1(0, M)
                # D = dijkstra2(0, M)

                t1 = time.time() - t0
                T.append(t1)
            print(v, e, sum(T) / 10)

Ergebnis

Der Durchschnitt von 10 Mal war die Einheit Sekunden und 2 gültige Zahlen.

  • dijkstra0 </ b>: Eine Methode wie die Suche nach Breitenpriorität ($ O (V ^ 2) $)
V \ E 10^3 10^4 10^5 3\cdot 10^5 10^6 10^7
10^3 0.00060 0.0074 0.14 0.40
10^4 0.0069 0.29 0.96 3.1 25
10^5 0.13 1.0 4.4 53
10^6 1.6 75
10^7 18
  • dijkstra1 </ b>: Dikstra-Methode unter Verwendung der Prioritätswarteschlange ($ O ((E + V) \ log {V}) $)
V \ E 10^3 10^4 10^5 3\cdot 10^5 10^6 10^7
10^3 0.0016 0.0059 0.070 0.23
10^4 0.020 0.16 0.41 1.1 8.9
10^5 0.36 1.3 2.6 15
10^6 4.4 37
10^7 53
  • dijkstra2 </ b>: Die grundlegendste Dikstra-Methode ($ O (V ^ 2) $)
V \ E 10^3 10^4 10^5 3\cdot 10^5 10^6 10^7
10^3 0.017 0.023 0.11 0.29
10^4 1.9 2.1 2.2 3.1 14
10^5 180 - - -
10^6 - -
10^7 -

\ -: Nicht gemessen, da es länger als ein paar hundert Sekunden gedauert hat

Erwägung

Die grundlegendste Dyxtra-Methode ist offensichtlich langsamer, wenn $ V $ zunimmt. Die Methode mit dijkstra0 </ b> und der Prioritätswarteschlange ( dijkstra1 </ b>) scheint jedoch im Bereich der Tabelle keinen großen Unterschied zu machen. Wenn die Bedingungen wahrscheinlich in einem Wettbewerbsprofi liegen ($ V \ leqq 10 ^ 5, ~ E \ leqq 3 \ cdot 10 ^ 5 $), wird entweder eine bestanden oder in einigen Fällen dijkstra0 </ b> ist besser. Es scheint auch schnell zu sein (Zeitvergleiche sind in der folgenden Tabelle angegeben). Ein weiteres Merkmal von dijkstra0 </ b> ist, dass es schneller sein kann, wenn Sie $ E $ reparieren und $ V $ erhöhen. Anscheinend ist dijkstra0 </ b> gut in spärlichen, baumartigen (= $ V $ ist groß, aber $ E $ ist klein) Graphen. Wenn es sich in der Nähe eines Baumes befindet, ist es fast $ O (V) $.

Tabelle unten: Zeitbedarfsverhältnis ( dijkstra0 </ b> / dijsktra1 </ b>)

V \ E 10^3 10^4 10^5 3\cdot 10^5 10^6 10^7
10^3 0.38 1.3 2.0 1.7
10^4 0.35 1.8 2.4 2.8 2.8
10^5 0.37 0.82 1.7 3.6
10^6 0.36 2.0
10^7 0.34

Im schlimmsten Fall ...

Das folgende Diagramm kann als der schlimmste Fall von dijkstra0 </ b> betrachtet werden. Die Anzahl der Aktualisierungen der kürzesten Entfernung beträgt $ (V-1) $ mal für den oberen linken Eckpunkt, $ (V-2) $ mal für den rechten Eckpunkt, ..., $ 1 $ für den oberen rechten Eckpunkt. Da die Gesamtzahl der Aktualisierungen $ \ frac {V (V-1)} {2} $ mal beträgt, erhöht sich die Berechnungszeit fast proportional zu $ V ^ 2 $. Schreiben Sie den anderen Code als den Funktionsteil der Dyxtra-Methode wie folgt um: $ V \ in \ {10 ^ 3, ~ 5 \ cdot 10 ^ 3, ~ 10 ^ 4, ~ 5 \ cdot 10 ^ 4 \} $ Ich habe es mit verglichen.

worstcase.py


from random import randint, random, sample
import heapq
import time

for v in [1000, 5000, 10000, 50000]:
        T = []
        for _ in range(10):

            N_shuf = [0] + sample([i for i in range(1, v)], v-1)
            M = [[] for i in range(v)]

            for i in range(1, v):
                M[0].append([N_shuf[i], 10**7 - i * 2])
                M[N_shuf[i]].append([0, 10**7 - i * 2])
            for i in range(1, v-1):
                M[N_shuf[i+1]].append([N_shuf[i], 1])
                M[N_shuf[i]].append([N_shuf[i+1], 1])

            t0 = time.time()
            D = dijkstra0(0, M, v, 1)
            # D = dijkstra1(0, M)
            # D = dijkstra2(0, M)
            t1 = time.time() - t0
            T.append(t1)
        print(v, sum(T) / 10)
V E dijkstra0 dijkstra1 dijkstra2
1000 1997 0.11 0.0024 0.017
5000 9997 3.1 0.016 0.45
10000 19997 12 0.033 1.8
50000 99997 740 0.25 48

Wenn in dijkstra0 </ b> $ V $ mit 10 multipliziert wird, beträgt die Berechnungszeit das 100- bis 200-fache (dies ist auch bei dijkstra2 </ b> der Fall). Das Ergebnis ist völlig anders. dijkstra1 </ b>, also $ O ((E + V) \ log {V}) $, ist immer noch stabil und schnell. Es kann Fälle geben, in denen dieser schlimmste Fall nicht wie zu Beginn des Problems im Testfall enthalten ist, es jedoch besser erscheint, dijkstra1 </ b> für Wettbewerbsprofis zu verwenden. Im Falle der zeitlichen Begrenzung ist es besser, eine schnelllebige Implementierung zu implementieren, die andere Teile als die Dyxtra-Methode enthält.

Zusammenfassung

  • Wenn die Suche nach Breitenpriorität für Bäume für ein Diagramm mit einem geschlossenen Pfad verwendet wird, beträgt die Zeitberechnung $ O (V ^ 2) , aber die Dyxtra-Methode wurde entwickelt ( O ((E + V) \ log {V}) $ ) Kann schneller sein. ――Für Grafiken, die ungefähr die Größe eines Wettbewerbsprofis haben, können Sie bestehen. ――Aber im schlimmsten Fall ist es langsam, daher ist es besser, die Dyxtra-Methode zu verwenden.

Änderungsprotokoll

--2020 / 04/20 Geänderter Artikeltitel (früherer Titel: O (V ^ 2) ist schneller als Dyxtra? Suche nach Breite mit Priorität für Diagramme mit geschlossenen Pfaden), teilweise überarbeiteter Text