Vous êtes sur la page 1sur 8

Lycée Louis-Le-Grand, Paris 11/01/2016

MPSI 4 – Informatique commune


G. Dewaele, A. Troesch

DS d’informatique no 1

Correction du problème – Poteaux télégraphiques (d’après X PSI/PT 2013)

Partie I – Planter le paysage

1. Il s’agit d’un cumul des dénivelés, à partir de la hauteur 0 (convention donnée pour la hauteur initiale) :
def calculHauteurs(deniveles):
""" calcul des hauteurs (il est sous-entendu que le dénivelé
initial est 0; il ne sert d’ailleurs à rien"""
hauteurs=[0]
for i in range(1,len(deniveles)):
hauteurs.append(hauteurs[i-1]+deniveles[i])
return(hauteurs)

2. Il s’agit d’un calcul de maximum et de minimum en retenant la première occurrence pour chacun (donc on
n’actualise qu’en cas d’inégalité stricte) :
def calculFenetre(hauteurs):
hmax=hmin=hauteurs[0]
imax=imin=0
for i in range(1,len(hauteurs)):
if hauteurs[i] < hmin:
hmin = hauteurs[i]
imin = i
if hauteurs[i] > hmax:
hmax = hauteurs[i]
imax = i
return(hmin,imin,hmax,imax)

3. Il s’agit de sommer les longueurs des segments Pk Pk+1 , en utilisant le théorème de Pythagore :
def distanceAuSol(deniveles,i,j):
d = 0
for i in range(i,j):
d += sqrt(1+deniveles[i]**2)
return d

4. On commence par définir une fonction retournant la liste des points remarquables, sans oublier les points
extrémaux :
def pointsRemarquables(hauteurs):
P=[0]
for i in range(1,len(hauteurs)-1):
if hauteurs[i]> max(hauteurs[i-1],hauteurs[i+1]):
P.append(i)
P.append(len(hauteurs)-1)
return P

1
On calcule ensuite les longueurs entre deux points remarquables consécutifs, en retenant la plus grande longueur
trouvée :

def longueurDuPlusLongBassin(deniveles):
hauteurs = calculHauteurs(deniveles)
pointsRem = pointsRemarquables(hauteurs)
lmax = 0
for i in range(1,len(pointsRem)):
l = distanceAuSol(deniveles,pointsRem[i-1],pointsRem[i])
if l > lmax:
lmax = l
return(l)

La fonction pointsRemarquables est en O(n), puisqu’on passe n fois dans une boucle qui contient un nombre
borné d’opérations. La fonction distanceAuSol nécessite un nombre d’opération qu’on peut majorer par M (j −
i) + M ′ , où M et M ′ sont deux constantes indépendantes de i, j et n. Ainsi, en notant i0 , · · · ik les points
remarquables, la boucle for de la fonction longueurDuPlusLongBassin a un nombre d’opérations pouvant être
majoré par :
Xk
k × M ′′ + k × M ′ + M (ij − ij−1 ) = k(M ′ + M ′′ ) + M n.
j=1

Comme k 6 n + 1, on en déduit que la complexité de longueurDuPlusLongBassin est en O(n2 ).

Partie II – Planter les poteaux

5. Le réel αi,k est la pente du segment reliant le sommet d’un poteau planté en Pi et la hauteur (absolue) minimale
à laquelle doit passer le fil en abscisse k
Le réel βi,k est la pente du segment reliant les sommets de deux poteaux, l’un planté en Pi , l’autre en Pj .
On illustre le cas j > i par la figure 1
pente βi,j
pente max αi,k
i<k<j

pente αi,k

k j
i

Figure 1 – Condition sur les pentes pour pouvoir tirer un fil

Le cas i > j s’illustre par une figure symétrique, ce qui inverse le sens des inégalités, donc aussi le min et le
max.
6. On vérifie la condition donnée ci-dessous. Par défaut, si i = j, on retourne la valeur True aussi. On définit à
part les fonctions a et b calculant les valeurs de α et β.

def a(hauteurs,D,l,i,k):
return ((hauteurs[k] + D)-(hauteurs[i]+l))/(k-i)

def b(hauteurs,l,i,k):

2
return ((hauteurs[k] + l)-(hauteurs[i]+l))/(k-i)

def estDeltaAuDessusDuSol(hauteurs,i,j,l,D):
if i == j:
return True
pente = b(hauteurs,l,i,j)
if i < j:
for k in range(i+1,j):
if pente < a(hauteurs,D,l,i,k):
return False
return True
if i > j:
for k in range(j+1,i):
if pente > a(hauteurs,D,l,i,k):
return False
return True

