Sujet du CAPES 2018, épreuve d'informatique¶
Source: http://www4.ac-nancy-metz.fr/capesmath/data/uploads/EP1_info_2018.pdf
N.B. : Ceci ne représente ni LA solution ni LE corrigé de cette épreuve mais uniquement la façon avec laquelle je répondrais à une telle série d'exercices dans le cadre de cette épreuve.
Problème 1 - suite de Lucas¶
# importation de quelques fonctions que je vais utiliser
from math import ceil, floor, log, sqrt, log2
# illustration des deux fonction d'arrondi présentées dans le sujet: ceil (arrondi supérieur) et floor (arrondi inférieur)
# arrondi sup.
print(ceil(1.24))
print(ceil(2.0))
print(ceil(2))
# arrondi inf.
print(floor(1.24))
print(floor(1.0))
print(floor(1))
La suite $(L_n)_{n \geq 0}$ des nombres de Lucas est définie par récurrence ainsi :
$\left\{ \begin{array}{ccl} L_0 & = & 2 \\ L_1 & = & 1 \\ L_n & = & L_{n-1} + L_{n-2} \quad pour \quad n \geq 2 \end{array} \right.$
Question 1¶
Calculer $L_2$, $L_3$, $L_4$, $L_5$, $L_6$ et $L_7$.
# définition d'une fonction simple reprenant strictement la relation énoncée précédemment
def L_n(n):
""" Calcul le nombre de Lucas L(n) """
if n == 0:
return 2
elif n == 1:
return 1
else:
# petit calcul récursif pour les éléments n ≥ 2
return L_n(n - 1) + L_n(n - 2)
# Les éléments demandés
[L_n(i) for i in range(2, 8)]
Question 6.1¶
On propose la fonction suivante pour calculer le n-ième nombre de Lucas. On rappelle que **
est l'opérateur Python
d'élévation à la puissance.
def lucas1(n):
if n == 0:
return 2
phi = (1 + sqrt(5)) / 2
phi2 = (1 - sqrt(5)) / 2
return phi**n + phi2**n
# calcul des 8 premières valeurs, comme précédemment
[lucas1(n) for n in range(8)]
Nous n'obtenons pas des entiers car $\phi$ et $\hat{\phi}$ sont des nombres réels qui, sur un ordinateur, on une représentation inexacte en base 2 (codage des flottant IEEE 754 sur 32 ou 64 bits). Plus $n$ va être grand, plus l'erreur cumulée va être grande.
Question 6.2¶
On propose maintenant la fonction suivante pour calculer le n-ième nombre de Lucas:
def lucas2(n):
if n == 0:
return 2
phi = (1+ sqrt(5)) / 2
if n % 2 == 0:
return ceil(phi**n)
else:
return floor(phi**n)
# calcul des 8 premières valeurs
[lucas2(n) for n in range(8)]
L'utilisation des fonctions ceil
et floor
permet de projeter dans l'espace des entiers les valeurs réelles finales calculées.
Par ailleurs, il y a un phénomène de quantification.
Je pense que le choix des fonctions des lignes 6 à 9 vient du fait que $|\hat\phi| < 1$, et que donc $|\hat\phi|^n$ tend vers 0.
Si $n$ est pair, elle tend vers 0 de manière positive, si $n$ est impair, elle tend vers 0 de manière négative.
Ainsi dans un cas il faut minorer et dans l'autre majorer.
Question 6.3¶
Un calcul exact montre que $L_{36} = 33385282$, mais lucas2(36)
renvoie la valeur 33385283.
Comment expliquez-vous cela ?
lucas2(36)
Or $L_{36} = 33385282$. L'erreur vient encore une fois du fait que la représentation et le calcul des réels n'est pas exacte. Le cumul des erreurs réussit à incrémenter de 1 la valeur théorique. La précision de la représentation IEEE 754 des flottants sur 32 bits est de l'ordre de 7 chiffres significatifs. Or ici, $n=36$ donc $L_{36} \geq 10^7$ (cf. question 5). Il y a donc un risque fort d'erreur sur le chiffre des unités du nombre que l'on va calculer.
Question 7.1¶
On souhaite écrire une nouvelle fonction de calcul des termes de la suite de Lucas.
Pour éviter tout problème lié au calcul avec des flottants, on souhaite ne travailler qu'avec des entiers, sur lesquels Python calcule de manière exacte.
Recopier et compléter la fonction suivante, qui renvoie la valeur de $L_n$.
def lucas3(n):
if n == 0:
return 2
if n == 1:
return 1
a, b = (2, 1)
for i in range(n):
a, b = (b, a + b)
return a
# calcul des 8 premiers nombrer de Lucas avec cette nouvelle fonction
[lucas3(n) for n in range(8)]
Question 7.2¶
Déterminer un invariant de boucle qui précise, en fonction de la valeur de i
, les valeurs des variables a
et b
avant et après l'exécution de la ligne 7 et en déduire que lucas3
renvoie un résultat exact.
N'ayant à chaque étape qu'une opération élémentaire d'addition sur des entiers, il n'y a pas de passage intermédiaire par des flottants. Le calcul sera donc exact.
QuestionÇ 7.3¶
Pour $n \geq 2$, combien d'additions sont effectuées par lucas3(n)
?
Pour $n \geq 2$, il est effectué 1 addition par pas de la boucle, et il y a $n$ pas. Donc au total $n$ additions.
Question 9.1¶
Si $k$ est un entier, que calcule l'expression Python suivante : 1 - 2*(k % 2)
?
On rappelle que l'opérateur %
calcule le reste dans la division entière : 7 % 3
vaut 1 ;
8 % 3
vaut 2 et 9 % 3
vaut 0.
# petit rappel sur l'opérateur %
print(7 % 3, 8 % 3, 9 % 3)
# k % 2 est à valeur dans {0, 1}, donc 1-2*(k%2) vaudra 1 si k est pair, 1-2=-1 si k est impair
[1 - 2*(k % 2) for k in range(10)]
# l'expression précédente est équivalente à classique (-1)**k
[(-1)**k for k in range(10)]
Question 9.2¶
Recopier et compléter la fonction récursive suivante, de sorte que lucas4(n)
renvoie le couple
$(L_n , L_{n+1})$ .
def lucas4(n):
if n == 0:
return (2, 1)
if n == 1:
return (1, 3)
k = n // 2
u = 1 - 2*(k % 2)
a, b = lucas4(k)
if n % 2 == 0:
return (a**2 - 2*u, a*b - u)
else:
return (a*b - u , b*b + 2*u)
# calcul sur les 10 premières valeur entières
[lucas4(k) for k in range(10)]
Question 9.3¶
Pour tout entier $n \geq 2$, exprimer en fonction de $n$ le nombre d'appels récursifs que réalise lucas4(n)
.
Pour $n \geq 2$, la fonction appelle un fois récursivement avec la valeur n // 2
. Ainsi, à chaque étape, $n$ est
«divisé» par 2.
Au final, il y aura $log_2(n)$ appels récursifs, et pour être plus précis $\lfloor log_2(n) \rfloor$.
# n = 3
print(floor(log2(3))) # il y a un appel récursif
# n = 4
print(floor(log2(4))) # il y a 2 appels récursifs
Question 10¶
Pour $n$ un entier naturel, on considère le vecteur colonne de $\mathbb{R}^2$ défini par $V_n = \left( \begin{array}{c} L_n \\ L_{n+1} \end{array} \right) $
On considère également la matrice $A = \left(\begin{array}{cc} 0 & 1 \\ 1 & 1 \end{array}\right)$
Pour tout entier $n \geq 1$ , exprimer $V_n$ en fonction de $A$ et de $V_{n−1}$, puis exprimer $V_n$ en fonction de $A$, de $n$ et de $V_0$.
On représente dans la suite une matrice carrée $\left(\begin{array}{cc}a & b \\ c & d \end{array}\right)$ par la liste de deux listes [[a, b], [c, d]]
.
On montre par récurrence que $V_{n} = A \times V_{n-1}$ et donc $V_n = A^n \times V_0$
Question 11.1¶
Écrire en Python une fonction prodMat(M1, M2)
qui prend en arguments deux matrices $2 \times 2$ représentées par
des listes comme indiqué ci-dessus, et qui renvoie la liste qui représente la matrice produit $M1 \times M2$.
def prodMat(M1, M2):
""" Renvoie le produit de 2 matrice 2x2 """
return [
[M1[0][0]*M2[0][0] + M1[0][1]*M2[1][0],
M1[0][0]*M2[0][1] + M1[0][1]*M2[1][1]],
[M1[1][0]*M2[0][0] + M1[1][1]*M2[1][0],
M1[1][0]*M2[0][1] + M1[1][1]*M2[1][1]]
]
# un petit test de la fonction
prodMat([[1, 2], [3, 4]], [[5, 6],[7, 8]])
Question 11.2¶
Combien l'appel prodMat(M1, M2)
effectue-t-il d'additions ? de multiplications ?
La fonction prodMat
effectue pour chacun des coefficients 2 multiplications et 1 addition.
Donc au total: $4 \times 2 = 8$ multiplications et $4 \times 1 = 4$ additions.
Question 11.3¶
On considère la fonction (naïve) d'élévation d'une matrice à une puissance entière suivante.
Combien l'appel puissanceMat(A, p)
réalise-t-il d'appels à prodMat en fonction de $p$ ?
def puissanceMat(M, p):
R = [[1, 0], [0, 1]]
for i in range (p):
R = prodMat(R, M)
return R
La fonction puissanceMat
fait $p$ appels à la fonction prodMat
.
Question 12.1¶
On propose la fonction suivante qui implémente l'exponentiation rapide pour les matrices $2 \times 2$ .
def puissanceMatRapide(M, n):
R = [[1, 0], [0, 1]]
P = M
while n > 0:
if n % 2 == 1:
R = prodMat(R, P)
P = prodMat(P, P)
n = n // 2
return R
Montrer que par cette méthode, le nombre d'appel à la fonction prodMat
est majoré par $2 + 2\lfloor log_2 n \rfloor$ .
La boucle while
divise $n$ par 2 à chaque étape jusqu'à atteindre 0. Il y aura donc $\lfloor log_2n \rfloor + 1$ étapes de boucle.
Au pire à chaque étape, il y a 2 appels à prodMat
. Donc au final, il y a au plus $2 + 2\lfloor log_2 n \rfloor$ appels à prodMat
.
Question 12.2¶
Exprimer en fonction de $M$ et de $i$ la valeur de la matrice $P$, après la ième itération de la boucle while
.
Après la ième itération de la boucle while
, $P$ aura effectué $i$ fois l'opération $P = P^2$.
Ainsi, $P$ vaudra $P = ((((M^2)^2)^2)...)^2$ $i$ fois, c'est-à-dire $M^{2^i}$.
Question 13¶
Proposer le code d'une fonction lucas5(n)
qui retourne la valeur du nombre de Lucas $L_n$ en utilisant
la fonction puissanceMatRapide
.
def lucas5(n):
""" calcul le nombre de Lucas en utilisant la représentation matricielle.
V_n = (L_n, L_n+1) = A^n x V_0
"""
# initialisation des variables
V0 = [2, 1] # (L_0, L_1)
A = [[0, 1], [1, 1]]
# on calcul V_{n-1} = A^{n-1} x V_0
A_n = puissanceMatRapide(A, n - 1) # on s'arrête un grand avant, c'est suffisant
V_n = [A_n[0][0] * V0[0] + A_n[0][1]*V0[1], A_n[1][0]*V0[0] + A_n[1][1]*V0[1]]
# comme en fait on a calculé V_{n-1}, on prend le deuxième coefficient
return V_n[1]
lucas5(36)
Problème 2 - emplois du temps et graphes d'intervalles¶
# importatior de quelques modules pour cette partie, pour les graphiques
%pylab inline
def affiche(liste_intervalles):
""" fonction qui affiche les graphes d'intervalles. """
figure()
rc('text', usetex=True)
ylim((-0.25, len(liste_intervalles) - 0.25))
t_min = min(map(lambda x: x[0], liste_intervalles))
t_max = max(map(lambda x: x[1], liste_intervalles))
for i, intervalle in enumerate(liste_intervalles):
plot(intervalle, [i]*2, "k")
text(intervalle[0], i + 0.10, "$C_" + str(i) + "$", {'fontsize': 20})
# on défini deux exemples de liste de cours
# exemple 1
intervalles_1 = [[0, 7], [2, 13], [8, 10]]
affiche(intervalles_1)
# exemple 2
intervalles_2 = [[0, 2], [1, 7], [4, 11], [5, 6], [8, 11], [9, 13]]
affiche(intervalles_2)
Question 2.2¶
Compléter la définition suivante : def insere(l, elt):
où l
est une liste d'entiers triée dans l'ordre croissant
et elt
est un entier, et qui renvoie une liste triée obtenue par insertion à sa place de elt
dans l
.
On notera que la liste l
peut être vide.
# version qui utilise peu certains raccoucis du langage python
def insere(l, elt):
""" Insere elt dans une copie de la liste l et la retourne """
# l est supposée triée croissante
idx = 0
nouvelle_liste = []
while idx < len(l):
e = l[idx]
if e < elt:
# on ajoute les éléments plus petits
nouvelle_liste.append(e)
else:
break
idx = idx + 1
nouvelle_liste.append(elt) # on ajoute l'élément elt
nouvelle_liste.extend(l[idx:]) # on complète avec le reste des éléments
return nouvelle_liste
# quelques tests pour vérifier que cela fonctionne
insere([], 2)
insere([1, 2, 4, 5], 3)
insere([1, 2, 3, 4, 5], 3)
insere([1, 2, 3], 0)
On suppose pour la suite que l'on dispose d'une fonction similaire insereBis
qui opère
sur des listes de listes plutôt que sur des listes d'entiers, le critère de tri étant l'ordre
des premiers éléments de chaque sous-liste. Plus précisément, la fonction insereBis
a
pour en-tête def insereBis(LL, li):
et a pour effet d'insérer une liste li
à la place
adéquate dans la liste de listes LL
.
# Il n'est pas précisé dans le texte le comportement à adopter si deux listes possèdent le même premier élément
def insereBis(LL, li):
""" Insere la liste li dans une copie de la liste de liste LL, par rapport au premier élément des listes. """
# le code est très similaire à celui de la fonction insere précédente.
idx = 0
nouvelle_liste = []
while idx < len(LL):
e = LL[idx]
# on compare le premier élément de la liste li et d'une des listes de LL
if e[0] < li[0]:
nouvelle_liste.append(e)
else:
break
idx = idx + 1
nouvelle_liste.append(li)
nouvelle_liste.extend(LL[idx:])
return nouvelle_liste
# quelques tests
insereBis([], [1, 2])
insereBis([[2, 3, 4], [5, 6, 7]], [3, 4])
insereBis([[4, 5], [5, 6], [6, 7]], [1, 2, 3])
insereBis([[1, 2], [2, 3], [3, 4]], [2, 4])
Question 3.1¶
Pour automatiser l'allocation des salles, on va utiliser une autre modélisation. L'intervalle [deb, fin[
représentant le cours numéro i
va être représenté par deux événements,
modélisés par des listes de longueur 3 : [deb,i,0]
et [fin,i,1]
. Une liste d'intervalles
pourra ainsi être représentée par une liste d'événements, c'est-à -dire une liste de listes
de longueur 3 de la forme [instant,num,0 ou 1]
.
Compléter la définition def traduit(liste_intervalles):
qui prend en argument une liste d'intervalles représentés par des listes de longueur 2 et qui renvoie une liste d'événements (listes de longueur 3) correspondante.
Notons que pour $n$ intervalles, on obtient $2n$ événements.
# par rapport à la description d'un événement, le codage suivant est utilisé :
# la valeur 0 du tuple indique un début de cours, la valeur 1 une fin de cours.
def traduit(liste_intervalles):
""" Traduit une liste d'intervalles en liste d'événements. """
liste_evt = []
for i, intervalle in enumerate(liste_intervalles):
liste_evt.append([intervalle[0], i, 0])
liste_evt.append([intervalle[1], i, 1])
return liste_evt
traduit(intervalles_1)
traduit(intervalles_2)
Question 3.2¶
Pour l'efficacité des algorithmes de résolution, on va travailler sur une liste d'événements triée par instants croissants. On appellera agenda toute liste d'événements triés par instants croissants. Quels sont les agendas correspondant aux exemples 1 et 2 ?
# pour accélérer un peu, on peut obtenir les événements triés ainsi
sorted(traduit(intervalles_1), key=lambda x: x[0])
sorted(traduit(intervalles_2), key=lambda x: x[0])
Question 3.3¶
Compléter la définition def agenda(liste_evt):
qui prend en argument une liste d'événements (listes de longueur 3)
obtenue par un appel à la fonction traduit
et qui renvoie l'agenda correspondant.
On pourra utiliser la fonctions insereBis
.
# Soit on fait comme juste au dessus avec sorted, soit on le fait à la main
# je pense qu'il est attendu de le faire à la main, surtout avec l'indication d'utiliser insereBis
def agenda(liste_evt):
result = []
# on insere les éléments un par un à partir d'une liste vide au départ
for elt in liste_evt:
result = insereBis(result, elt)
return result
# exemple 1
A = traduit(intervalles_1)
print(A)
agenda(A)
# exemple 2
B = traduit(intervalles_2)
agenda(B)
Question 4.1¶
On a demandé à des élèves d'écrire une fonction qui vérifie qu'une liste d'événements donnée contient bien autant de fin que de début, et dans le bon ordre, mais sans tester l'appariemment cours par cours, c'est-à -dire sans considérer les numéros d'intervalles.
Parmi les quatres solutions proposées, déterminer (sans justifier) la ou les réponses correctes.
ValideA est incorrecte: il peut y avoir plus de débuts que de fins
ValideB semble correcte: gère l'ordre et à la fin on doit bien avoir c == 0
ValideC est incorrecte: si on ne fait que des fins, la réponse est True
ValideD semble correcte: gère le c == 0 et l'ordre
Question 4.2¶
Adapter une des fonctions précédentes pour écrire une fonction intersection_max
qui à partir d'un agenda calcule le nombre maximal d'intervalles qui s'intersectent
mutuellement. Justifier la correction de l'approche.
# je prend la version valideD comme fonction de départ
def valideD(agenda):
c = 0
for e in agenda:
c += 1 - 2*e[2]
if c < 0:
return False
return c == 0
# je la modifie pour prendre en compte l'information du nombre d'intersection maximal
def intersection_max(agd):
c = 0
m = 0 # le max
for e in agd:
c += 1 - 2*e[2]
m = max(m, c)
if c < 0:
return None
return m
# exemple 1
intersection_max(agenda(A))
# exemple 2
intersection_max(agenda(B))
Question 4.3¶
En utilisant les fonctions précédentes, écrire une fonction nbr_optimal
qui à
partir d'une liste d'intervalles (modélisation initiale), calcule le nombre de salles
nécessaire.
def nbr_optimal(liste_intervalles):
evt = traduit(liste_intervalles)
agd = agenda(evt)
if valideD(agd):
return(intersection_max(agd))
return None
# pour l'exemple 1
nbr_optimal(intervalles_1)
# pour l'exemple 2
nbr_optimal(intervalles_2)
Question 5.1¶
On a demandé à un élève d'écrire une fonction qui, étant donné une liste de
booléens, calcule le plus petit entier $i$ tel que la case d'indice $i$ vaille True
. La
fonction renverra $−1$ si un tel indice n'existe pas. Corriger sa fonction.
def plus_petit_vrai(liste):
n = len(liste) (1)
while liste[i] and (i < n): i += 1 (2)(3)
if i = n: return -1 (4)
else: return i
# Premier bémol: conformité pep8, mais bon
# (1): pas d'initialisation de la variable "i"
# (2): la boucle va faire une erreur pour l'après dernier élément. En effet, le test liste[i] est effectué en premier
# et donc si on a débordé, il y aura une erreur immédiatemente, sans même faire le deuxiène test. En inversant les
# deux tests c'est bon
# (3): le test liste[i] est inversé: il faut boucler tant que le contenu est False
# (4): erreur de confusion "=" (affectation) et "==" (test)
#
# on obtient au final:
def plus_petit_vrai(liste):
n = len(liste)
i = 0
while (i < n) and not liste[i]:
i += 1
if i == n:
return -1
else:
return i
# Quelques tests
plus_petit_vrai([True, True])
plus_petit_vrai([False, False])
plus_petit_vrai([])
plus_petit_vrai([False, True])
plus_petit_vrai([True, False])
Question 5.2¶
On utilise à chaque instant une liste de booléens pour indiquer si une salle est disponible ou non. En utilisant les fonctions précédentes, compléter la fonction suivante qui étant donnée une liste d'intervalles (listes de longueur 2), calcule une liste d'allocations des salles, toujours en allouant la salle disponible avec le plus petit numéro. Dans cette liste, la case d'indice le numéro du cours contient le numéro de la salle allouée.
def allocation(liste_intervalles):
""" retourne le numéro de la salle allouée au cours i. """
nb_cours = len(liste_intervalles)
liste = agenda(traduit(liste_intervalles))
nb_salles = nbr_optimal(liste_intervalles) # nb salle max
salles_dispos = [True] * nb_salles
alloc = [-1] * nb_cours
for l in liste:
if l[2] == 0:
alloc[l[1]] = plus_petit_vrai(salles_dispos) # on cherche la première salle dispo
salles_dispos[plus_petit_vrai(salles_dispos)] = False # on l'occupe (plus disponible)
else:
salles_dispos[alloc[l[1]]] = True # le cours est fini, on rend disponible la salle
return alloc
# pour l'exemple 1
allocation(intervalles_1)
# pour l'exemple 2
allocation(intervalles_2)
def affiche_allocation(liste_intervalles):
""" fonction qui va afficher l'allocation de salle, chaque salle a une couleur dédiée. """
# attention, pas plus de 8 salles, sinon il faudrait des couleurs supplémentaires
rc('text', usetex=True)
nbr_o = nbr_optimal(liste_intervalles)
ylim((-0.25, len(liste_intervalles) - 0.25))
allocations = allocation(liste_intervalles)
couleurs = list(zip(range(nbr_o), ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'w']))
t_min = min(map(lambda x: x[0], liste_intervalles))
t_max = max(map(lambda x: x[1], liste_intervalles))
for i, intervalle in enumerate(liste_intervalles):
plot(intervalle, [i]*2, couleurs[allocations[i]][1])
text(intervalle[0], i+0.10, "$C_" + str(i)+"$", {'fontsize': 20})
# Pour l'exemple 1
affiche_allocation(intervalles_1)
# pour l'exemple 2
affiche_allocation(intervalles_2)