1. Informatique tronc commun, MP, premier devoir surveillé
Durée deux heures.
1 Bases de données (Mines 2017)
On modélise ici un réseau routier par un ensemble de croisements et de voies reliant ces croisements. Les
voies partent d’un croisement et arrivent à un autre croisement. Ainsi, pour modéliser une route à double
sens, on utilise deux voies circulant en sens opposés. La base de données du réseau routier est constituée des
relations suivantes :
• Croisement(id, longitude, latitude)
• Voie(id, longueur, id_croisement_debut, id_croisement_fin)
Remarque : Cela signifie que la base est constituées de deux tables : Croisement et Voie ; que les colonnes de
Croisement sont id, longitude, et latitude ; que les colonnes de Voie sont id, longueur, id_croisement_debut,
et id_croisement_fin ; et les colonnes soulignées sont les clés primaires de leur tables.
Dans la suite on considère c l’identifiant (id) d’un croisement donné.
1. (1 pt(s)) Écrire la requête SQL qui renvoie les identifiants des croisements atteignables en utilisant
une seule voie à partir du croisement ayant l’identifiant c.
SELECT id_croisement_fin
FROM Voies
WHERE id_croisement_debut = c
;
2. (2 pt(s)) Écrire la requête SQL qui renvoie les longitudes et latitudes des croisements atteignables en
utilisant une seule voie, à partir du croisement c.
SELECT Croisement.longitude, Croisement.latitude
FROM Croisement JOIN Voie ON Voie.id_croisement_fin=Croisement.id
WHERE Voie.id_croisement_debut = c
;
3. (1 pt(s)) Que renvoie la requête SQL suivante ?
SELECT V2 . id_croisement_fin
FROM Voie AS V1 JOIN Voie AS V2
ON V1 . id_croisement_fin = V2 . id_croisement_debut
WHERE V1 . id_croisement_debut = c
Cette requête renvoie les identifiants des croisements atteignables en deux voies. L’alias V1 représente la première
voie empruntée, et V2 la seconde.
4. (1 pt(s)) Préciser l’importance du AS V1 et du AS V2.
On joint ici la table Voie avec elle-même. Il est nécessaire de donner un alias à chaque copie de la table sans
quoi sql ne saura pas dans laquelle chercher les champs demandés.
5. On considère l’identifiant d d’un nouveau croisement. Écrire une requête qui renvoie tous les itinéraires
formés de deux voies permettant d’aller de c vers d, et pour chacun la longueur totale du trajet.
SELECT V1 . id_croisement_debut , V1 . id_croisement_fin , V2 . id_croisement_fin ,
V1 . longueur+V2 . longueur AS longueurDuParcours
FROM Voie as V1 JOIN Voie as V2
ON V1 . id_croisement_fin = V2 . id_croisement_debut
WHERE V1 . id_croisement_debut = c AND V2 . id_croisement_fin = d
;
1
2. 2 Piles : algorithme de Horner
Un polynôme sera représenté par la liste de ses coefficients, par exemple [1,5,2,3] représente le polynôme
1 + 5X + 2X2
+ 3X3
. Le but est décrire une fonction d’évaluation, qui prendra en entrée un polynôme P et
un nombre x et qui calculera P(x).
Dans la suite, nous notons x un flottant, P un polynôme, n son degré, et a0, . . . , an ses coefficients.
1. (1 pt(s)) Quel est le nombre de multiplications utilisé pour calculer P(x) par l’algorithme naïf où pour
tout k ∈ N, xk
est calculé par k multiplications ?
Pour tout k ∈ J0, nK, xk
est calculé en k multiplications, donc akxk
en k + 1 multiplications, et le nombre total
de multiplications est :
n
X
k=0
k + 1 =
(n + 1)(n + 2)
2
= Θn→∞
n2
.
Ainsi la complexité est quadratique.
2. À l’aide par exemple d’une comparaison séries-intégrales, montrer que
n
X
k=1
ln(k) ∼
n→∞
n ln(n).
Soit n ∈ N∗
.
Pour tout k ∈ J1, nK et tout t ∈ [k, k + 1], ln(k) 6 ln(t) ln(k + 1), d’où, puisque la fonction ln est croissante :
ln(k) 6
Z k+1
t=k
ln(t) 6 ln(k + 1).
Notons F : x 7→ x ln(x) − x, c’est une primitive de ln, et donc :
ln(k) 6 F(k + 1) − F(k) 6 ln(k + 1).
On déduit l’encadrement de ln(k) :
Pour tout k ∈ J2, nK :
F(k) − F(k − 1) 6 ln(k) 6 F(k + 1) − F(k)
Puis en sommant :
F(n) − F(1) 6
n
X
k=2
ln(k) 6 F(n + 1) − F(2)
Comme ln(1) = 0,
Pn
k=2
ln(k) =
Pn
k=1
ln(k)
Nous voulons prouver que limn→∞
Pn
k=1
ln(k)
n ln(n)
= 1, encadrons ce quotient :
Pour tout n ∈ N∗
:
n ln(n) − n + 1
n ln(n)
6
Pn
k=1
ln(k)
n ln(n)
6
(n + 1) ln(n + 1) − (n + 1) − F(2)
n ln(n)
Ainsi le quotient est encadré par deux suites qui convergent vers 1, il converge donc lui aussi vers 1.
D’où limn→∞
Pn
k=1
ln(k)
n ln(n)
= 1, puis
Pn
k=1
ln(k) ∼
n→∞
n ln(n).
3. Donner l’ordre de grandeur du nombre de multiplications si on calcule les puissances par l’algorithme
d’exponentiation rapide vu en cours.
Pour tout k ∈ J1, nK, le calcul de xk
se fera en O(log2(k)) multiplications. Donc le nombre de multiplication
nécessaire au calcul de P(x) sera :
n
X
k=1
1 + O(log2(k)) = n +
n
X
k=1
O(log2(k)) = n + O
n
X
k=1
log2(k)
= n +
O(n log n) = O(n log n).
4. Un élève propose cette fonction :
2
3. def evalue (P, x ) :
xpi=1
r e s=0
for i in range (0 , len (P) ) :
# i c i , xpi contient . . . . et res contient . . .
xpi∗=x
r e s+=P[ i ] ∗ xpi
return r e s
(a) (0,5 pt(s)) ] Combien de multiplications sont effectuées pour évaluer un polynôme de degré n ?
Il y a deux multiplications à chaque tour de boucle, donc un total de 2n multiplications.
(b) (0,5 pt(s)) À quoi sert la variable xpi ?
La variable xpi enregistre xi
pour pouvoir calculer xi+1
plus rapidement au tour de boucle prochain.
(c) (1,5 pt(s)) Il a une erreur dans ce code. La corriger, et compléter le commentaire dans la boucle.
Avec le code de l’élève, xpi contient xi+1
au moment où il est multiplié par P[i]. On calcule donc
n
X
k=0
akxk+1
. Voici une version corrigée avec l’invariant de boucle complété :
def evalue (P, x ) :
xpi=1
r e s=0
for i in range (0 , len (P) ) :
# i c i , xpi contient x∗∗ i et res contient l a somme pour k de 0 à i −1 de ak∗x∗∗k
r e s+=P[ i ] ∗ xpi
xpi∗=x
return r e s
(d) (2 pt(s)) Prouver que la fonction est correcte en utilisant la propriété en commentaire comme
invariant de boucle.
Pour tout i ∈ J0, n + 1K, notons P(i) : « au début de l’itération i (c’est-à-dire à la fin de l’itération i − 1),
xpi contient xi
et res contient
i−1
X
k=0
akxk
. »
• initialisation : au début de l’itération 0, c’est-à-dire avant de rentrer dans la boucle, xpi contient 1,
qui est bien égal à x0
, et res contient 0, qui est bien égal à
−1
X
k=0
akxk
.
• hérédité : Soit i ∈ J0, nK, supposons P(i). On aborde l’itération i + 1 de la boucle. Au début, par
P(i), xpi contient xi
et res contient
i−1
X
k=0
akxk
.
On effectue res+=P[i]*xpi, res contient alors :
i−1
X
k=0
akxk
+ ai ∗ xi
, qui vaut bien
i
X
k=0
akxk
.
Ensuite, on effectue xpi*=x, à la suite de quoi xpi contient xi
∗ i c’est-à-dire xi+1
.
Ainsi, au début de l’itération i + 1, xpi contiendra xi+1
et res contiendra
i
X
k=0
akxk
. Donc P(i + 1).
Ainsi, P est invariant de boucle : ∀i ∈ J0, n + 1K, P(i). En particulier, d’après P(n + 1), à la fin de
l’itération n, c’est-à-dire à la sortie de la boucle, res contient
n
X
k=0
akxk
, qui est le résultat renvoyé et le
résultat attendu.
5. On propose enfin l’algorithme de Horner, basé sur la remarque suivante :
P(x) = a0 +
a1 + a2x + . . . anxn−1
× x.
Pour cet algorithme, il sera plus pratique de ranger les coefficients dans une pile, le coefficient constant
étant au sommet et le coefficient de plus haut degré au fond. Ainsi, P sera représenté par [an,...,a1,a0]
et le polynôme a1 + a2X + . . . anXn−1
n’est autre que celui représenté par la pile [an,...,a1].
3
4. (a) Programmer l’algorithme de Horner.
Une version récursive permet de traduire immédiatement la formule proposée par l’énoncé :
def horner (P, x ) :
i f P= [ ] : return 0
else :
return P. pop ( ) + x∗ horner (P, x )
(b) Combien de multiplications sont effectuées pour calculer P(x) ?
Il y a une multiplication dans chaque appel récursif, et n appels, donc n multiplications.
3 Récursivité : résolution d’un sudoku
Nous allons étudier ici une méthode appelée backtracking qui permet de résoudre un sudoku et de
nombreux problèmes similaires.
Le principe est extrêmement basique : on choisit la case où il y a le moins chiffres possibles pour respecter
les règles du jeu, on en inscrit un, et on poursuit la résolution. Si on se rend compte que la résolution est
impossible, on efface alors le chiffre qu’on avait inscrit (c’est cette étape qui se nomme backtracking, pour
retour en arrière).
Rappel des règles et implémentation : Nous considérons une grille de format 9 × 9, qui sera repré-
sentée par un objet de type list list sous Python. Certaines cases sont déjà remplie par un chiffre de
J1, 9K. En Python, nous mettrons 0 dans les cases non remplies.
Par exemple, voici une grille complètement vide :
[[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]]
Le but est d’inscrire un chiffre de J1, 9K dans chaque case de la grille, en vérifiant les trois règles suivantes :
(i) Pour tout i ∈ J0, 8K, les chiffres inscrit sur la ligne i sont deux à deux distincts ;
(ii) Pour tout j ∈ J0, 8K, les chiffres inscrit sur la colonne j sont deux à deux distincts ;
(iii) La grille étant partagées en 9 blocs carrés de format 3 × 3, les chiffres inscrits dans chaque bloc sont
deux à deux distincts.
1. Dans un premier temps, on se concentre sur la fonction principale, qui contient le squelette de l’algo-
rithme. On supposera déjà connues les fonctions utilitaires suivantes :
• chiffresPossible telle que pour toute grille g et tout (i, j) ∈ J0, 9J2
, chiffresPossible(g,i,j)
renvoie la liste des chiffres possibles en case (i, j) de la grille g pour vérifier les trois conditions de
la règle du jeu.
• prochaineCaseARemplir telle que pour toute grille g, prochaineCaseARemplir(g) renvoie :
None si toutes les cases sont déjà remplies (auquel cas la grille est résolue !)
un triplet (i, j, listePoss) où (i, j) est une case ayant un minimum de possibilités et listePoss
est la liste de ces possibilités dans le cas contraire.
4
5. Écrire alors le programme resolution prenant en entrée une grille g et renvoyant True si cette
grille est remplissable et False sinon. En outre, ce programme remplira g au fur et à mesure, de
sorte que si il renvoie True, g soit alors complètement remplie. Par contre, si il renvoie False, la
grille devra être revenue à son état initial.
On propose la stratégie suivante :
Utiliser le programme prochaineCaseARemplir pour repérer si la grille est déjà remplie, et
dans le cas contraire, identifier la prochaine case à aller remplir ainsi que la liste listePoss des
possibilités pour remplir cette case.
Pour tout n ∈ listePoss, mettre n dans la case, et reappeler récursivement la fonction
resolution. Si cette fonction renvoie True c’est gagné ! Si cette fonction renvoie False c’est
qu’il n’a pas été possible de compléter la grille en y mettant n : dans ce cas, effacer n.
Si toutes les valeurs de listePoss ont été testées sans succès, c’est qu’il n’est pas possible de
remplir la grille.
Voir le fichier .py
2. bonus : Écrire les fonctions chiffresPossibles et prochaineCaseARemplir pour obtenir un solveur
de sudoku complet.
5