7. La façon naïve d’implémenter cet algorithme consiste à utiliser la fonction précédente : on avance en testant à
chaque fois si on peut encore tirer un fil depuis le dernier poteau planté ; si ce n’est pas le cas, on plante un
poteau à l’emplacement précédent, et on recommence depuis ce poteau.

def placementGloutonEnAvant(hauteurs,l,D):
poteaux = [0]
i = 0
for j in range(2,len(hauteurs)):
if not estDeltaAuDessusDuSol(hauteurs,i,j,l,D):
poteaux.append(j-1)
i = j-1
poteaux.append(len(hauteurs)-1)
return poteaux

Pour le paysage de l’énoncé, on trouve bien la liste [0, 1, 5, 14, 18], ce qui correspond au résultat attendu.
8. La terminaison ne pose pas de problème (il s’agit d’une boucle for).
Pour la correction, on peut considérer l’invariant booléen suivant : à l’entrée dans la boucle indexée par j, le fil
tendu entre les poteaux de la liste poteaux vérifie les conditions légales, et le fil tendu entre le dernier poteau
et tout poteau précédant strictement la position courante vérifie aussi les conditions légales. De plus, décaler
vers la droite l’un des poteaux de la liste poteaux enlève la condition de légalité sur le fil arrivant en ce poteau.
Le fait que ce soit un invariant de boucle est assez immédiat, et si on le considère à la sortie de la dernière
boucle (donc j = n + 1), on on peut relier le dernier poteau à un poteau additionnel planté en Pn , ce qui nous
assure qu’on a bien relié les deux bords en tandant le fil suffisamment haut. de plus, déplacer un des poteaux
intermédiaires vers la droite fait perdre cette propriété.
9. La fonction estDeltaAuDessusDuSol est en O(|j − i|), Ainsi, il existe une constante M tel que le nombre
P
d’opérations nécessaires pour planter un poteau depuis le précédent soit inférieur à M dk=0 k = O(d2 ), où
d est la distance séparant les deux poteaux. On a donc une complexité totale obtenue en sommant les carrés
des distances des poteaux. Or, si d1 + · · · + dk = n, en élevant au carré, et en développant avec la formule du
binôme, si les di sont positifs, d21 + · · · + d2k 6 n2 .
On en déduit que la complexité est en O(n2 )
On peut améliorer en un algorithme en O(n), car les calculs effectués par la fonction estDeltaAuDessusDuSol
pour les fils issus d’un même poteau sont redondants : on reprend la comparaison avec tous les αi,k depuis le
début, alors qu’il suffit de mémoriser le maximum obtenu depuis le dernier poteau : on peut alors comparer
de façon linéaire, en se contenant à chaque étape de comparer βi,j à ce maximum ainsi qu’à la nouvelle valeur

3
de αi,j−1 (qui permet aussi d’actualiser le maximum). On progresse donc linéairement dans le tableau, en
effectuant à chaque étape un nombre borné d’opérations. La complexité est alors en O(n).
Même si l’implémentation n’était pas demandée, nous donnons la version obtenue après ces modifications :

def placementGloutonEnAvant2(hauteurs,l,D):
poteaux = [0]
i = 0
amax = a(hauteurs,D,l,0,1)
for j in range(2,len(hauteurs)):
if b(hauteurs,D,i,j) < amax:
poteaux.append(j-1)
i = j-1
amax = a(hauteurs,D,l,i,i+1)
else:
aj = a(hauteurs,D,l,i,j)
if aj > amax:
amax = aj
poteaux.append(len(hauteurs)-1)
return poteaux

10. On fait de même, mais à chaque poteau placé, on repart de la fin en essayant de voir si on peut relier ce poteau
au poteau final, et sinon, au poteau en position précédente, etc., en remontant petit à petit. On obtient :

def placementGloutonAuPlusLoin(hauteurs,l,D):
poteaux = [0]
i = 0
while i < len(hauteurs)-1:
j = len(hauteurs)-1
while not estDeltaAuDessusDuSol(hauteurs,i,j,l,D):
j -= 1
poteaux.append(j)
i = j
return(poteaux)

