Texte : en pdf.
Dans l'épisode 23 de la saison 3 de Numb3rs, intitulé Money for Nothing, un camion contenant de l'argent et des médicaments a été attaqué.
Comme les voleurs souhaitent quitter la ville (Los Angeles) le plus rapidement possible, et qu'ils ont prémédité leur coup, le héros (Charlie) essaye de déterminer le plus rapide des chemins possibles, reliant le lieu de l'embuscade à la porte de sortie de la ville la plus proche.
L'embuscade a eu lieu dans le noeud (croisement) supérieur gauche de l'image ci-dessous, et la sortie est le noeur inférieur droit ;
Les valeurs sur les arcs/flèches représentent les temps nécessaires pour rejoindre chaque croisement.
Tester toutes les solutions devient vite inextricable. L'idée est alors d'utiliser un algorithme de recherche du plus court chemin : Dijkstra.
Il y a deux solutions possibles, elles sont indiquées sur le schéma en vert.
On importe le module networks, et on crée un graphe orienté :
>>> from networkx import * >>> G=MultiDiGraph()
On commence par saisir les 20 noeuds dans une liste. Par exemple, le noeud correspondant au croisement des rues Maple Street et Behler Ave., numéroté 21 sur le dessin, correspond à l'élément (1,3) dans notre liste (le point (0,0) est le noeud supérieur gauche, lieu de l'attaque).
>>> L=[] >>> for x in range(4): ... for y in range(5): ... L.append((x,y)) ...
On ajoute cette liste de noeuds à notre digraphe :
>>> G.add_nodes_from(L)
Il faut maintenant mettre les poids sur les flèches, c'est-à-dire mettre le temps nécessaire à rejoindre deux croisements adjacents. C'est un peu long, mais bon...
>>> G.add_edge((0,0),(1,0),5) >>> G.add_edge((1,0),(2,0),8) >>> G.add_edge((2,0),(3,0),4) >>> G.add_edge((0,0),(0,1),4) >>> G.add_edge((1,0),(1,1),7) >>> G.add_edge((2,0),(2,1),4) >>> G.add_edge((3,0),(3,1),6) >>> G.add_edge((0,1),(1,1),9) >>> G.add_edge((1,1),(2,1),6) >>> G.add_edge((2,1),(3,1),7) >>> G.add_edge((0,1),(0,2),8) >>> G.add_edge((1,1),(1,2),5) >>> G.add_edge((2,1),(2,2),7) >>> G.add_edge((3,1),(3,2),6) >>> G.add_edge((0,2),(1,2),7) >>> G.add_edge((1,2),(2,2),5) >>> G.add_edge((2,2),(3,2),8) >>> G.add_edge((0,2),(0,3),4) >>> G.add_edge((1,2),(1,3),4) >>> G.add_edge((2,2),(2,3),7) >>> G.add_edge((3,2),(3,3),8) >>> G.add_edge((0,3),(1,3),8) >>> G.add_edge((1,3),(2,3),6) >>> G.add_edge((2,3),(3,3),5) >>> G.add_edge((0,3),(0,4),7) >>> G.add_edge((1,3),(1,4),5) >>> G.add_edge((2,3),(2,4),4) >>> G.add_edge((3,3),(3,4),6) >>> G.add_edge((0,4),(1,4),3) >>> G.add_edge((1,4),(2,4),8) >>> G.add_edge((2,4),(3,4),7)
Astuce : Ne recopiez pas ce qui précède, ligne par ligne. Faites un copier-coller dans un éditeur, et remplacez tous les >>> par rien du tout.
On a, dans ce qui précède, supposé que toutes les rues étaient à sens unique. Rajoutez des arcs orientés au graphe G pour coller exactement au problème de Charlie.
Il reste maintenant à réfléchir à un algorithme trouvant le plus court chemin. Une étude systématique de tous les chemins possibles n'étant évidemment pas envisageable, on va s'orienter vers des algorithmes évolués de recherche de plus court chemin.
L'algorithme de Floyd est un premier algorithme, naïf, permettant de répondre au problème de la recherche de plus court chemin. Il est simple, mais coûteux.
Il faut d'abord qu'un plus court chemin existe, donc supposer les graphes :
Il y a, à la base de l'algorithme, les remarques suivantes :
La suite de matrices $M^k$ est initialisée par la matrice $M^0$, de taille $n\times n$ (où $n$ est le nombre de sommets du graphe), telle que
Il suffit ensuite de calculer la suite de matrices $M^(k), k \gesqlant 1$ définies par :
$M_{i,j}^{k}=min \left(M_{i,j}^{k-1},M_{i,k}^{k-1} + M_{k,j}^{k-1}\right)$
C'est-à-dire...
$\left( \begin{array}{cccc} \min{\left(M_{1,1}^0,M_{1,1}^0 + M_{1,1}^0\right)} & \min{\left(M_{1,2}^0,M_{1,1}^0 + M_{1,2}^0\right)} & ... & \min{\left(M_{1,n}^0,M_{1,1}^0 + M_{1,n}^0\right)}\\ \min{\left(M_{2,1}^0,M_{2,1}^0 + M_{1,1}^0\right)} & \min{\left(M_{2,2}^0,M_{2,1}^0 + M_{1,2}^0\right)} & ... & \min{\left(M_{2,n}^0,M_{2,1}^0 + M_{1,n}^0\right)} \\ \min{\left(M_{3,1}^0,M_{3,1}^0 + M_{1,1}^0\right)} & \min{\left(M_{3,2}^0,M_{3,1}^0 + M_{1,2}^0\right)} & ... & \min{\left(M_{3,n}^0,M_{3,1}^0 + M_{1,n}^0\right)} \\ \vdots & \vdots & ... & \vdots \\ \min{\left(M_{n,1}^0,M_{n,1}^0 + M_{1,1}^0\right)} & \min{\left(M_{n,2}^0,M_{n,1}^0 + M_{1,2}^0\right)} & ... & \min{\left(M_{n,n}^0,M_{n,1}^0 + M_{1,n}^0\right)} \end{array}\right)$
$\left( \begin{array}{cccc} \min{\left(M_{1,1}^1,M_{1,2}^1 + M_{2,1}^1\right)} & \min{\left(M_{1,2}^1,M_{1,2}^1 + M_{2,2}^1\right)} & ... & \min{\left(M_{1,n}^1,M_{1,2}^1 + M_{2,n}^1\right)}\\ \min{\left(M_{2,1}^1,M_{2,2}^1 + M_{2,1}^1\right)} & \min{\left(M_{2,2}^1,M_{2,2}^1 + M_{2,2}^1\right)} & ... & \min{\left(M_{2,n}^1,M_{2,2}^1 + M_{2,n}^1\right)} \\ \min{\left(M_{3,1}^1,M_{3,2}^1 + M_{2,1}^1\right)} & \min{\left(M_{3,2}^1,M_{3,2}^1 + M_{2,2}^1\right)} & ... & \min{\left(M_{3,n}^1,M_{3,2}^1 + M_{2,n}^1\right)} \\ \vdots & \vdots & ... & \vdots \\ \min{\left(M_{n,1}^1,M_{n,2}^1 + M_{2,1}^1\right)} & \min{\left(M_{n,2}^1,M_{n,2}^1 + M_{2,2}^1\right)} & ... & \min{\left(M_{n,n}^1,M_{n,2}^1 + M_{2,n}^1\right)} \end{array}\right)$
En effet,pour $k>0$, $M_{i,j}^{k}$ est la longueur du plus court chemin entre $i$ et $j$, en ne considérant que les sommets intermédiaires $0,1,...,k$.
Ainsi, après $n$ calculs, on obtient la matrice des longueurs des plus courts chemins.
On souhaite appliquer l'algorithme de Floyd-Warshall au graphe :
$M^0 = \left(\begin{array}{cccc} 0 & 7 & 6 & \infty \\ 7 & 0 & \infty & 8 \\ 6 & \infty & 0 & 5 \\ \infty & 8 & 5 & 0 \end{array}\right)$
$P^0 = \left(\begin{array}{cccc} 1 & 1 & 1 & None \\ 2 & 2 & None & 2 \\ 3 & None & 3 & 3 \\ None & 4 & 4 & 4 \end{array}\right)$
$M^1 = \left(\begin{array}{cccc} 0 & 7 & 6 & \infty \\ 7 & 0 & 13 & 8 \\ 6 & 13 & 0 & 5 \\ \infty & 8 & 5 & 0 \end{array}\right)$
$P^1 = \left(\begin{array}{cccc} 1 & 1 & 1 & None \\ 2 & 2 & 1 & 2 \\ 3 & 1 & 3 & 3 \\ None & 4 & 4 & 4 \end{array}\right)$
$M^2 = \left(\begin{array}{cccc} 0 & 7 & 6 & 15 \\ 7 & 0 & 13 & 8 \\ 6 & 13 & 0 & 5 \\ 15 & 8 & 5 & 0 \end{array}\right)$
$P^2 = \left(\begin{array}{cccc} 1 & 1 & 1 & 2 \\ 2 & 2 & 1 & 2 \\ 3 & 1 & 3 & 3 \\ 2 & 4 & 4 & 4 \end{array}\right)$
$M^3 = \left(\begin{array}{cccc} 0 & 7 & 6 & 11 \\ 7 & 0 & 13 & 8 \\ 6 & 13 & 0 & 5 \\ 11 & 8 & 5 & 0 \end{array}\right)$
$P^3 = \left(\begin{array}{cccc} 1 & 1 & 1 & 3 \\ 2 & 2 & 1 & 2 \\ 3 & 1 & 3 & 3 \\ 3 & 4 & 4 & 4 \end{array}\right)$
Il nous reste un dernier sommet à envisager : le 4. On ne trouve aucun changement...
$M^4 = \left(\begin{array}{cccc} 0 & 7 & 6 & 11 \\ 7 & 0 & 13 & 8 \\ 6 & 13 & 0 & 5 \\ 11 & 8 & 5 & 0 \end{array}\right) P^4 = \left(\begin{array}{cccc} 1 & 1 & 1 & 3 \\ 2 & 2 & 1 & 2 \\ 3 & 1 & 3 & 3 \\ 3 & 4 & 4 & 4 \end{array}\right)$
Le module networkx possède une méthode permettant d'appliquer l'algorithme de Floyd à notre graphe. Cette méthode s'appelle path.floyd_warshall :
>>> path.floyd_warshall(G) ({(0, 1): {(0, 1): 0, (1, 2): 14, (3, 2): 27, (0, 0): inf, (3, 3): 29, (3, 0): inf, (2, 4): 28, (3, 1): 22, (1, 4): 22, (0, 2): 8, (2, 0): inf, (1, 3): 18, (2, 3): 24, (2, 1): 15, ...
Le retour semble assez compliqué. En fait, on retrouve les deux éléments renvoyés par l'algorithme de Floyd : la distance, et le chemin (un couple de dictionnaires)...
>>> (distance, chemin)=path.floyd_warshall(G) >>> distance[(0,1)]
{(0, 1): 0, (1, 2): 14, (3, 2): 27, (0, 0): inf, (3, 3): 29, (3, 0): inf, (2, 4): 28, (3, 1): 22, (1, 4): 22, (0, 2): 8, (2, 0): inf, (1, 3): 18, (2, 3): 24, (2, 1): 15, (2, 2): 19, (1, 0): inf, (0, 4): 19, (0, 3): 12, (3, 4): 35, (1, 1): 9}
On trouve les distances du sommet (0,1) à tous les autres sommets :
On peut aussi préciser le sommet d'arrivée qui nous intéresse :
>>> distance[(0,1)][(1,1)] 9
On peut donc trouver le temps (distance) du plus court chemin entre notre lieu d'attaque, et la sortie de la ville :
>>> distance[(0,0)][(3,4)] 38
Il reste à obtenir ce chemin. On se souvient que, dans l'algorithme ci-dessus, la matrice des chemins doit être remontée récursivement. Ainsi,
>>> chemin[(0,0)][(3,4)] (3, 3)
signifie que, quand on emprunte le plus court chemin entre le croisement (0,0) et le croisement (3,4), le dernier croisement visité est (3,3).
On peut recommencer pour trouver le plus court chemin entre (0,0) et (3,3) - on connaît la fin du parcours :
>>> chemin[(0,0)][(3,3)] (2, 3)
On continue ainsi, récursivement :
>>> chemin[(0,0)][(2,3)] (1, 3) >>> chemin[(0,0)][(1,3)] (1, 2) >>> chemin[(0,0)][(1,2)] (1, 1) >>> chemin[(0,0)][(1,1)] (1, 0) >>> chemin[(0,0)][(1,0)] (0, 0)
On retrouve bien le chemin indiqué en vert. Connaissant le moment de l'attaque, on sait donc à quel carrefour se placer pour avoir le plus de chances de retrouver les agresseurs.