[PYTHON] Trouver le chemin le plus court sur un graphe avec un chemin fermé est parfois plus rapide que la méthode Dyxtra, mais dans le pire des cas, c'est plus lent.

Il y a un chemin fermé (= pas un arbre) Le chemin le plus court dans le graphique est également trouvé à grande vitesse en utilisant la recherche de priorité de largeur ...!?

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

Lorsque j'ai résolu cette question passée d'AtCoder, j'ai pu faire AC en utilisant la méthode Dyxtra, mais avec Python (PyPy), le délai d'exécution (2000ms) est à peine atteint. Lorsque j'ai recherché des soumissions passées, j'ai trouvé une réponse rapide de 789 ms alors qu'il y en avait beaucoup plus de 1000 ms.

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

Il fait quelque chose comme le calcul de l'itinéraire le plus court par priorité à la largeur pour rechercher un arbre, mais si le graphique a un chemin fermé au lieu d'un arbre, le temps de calcul semble être $ O (V ^ 2) $ (s'il s'agit d'un arbre). $ O (V) = O (E) $). Cependant, la réponse est en fait rapide. Considérant la possibilité que le montant du calcul soit mal estimé, j'ai comparé les trois méthodes suivantes.

Référence: <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> Pseudo code article Wikipedia

Si l'on considère uniquement le temps de calcul, dijkstra1 </ b> semble être le plus rapide dans de nombreux cas.

Problème de réglage

Utilisez un graphe concaténé non orienté ($ V $ pour les sommets, $ E $ pour les côtés, pas de côtés multiples, pas d'auto-boucle). Tout d'abord, comme le montre la figure ci-dessous, en créant une route unique qui traverse tous les points une fois dans l'ordre à partir du point de départ, la connexion est garantie (l'ordre des sommets le long du chemin est aléatoire). Les autres côtés sont étirés au hasard entre des sommets qui n'ont pas encore de côtés. Le poids des côtés est déterminé aléatoirement de 1 $ à 10 $ ^ 7 $ pour chaque côté.

$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 \} $ (La raison pour laquelle j'ai mis $ 3 \ cdot 10 ^ 5 $ dans le nombre de côtés est parce que j'ai assumé les restrictions de la concurrence pro)

Le temps a été mesuré pour $ (V, E) $ satisfaisant $ V-1 \ leqq E \ leqq \ frac {V (V-1)} {2} $.

code

worstcase.py


def dijkstra0(a, in_connect_list, v, money_kind): 
    out_shortest_list = [10**15 for i in range(v)]   #Distance du sommet 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))  #Changement du code d'origine
    return out_shortest_list


def dijkstra1(s, M):
    D = [10**15] * v  #Distance des sommets s
    D[s] = 0
    V = [0] * v  #D à ce sommet[i]Si est déterminée comme étant la distance la plus courte 1
    Q = []  #File d'attente de priorité
    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  #Distance des sommets 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:  #Conditions de concaténation et pas de côtés multiples
            T = []
            for _ in range(10):  #Prendre la moyenne mesurée 10 fois
                
                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))  #Supprimer plusieurs côtés

                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)

résultat

La moyenne de 10 fois, l'unité était de secondes et 2 nombres valides.

  • dijkstra0 </ b>: une méthode comme la recherche de priorité de largeur ($ 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>: méthode Dikstra utilisant la file d'attente prioritaire ($ 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>: la méthode dikstra la plus basique ($ 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 -

\ -: Non mesuré car cela a pris plus de quelques centaines de secondes

Considération

La méthode Dyxtra la plus basique est évidemment plus lente à mesure que $ V $ augmente. Cependant, la méthode utilisant dijkstra0 </ b> et la file d'attente prioritaire ( dijkstra1 </ b>) ne semble pas faire une grande différence dans la plage de la table. Si les conditions sont susceptibles d'être dans une compétition pro ($ V \ leqq 10 ^ 5, ~ E \ leqq 3 \ cdot 10 ^ 5 $), soit l'une passera, soit dans certains cas dijkstra0 </ b> sera meilleure. Il semble également rapide (les comparaisons de temps sont données dans le tableau ci-dessous). Une autre caractéristique de dijkstra0 </ b> est que si vous corrigez $ E $ et augmentez $ V $, cela peut être plus rapide. Apparemment, dijkstra0 </ b> est bon pour les graphes clairsemés et arborescents (= $ V $ est grand mais $ E $ est petit). S'il est proche d'un arbre, ce sera presque $ O (V) $.

Tableau ci-dessous: Ratio de temps requis ( 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

Au pire des cas ...

Le graphique suivant peut être considéré comme le pire des cas de dijkstra0 </ b>. Le nombre de mises à jour de la distance la plus courte est $ (V-1) $ fois pour le sommet supérieur gauche, $ (V-2) $ $ fois pour celui de droite, ..., $ 1 $ pour le sommet supérieur droit. Puisque le nombre total de mises à jour est de $ \ frac {V (V-1)} {2} $ fois, le temps de calcul augmentera presque proportionnellement à $ V ^ 2 $. Réécrivez le code autre que la partie fonction de la méthode Dyxtra comme suit, $ V \ in \ {10 ^ 3, ~ 5 \ cdot 10 ^ 3, ~ 10 ^ 4, ~ 5 \ cdot 10 ^ 4 \} $ Je l'ai comparé avec.

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

Encore une fois, dans dijkstra0 </ b>, si $ V $ est multiplié par 10, le temps de calcul sera de 100 à 200 fois (c'est également le cas avec dijkstra2 </ b>). Le résultat est complètement différent. dijkstra1 </ b>, qui est $ O ((E + V) \ log {V}) $, est toujours stable et rapide. Il peut y avoir des cas où ce pire cas n'est pas inclus dans le cas de test comme dans le problème au début, mais il semble préférable d'utiliser dijkstra1 </ b> pour les professionnels de la compétition. Dans le cas de la limite de temps, il vaut mieux y faire face en mettant en œuvre une implémentation rapide comprenant des parties autres que la méthode Dyxtra.

Résumé

--Lorsque la recherche de priorité de largeur pour les arbres est utilisée pour les graphiques avec des chemins fermés, le temps de calcul est $ O (V ^ 2) , mais la méthode Dyxtra est conçue ( O ((E + V) \ log {V}) $ ) Peut être plus rapide. ――Pour les graphiques qui ont à peu près la taille d'un professionnel de la compétition, vous pouvez réussir. ――Mais dans le pire des cas, c'est lent, il est donc préférable d'utiliser la méthode Dyxtra.

Journal des modifications

--2020 / 04/20 Changement du titre de l'article (ancien titre: O (V ^ 2) est plus rapide que Dyxtra? Recherche de priorité de largeur pour les graphiques avec des chemins fermés), partiellement révisé