La complexité peut être majorée par O(n3 ), chaque appel à estDeltaAuDessusDuSol étant en O(n). On peut
ici aussi améliorer un peu cette complexité, mais à condition de retourner l’algorithme, ce qui le ralentira
certainement pour des longueurs pas trop grandes : on peut comme précédemment calculer les maximums au
fur et à mesure de l’algorithme, mais cela impose de progresser par ordre croissant pour tester chaque position
du nouveau poteau : on fait le test des positions du nouveau poteau jusqu’à la fin, en retenant la dernière
position acceptable rencontrée : ceci se fait en O(n). On recommence à partir du poteau planté, jusqu’à arriver
en Pn . Dans le pire des cas on a un algorithme en O(n2 ), mais peut-être meilleur en moyenne, car plus on
progresse par grands pas vers la droite moins il y a d’étapes en O(n).
11. Voici un exemple de paysage pour lequel aucun des deux algorithmes ne donne de configuration minimale en
nombre de poteaux :
Dans cette figure le fil vert plein passe en-dessous du minimum légal en abscisse 5, alors que le fil vert interrompu
passe au-dessus.
L’algorithme glouton en avant plante un poteau en position 2, ainsi que l’algorithme glouton au plus loin (car
un poteau permettant de relier directement au poteau en position 0 doit être planté au dessus de la ligne noire
en pointillés). Le poteau en position 2 ne peut pas être relié directement au dernier poteau (le fil passe en
dessous de la hauteur de sécurité en 5). Ainsi, chacun des deux algorithmes fournit une solution nécessitant au
moins 4 poteaux. Or, en plantant un poteau en 1, on n’utilise que 3 poteaux (solution en pointillés). Ainsi, la
solution fournie par les deux algorithmes gloutons n’est pas optimale en nombre de poteaux.

4
Figure 2 – Paysage tel que ni glouton en avant ni glouton au plus loin ne soient optimaux

Partie III – Minimiser la longueur de fil


Dans cette partie on met en place un algorithme basé sur de la programmation dynamique.
12. Montrons par récurrence sur i ∈ [[0, n]] que si le tableau optL est rempli avec les relations de l’énoncé, opt[i] est
la longueur minimale de fil nécessaire pour relier P1 à Pi par un câblage légal.
L’initialisation pour i = 0 est trivial : il n’y a pas besoin de fil pour relier P0 à lui-même !
Soit i ∈ [[1, n]] et supposons que pour tout j ∈ [[0, i − 1]], opt[j] est la longueur minimale de fil pour relier P0
à Pj . Considérons alors un câblage légal entre P0 et Pi , et soit k la position de son avant dernier poteau. Les
poteaux Pk et Pn sont reliables, et la longueur de fil nécessaire est au moins opt[j] pour relier P0 à Pk , et Li,k
pour relier Pk à Pn . Ainsi, la longueur totale de fil est supérieure à Li,j + optL[j], donc à

min{Li,j + optL[j] où 0 6 j < i et il est possible de tirer un fil de Pj à Pi }.

Par ailleurs, en considérant k un indice en lequel ce minimum est réalisé, par hypothèse de récurrence, il existe
un câblage de longueur optL[k] de P0 à Pk , et un câblage direct de Pk à Pn , de longueur Li,j .
On a donc trouvé un cablâge admissible de longueur totale

min{Li,j + optL[j] où 0 6 j < i et il est possible de tirer un fil de Pj à Pi }.

Ce réel étant un minorant de la longueur de tous les câblages admissibles, on en déduit qu’il s’agit bien de la
longueur minimale d’un câblage admissible.
Le principe de récurrence nous permet donc d’affirmer que pour tout i ∈ [[0, n]], optL[i] est la longueur minimale
d’un câblage admissible.
13. On implémente la construction précédente :
def longueur(hauteurs,i,j):
return(sqrt((j-i)**2 + (hauteurs[j]-hauteurs[i])**2))

def longueurMinimale(hauteurs,l,D):
optL = [0]
for i in range(1,len(hauteurs)):
lmin = longueur(hauteurs,i,i-1) + optL[i-1]
for j in range(i-1):
long = longueur(hauteurs,i,j) + optL[j]
if (long < lmin) and estDeltaAuDessusDuSol(hauteurs,j,i,l,D):
lmin = long
optL.append(lmin)
return optL[-1]

14. On peut créer en parallèle un tableau indiquant en chaque position la position du poteau précédent dans un
câblage admissible. Cette position correspond à la valeur de j minimisant l’expression de la question précédente
(donc l’entier k).

