Probleme – Poids ou : une file à deux bouts au lieu d'un tas
Exercice · niveau 3 (difficile) · informatique (MP2I/MPI), chapitre 19 — Parcours de graphes et plus courts chemins
Énoncé
Quand tous les poids valent ou , on peut faire mieux que Dijkstra.
- Quel est le rapport avec le parcours en largeur ?
- Écrire l'algorithme, et justifier qu'une file à deux bouts suffit.
- Donner la complexité, et vérifier sur un exemple que le résultat coïncide avec Dijkstra.
Corrigé
1. Le rapport. Le parcours en largeur est exactement Dijkstra quand tous les poids valent : la file joue le rôle d'une file de priorité, parce que les distances sortent déjà triées. On voudrait garder ce bénéfice quand certains arcs coûtent .
L'idée : un arc de poids ne change pas la distance. Le sommet qu'il découvre appartient donc à la couche courante, et non à la suivante. Il faut pouvoir l'insérer devant.
2. L'algorithme. La réserve devient une file à deux bouts (deque) : on insère devant pour un arc de poids , derrière pour un arc de poids .
/* Distances minimales depuis s, tous les poids valant 0 ou 1.
d[v] vaut INF si v est inaccessible.
Preconditions : 0 <= s < n ; adj[u][k].p vaut 0 ou 1.
Complexite : Theta(n + m) en temps, O(m) en memoire. */
void zero_un(int n, arc* adj[], const int deg[], int s, int d[]) {
assert(0 <= s && s < n);
int cap = 2; for (int u = 0; u < n; u = u + 1) { cap = cap + 2 * deg[u]; }
int* dq = malloc((size_t) cap * sizeof(int)); /* tableau circulaire */
if (dq == NULL) { return; }
int tete = cap / 2, queue = cap / 2; /* la zone utile est [tete, queue) */
for (int u = 0; u < n; u = u + 1) { d[u] = INF; }
d[s] = 0; dq[queue] = s; queue = queue + 1;
/* INVARIANT : les distances des sommets de la deque prennent au plus deux
valeurs consecutives, et les plus petites sont DEVANT. */
while (tete < queue) {
int u = dq[tete]; tete = tete + 1;
for (int k = 0; k < deg[u]; k = k + 1) {
int v = adj[u][k].v, p = adj[u][k].p;
if (d[u] + p < d[v]) {
d[v] = d[u] + p;
if (p == 0) { tete = tete - 1; dq[tete] = v; } /* DEVANT */
else { dq[queue] = v; queue = queue + 1; } /* DERRIERE */
}
}
}
free(dq);
}
Pourquoi deux bouts suffisent. C'est l'invariant écrit dans le code, et c'est le même que celui du parcours en largeur du chapitre, à un détail près : les distances présentes dans la deque prennent au plus deux valeurs consécutives et , les étant devant. En traitant un sommet de distance :
- un arc de poids produit un sommet de distance , qu'on met devant : il rejoint le bon groupe ;
- un arc de poids produit un sommet de distance , qu'on met derrière : idem.
L'invariant est conservé, donc le sommet retiré en tête est toujours de distance minimale — la propriété que Dijkstra obtient par un tas, on l'obtient ici par la structure de l'entrée.
Le dimensionnement de la deque mérite une ligne : un sommet peut y entrer plusieurs fois (une fois par relâchement réussi, donc au plus une fois par arc), et l'on peut pousser devant autant que derrière. Réserver cases et démarrer au milieu garantit qu'aucun des deux bouts ne déborde. C'est le prix de la simplicité : un tableau circulaire ferait la même chose en de mémoire, avec un modulo à chaque accès.
3. Complexité et vérification. Chaque arc provoque au plus un ajout et chaque ajout un retrait : , comme un parcours en largeur, sans le facteur de Dijkstra. Mesure sur un graphe de six sommets et sept arêtes de poids ou :
parcours 0-1 : 0 0 1 1 1 2
Dijkstra : 0 0 1 1 1 2
Les deux coïncident, et le premier ne construit aucun tas.
Généralisation. Avec des poids entiers bornés par , la même idée donne l'algorithme de Dial : files, indexées par la distance modulo , parcourues cycliquement. Complexité , meilleure que Dijkstra dès que est petit. La leçon est générale : le tas est le prix de l'ignorance sur les poids. Dès qu'on en sait davantage, on paie moins.
Les autres exercices de ce chapitre Le cours du chapitre
Un blocage sur cet exercice ? Le tuteur d'Adloun guide par questions, sans donner la réponse.