5
def longueurMinimale2(hauteurs,l,D):
optL = [0]
precOpt = [-1]
for i in range(1,len(hauteurs)):
lmin = longueur(hauteurs,i,i-1) + optL[i-1]
poteau = i-1
for j in range(i-1):
long = longueur(hauteurs,i,j) + optL[j]
if (long < lmin) and estDeltaAuDessusDuSol(hauteurs,j,i,l,D):
lmin = long
poteau = j
optL.append(lmin)
precOpt.append(poteau)
return optL[-1], precOpt

15. Pour retrouver le placement final, on part de la fin (poteau n), on regarde quel est le poteau précédent, puis
celui d’avant, et on remonte petit à petit jusqu’au premier poteau.

def placementOpt(hauteurs,l,D):
l, precOpt = longueurMinimale2(hauteurs,l,D)
poteaux = [len(hauteurs)-1]
j = len(hauteurs)-1
while j != 0:
j = precOpt[j]
poteaux = [j]+ poteaux
return poteaux

16. Pour toute valeur de i, on doit, pour chaque valeur de j inférieure à i, tester si le cablâge direct de j à i est
admissible, ce qui se fait en un nombre d’opérations borné par M (j − i)2 , pour une certaine constante M . La
somme de ces tests est donc en O(n2 ). Les autres opérations ne rajoutent qu’une composante linéaire, donc
cela reste en O(n2 ). Comme on énumère n valeurs de i, on obtient donc une complexité en O(n3 ). On peut
l’améliorer en O(n2 ), c’est ce que nous ferons dans la partie suivante, dans un contexte un peu plus général.

Partie IV – Minimiser le coût

17. On peut adapter un peu l’algorithme de la question précédente, en construisant un tableau des coûts minimaux,
plutôt que des longueurs de fil minimales. À chaque nouveau fil tiré, il faut ajouter le coût du fil et le coût du
poteau ajouté. L’algorithme se réécrit alors facilement, avec les mêmes justifications :

def minimiserCout(hauteurs,l,D,cf,cp):
optCout = [cp]
precOptCout = [-1]
for i in range(1,len(hauteurs)):
coutmin = cf * longueur(hauteurs,i,i-1) + cp + optCout[i-1]
poteau = i-1
for j in range(i-1):
cout = cf * longueur(hauteurs,i,j) + cp + optCout[j]
if (cout < coutmin) and estDeltaAuDessusDuSol(hauteurs,j,i,l,D):
coutmin = cout
poteau = j
optCout.append(coutmin)
precOptCout.append(poteau)

6
return optCout[-1], precOptCout

def placementOptCout(hauteurs,l,D,cf,cp):
cout, precOptCout = minimiserCout(hauteurs,l,D,cf,cp)
poteaux = [len(hauteurs)-1]
j = len(hauteurs)-1
while j != 0:
j = precOptCout[j]
poteaux = [j]+ poteaux
return poteaux

Mais comme précédemment, il s’agit d’un algorithme en O(n3 ). Peut-on l’améliorer de sorte à obtenir un
algorithme en n2 ? Intuitivement oui, car le facteur limitant est le calcul effectué sur la comparaison des pentes,
or, le nombre de pentes est en Θ(n2 ). On peut à nouveau faire le calcul progressif du minimum des αi,k , en
progressant cette fois en descendant, ce qui évite les redondances dans les calculs. Voici la nouvelle version, qui
est cette fois en O(n2 ) (le passage dans la boucle interne ayant un nombre borné d’opérations, le nombre de
passage lui-même étant majoté par n, et le nombre de passages dans la boucle externe étant de l’ordre de n).

def minimiserCout(hauteurs,l,D,cf,cp):
optCout = [cp]
precOptCout = [-1]
for i in range(1,len(hauteurs)):
coutmin = cf * longueur(hauteurs,i,i-1) + cp + optCout[i-1]
amin = a(hauteurs,D,l,i,i-1)
poteau = i-1
for j in range(i-2,-1,-1):
cout = cf * longueur(hauteurs,i,j) + cp + optCout[j]
if (cout < coutmin) and b(hauteurs,l,j,i) < amin:
coutmin = cout
poteau = j
amin = min(amin, a(hauteurs,D,l,i,j))
optCout.append(coutmin)
precOptCout.append(poteau)
return optCout[-1], precOptCout

18. Le nombre minimal de poteaux est obtenu en considérant que le coût des poteaux est non nul alors que le coût
du fil est nul.
En inversant, on obtient le placement donnant la longueur de fil minimale.

Partie V – Et quoi encore ?

19. La quantité len(conv) est positive à l’entrée de boucle, entière, et décroît strictement tant qu’on entre dans la
boucle (la décroissance provient du fait qu’on ne fait que supprimer des éléments de la liste, la décroissance est
stricte du fait que la stagnation correspond à la condition de sortie de la boucle, m représentant la valeur de
len(conv) à l’étape précédente). Ainsi, il s’agit d’un variant de boucle, assurant la terminaison de la fonction
convexe.
20. La fonction convexe retourne les points définissant l’enveloppe convexe du paysage (sur sa partie supérieure).
En effet, on supprime petit à petit les points en lesquels on a une concavité (croissance des pentes entre
deux segments consécutifs). Deux segments en position concave de la ligne brisée sont alors remplacés par un
segment direct qui se positionne au dessus des deux segments : la nouvelle ligne brisée majore le paysage initial,
en passant par des points de ce paysage.

7
On répète l’opération sur le paysage obtenu en enlevant ces points de concavité. Faites quelques essais : vous
verrez que le nouveau paysage obtenu n’est pas nécessairement convexe. On répète donc l’opération en parcou-
rant le paysage autant de fois que nécessaire, en enlevant à chaque fois les points de concavité, et en s’arrêtant
dès qu’un passage n’ôte plus aucun point : tous les points sont alors des points de concavité, et la ligne brisée
obtenue majore le paysage initial : il s’agit de l’enveloppe convexe. On effectue au maximum n balayages du
paysage, puisqu’on enlève au moins un point à cahque étape, sauf la dernière. Chaque étape se fait en un temps
linéaire. Ainsi, la complexité de cette fonction est en O(n2 ).
L’algorithme gloutonconvexe n’est autre que l’algorithme glouton en avant adapté à cette nouvelle situation,
implémenté en O(n). La nouveauté par rapport à la situation précédente, c’est que sur un paysage convexe,
l’algorithme glouton en avant donne le placement optimal en terme de nombre de poteaux. Montrons-le par
récurrence sur la longueur du paysage (ce que nous appelons ici la longueur est le nombre de bords ou points
saillants, même si le terme longueur n’est pas très approprié). Sur un paysage de longueur 0 ou 1, il n’y a pas
grand chose à faire. Soit n > 2. Supposons que pour tout paysage convexe de longueur k < n, l’algorithme
glouton donne un placement minimal. Soit un paysage convexe de longueur n, et i0 , i1 , · · · im les positions des
premiers poteaux (y compris les bords) par l’algorithme glouton, ainsi que j0 , · · · , jℓ les position des poteaux
d’un placement quelconque. On ne peut pas avoir j1 > i1 : ij 6= i1 + 1 par propriété de l’algorithme glouton,
et le paysage étant convexe, le fil tiré entre 0 et k, lorsque k > i1 + 1 serait en-dessous de celui tiré entre 0 et
i1 + 1, donc non valide. Ainsi, j1 6 i1 . Soit alors k l’unique entier tel que jk 6 i1 < jk+1 . On a donc k > 1. Par
convexité le fil tiré entre jk et jk+1 est en dessous de celui tiré entre i1 et jk+1 . Ainsi, i1 , jk+1 , · · · , jℓ est un
placement valide de poteaux de i1 jusqu’à ℓ. Par hypothèse de récurrence, son nombre de poteaux (ℓ − k + 1)
est supérieur à celui obtenu par l’algorithme glouton (c’est-à-dire m). On e déduit que ℓ > m + k − 1 > m.
Ainsi, la longueur du placement quelconque est supérieur à la longueur du placement obtenu par l’algorithme
glouton
Par principe de récurrence, l’algorithme glouton en avant donne donc bien le chemin minimisant le nombre de
poteau lorsque le paysage est convexe.
La combinaison des deux fonctions convexe et gloutonconvexe permet donc de déterminer un placement
minimisant le nombre de poteaux placés dur l’enveloppe convexe en temps O(n2 ) + O(n), donc O(n2 ).
En revanche, il n’est pas certain qu’en ne plantant que des poteaux en des points de l’enveloppe convexe, on
obtienne un placement minimal globalement. On peut imaginer des configurations volcaniques dans lesquelles
l’unique placement minimisant le nombre de poteaux passe par une dépression. Un tel exemple est donné dans
la figure 3

Figure 3 – Paysage tel qu’un placement minimal ne passe pas par l’enveloppe convexe

On peut penser que dans la plupart des cas, cet algorithme donnera un résultat proche de l’optimalité tout de
même. On pourrait l’améliorer en calculant l’enveloppe convexe en O(n ln(n)), par un algorithme classique.

Vous aimerez peut-être aussi