Projet : Écrire et prouver des algorithmes itératifs et récursifs
Une fois un algorithme créé, on le teste toujours sur de petites instances en se mettant à la place de l'ordinateur.
- on numérote chaque ligne
- on note sur une feuille les variables
- on exécute ligne à ligne en notant les différents résultats
- à la fin on vérifie que le retour de l'algorithme est bien correct
Démontrer un invariant donné
L'algorithme suivant est une variante itérative de l'algorithme factorielle que nous avons déjà vu :
algorithme factorielle_variante(n: int):
ref r ← 1
ref i ← n
tant que i ≥ 1:
r ← r * i
i ← i - 1
rendre r
code python
code python
def factorielle_variante(n):
r = 1
i = n
while i > 1:
r *= i
i -= 1
return r
Vous allez démontrer que cet algorithme calcule également la factorielle :
Démontrez la finitude du programme factorielle_variante.
corrigé
corrigé
$i$ est un nombre qui décroît de 1 à chaque itération : à une itération il sera forcément plus petit ou égal à 1.
Montrez que la boucle tant que de l'algorithme factorielle_variante admet l'invariant de boucle suivant :
Invariant de boucle : À la fin d'une itération de la boucle
tant que: $r = \frac{n!}{i!}$
corrigé
corrigé
- à la fin de la première itération $i = n - 1$ et $r = n = (i+1)$ : notre invariant est vérifié.
- on suppose la propriété vraie à la fin de la $k$ème itération. À la fin de l'itération suivante on a :
- $r' = r \cdot i$ (le $r$ de la fin de la $k+1$ème boucle est égal à celui de la fin de la $k$ème boucle fois le $i$ de la fin de $k$ème boucle)
- $i' = i - 1$ (le $i$ de la fin de la $k+1$ème boucle est le $i$ de la fin de la $k$ème boucle moins 1)
- $r = (i+1) \cdot \dots n$ (c'est notre invariant, vrai à la fin de l'itération $k$ par hypothèse)
- on a donc : $r' = ((i+1) \cdot \dots \cdot n) \cdot (i) = i \cdot \dots \cdot n = (i'+1) \cdot \dots \cdot n = \frac{n!}{i'!}$ : notre invariant est vérifié.
Déduire de l'exercice précédent que l'algorithme factorielle_variante calcule bien la factorielle de son entrée.
corrigé
corrigé
L'invariant étant vérifié à la fin de chaque itération, il est donc aussi vrai à la fin de la dernière itération. À ce moment-là, on a $i=1$ et donc $r = 1 \cdot 2 \cdot \dots \cdot n = n!$
Trouver un invariant
Vous trouverez ci-après une version itérative de l'algorithme de la division euclidienne que vous allez prouver par invariant de boucle.
algorithme division_euclidienne(a: int,
b: int) → []int:
ref r ← a
ref q ← 0
tant que r ≥ b:
r ← r - b
q ← q + 1
rendre [q, r]
code python
code python
def division_euclidienne(a, b):
r = a
q = 0
while r >= b:
r -= b
q += 1
return (q, r)
Le retour de la fonction python est un tuple à 2 éléments (c'est-à-dire un tableau à 2 éléments que l'on ne peut pas modifier).
Le retour de notre algorithme est un tableau à deux éléments.
Démontrez la finitude du programme division_euclidienne.
corrigé
corrigé
Si a et b sont des entiers positifs le programme s'arrête car :
rest un entierraprès une itération est strictement plus petit que leravant itération- on s'arrête si
rest strictement plus petit queb.
On veut montrer que l'on obtient bien une division euclidienne de $a$ par $b$, c'est-à-dire que $a = bq + r$ avec $r < b$. Regardez bien comment fonctionne la boucle de l'algorithme pour garantir ce résultat.
Proposez un invariant de boucle que vous démontrerez.
corrigé
corrigé
Invariant de boucle :
a = r + q * b
Prouvons l'invariant :
- l'invariant est bien vrai à la fin de la première boucle puisque $q=1$ et $r=a-b$ à ce moment-là.
- on doit prouver que
a' = r' + q' * b'à la fin de la $i+1$ème itération. - si l'on est passé dans la boucle on a
a'=a,r' = r - b,b' = betq' = q + 1 - donc
r' + q' * b' = r-b + (q+1) * b = r + q * b = a = a'puisque l'invariant est vrai à la fin de la $i$ème itération. On a biena' = r' + q' * b', l'invariant est démontré.
Utilisez votre invariant de boucle pour démontrer la correction de l'algorithme division_euclidienne.
corrigé
corrigé
L'invariant étant juste tout le temps, il l'est en particulier à l'issue de la dernière boucle. À ce moment-là on a a = r + qb avec r < b ce qui est bien ce qu'il fallait démontrer.
Créer un algorithme simple
Itératif
Créer et prouver un algorithme itératif de signature nombre(T: []int, x: int) → int qui rend le nombre d’occurrences d'une valeur dans un tableau d'entiers.
corrigé
corrigé
L'algorithme rend le nombre de fois où x est présent dans T
algorithme nombre(T: []int, x: int) → int:
ref nb ← 0
pour chaque e de T:
si e == x:
nb ← nb + 1
rendre nb
La finitude est claire puisqu'on a une unique boucle pour chaque qui a forcément un nombre fini d'itérations. On peut très bien dire que l'exactitude est claire aussi puisque l'on parcourt chaque valeur de T, mais si on veut être plus explicite on peut remarquer que :
nombre(T, x) = nombre(T[:-1], x) + 1siT[-1] == x,nombre(T, x) = nombre(T[:-1], x)siT[-1] ≠ x.
Et conclure avec une récurrence triviale sur la longueur du tableau.
Récursif
On a déjà vu une version itérative de cet algorithme, voyons (ou plutôt voyez) comment en faire une version récursive :
Donnez et prouvez un algorithme récursif de signature :
maximum_rec(T: []float, n: int) → int
Qui rend un indice $0 \leq j \leq n$ tel que $t[j] = \max({t[k] \vert 0\leq k \leq n})$.
corrigé
corrigé
algorithme maximum_rec(T: []float, n: int) → int:
ref x int
si n == 0:
rendre T[0]
sinon:
x ← maximum_rec(T, n-1)
si t[x] > t[n]:
rendre x
sinon:
rendre n
Le paramètre n est un entier qui diminue strictement. Il sera donc à un moment égal à 0 ce qui stoppera la récursion.
La correction se fait par récurrence sur n allant de n = 0 à n = T.longueur - 1.
- initialisation : pour
n = 0, le résultat est clair. - hypothèse de récurrence : on suppose que l'algorithme fonctionne pour
n - 1 - preuve pour
n - 1. Si l'algorithme fonctionne pourn - 1,xcontient l'indice max des indices allant de 0 àn - 1et on rendxsiT[x] > T[n]etnsinon : on rend bien l'indice de la valeur maximale du tableau.
Lorsque l'on crée des algorithmes récursifs, on a souvent besoin d'initialiser les paramètres. Ici, si on veut connaître le maximum d'un tableau en utilisant l'algorithme récursif maximum_rec(T: []float, n: int) → int, il faut écrire maximum_rec(T, T.longueur - 1). Le paramètre $n$ est un paramètre important pour la récursion mais inutile pour l'appel global.
Pour éviter d'avoir des paramètres inutiles on encapsulera la fonction récursive dans un algorithme dont le seul but est d'initialiser la récursion, c'est-à-dire que l'on appellera la fonction récursive via un algorithme dont le seul but est de fixer les paramètres de la récursion.
Encapsulez la fonction récursive maximum_rec(T: []float, n: int) → int dans un algorithme de signature maximum(T: []float) → int permettant de rendre le maximum d'un tableau passé en paramètre.
corrigé
corrigé
algorithme maximum(T: []int) → int
rendre maximum_rec(t, T.longueur - 1)
fonction maximum_rec(T: []float, n: int) → int:
ref x int
si n == 0:
rendre T[0]
sinon:
x ← maximum_rec(T, n-1)
si t[x] > t[n]:
rendre x
sinon:
rendre n
La technique de l'encapsulation est à utiliser dès que l'on a besoin de paramètres récursifs mais non utiles pour l'algorithme général.
Concaténation
Donnez et prouvez un algorithme itératif de signature :
concaténation(début: []int, fin: []int) → []int
Qui rend un nouveau tableau contenant la concaténation de début et de fin.
Vous utiliserez des invariants de boucle pour le prouver.
corrigé
corrigé
algorithme concaténation(début: []int, fin: []int) → []int
ref T ← [longueur: début.longueur + fin.longueur]int
ref i ← -1
pour chaque j de [0 .. début.longueur[:
i ← i + 1
T[i] ← début[j]
pour chaque j de [0 .. fin.longueur[:
i ← i + 1
T[i] ← fin[j]
rendre T
La finitude est claire puisqu'il n'y a que deux boucles de type pour chaque.
On a deux boucles, il faut donc a priori 2 invariants, un pour chaque boucle. Ici il est clair que l'on va remplir tous les éléments des tableaux début et fin dans t. L'invariant peut être :
Invariant de boucle : si $i_0$ est la valeur de $i$ à la ligne 5 (resp. 9) alors à chaque fin d'itération on a $t[i_0 + 1 + k] == \mbox{début}[k]$ (resp.
fin) pour tout $0\leq k \leq j$.
Démontrons l'invariant rigoureusement pour la première boucle.
À la fin de la première itération, on a bien :
- $t[i_0 + 1] == \mbox{début}[0]$
- $j = 0$
- $i = i_0 + 1 + j$
Supposons la propriété vraie après une itération donnée. Après une itération supplémentaire, les variables $i$ et $j$ ont été modifiées telles que :
- $j' = j + 1$
- $i' = i + 1 = i_0 + 1 + j'$
L'hypothèse de l'invariant de boucle stipule que $t[i_0 + k] == \mbox{début}[k]$ pour tout $0\leq k \leq j = j'-1$ et comme la boucle a effectué l'affectation : t[i'] ← début[j'], l'invariant continue d'être vérifié. Ce qui conclut la preuve.
Il faut juste faire très attention à l'endroit où l'on commence dans t. Ici c'est Ok car avant la première boucle i = -1 et i = début.longueur - 1 au début de la seconde boucle. Les éléments de début et fin ne vont pas se chevaucher dans t et se suivre sans interruption.
La preuve de l'invariant dans le corrigé est formelle mais évidente. Il n'est pas nécessaire (et on ne le fera plus) d'être aussi rigoureux pour des boucles aussi simples.
Lorsque le résultat d'une boucle est évident, il n'est pas nécessaire de faire une preuve formelle (qui sera souvent lourde et inintéressante).
Dans ces cas, contentez-vous de donner l'invariant ou le résultat de la boucle.
Égalité de tableaux
Intersection non vide
Écrivez un algorithme itératif de signature :
algorithme intersection_non_vide(T1: []int, T2: []int) → bool
Permettant de vérifier s'il existe un élément de T1 qui est égal à un élément de T2.
corrigé
corrigé
algorithme intersection_non_vide(T1: []int, T2: []int) → bool
pour chaque x de T1:
pour chaque y de T2:
si x == y:
rendre Vrai
rendre Faux
La boucle 3-5 va chercher à trouver x dans T2 : trouvé ne peut valoir Vrai que si c'est le cas. Si ce n'est pas le cas, x n'est pas dans T2 et il faut chercher une autre possibilité d'égalité.
Comme on répète cette boucle intérieure pour tout x de T1, l'algorithme ne peut arriver ligne 6 que si $x \notin T2$ pour tout $x \in T1$.
Prouver cet algorithme ne nécessite pas d'invariant de boucle formel.
Mêmes valeurs
Écrivez un algorithme itératif de signature :
algorithme égalité_valeurs(T1: []int, T2: []int) → bool
Permettant de vérifier que deux tableaux d'entiers T1 et T2 contiennent les mêmes valeurs.
Il faut vérifier que pour tout $0\leq i < T1.\text{\small longueur}$ il existe $0\leq i' < T2.\text{\small longueur}$ tel que $T1[i] = T2[i']$.
corrigé
corrigé
algorithme égalité_valeurs(T1: []int, T2: []int) → bool
ref trouvé bool
pour chaque x de T1:
trouvé ← Faux
pour chaque y de T2:
si x == y:
trouvé ← Vrai
si trouvé == Faux:
rendre Faux
rendre Vrai
Prouver cet algorithme ne nécessite pas d'invariant de boucle formel, mais il faut faire attention à sa construction.
La boucle 4-6 va chercher à trouver x dans T2 : trouvé ne peut valoir Vrai que si c'est le cas. Si ce n'est pas le cas, on peut sortir de l'algorithme (lignes 7-8) puisque x n'est pas dans T2.
Comme on répète cette boucle intérieure pour tout x de T1, l'algorithme est bien correct.
Permutations
Terminons cette partie avec l'algorithme suivant :
Écrivez un algorithme itératif de signature :
algorithme permutation(T1: []int, T2: []int) → bool
Permettant de vérifier que deux tableaux d'entiers $T$ et $T'$ contiennent les mêmes éléments (mêmes valeurs répétées le même nombre de fois), c'est-à-dire de savoir s'il existe une permutation $\sigma$ de $[0, T.\mbox{\small longueur}[$ telle que $T1[i] = T2[\sigma(i)]$ pour tout $i \in [0, T.\mbox{\small longueur}[$ (on n'a pas besoin de la donner).
Vous pourrez utiliser l'algorithme nombre.
corrigé
corrigé
Il faut pouvoir trouver tous les éléments de $T$ dans $T'$ autant de fois qu'ils sont dans $T$. En utilisant nombre, on a l'algorithme suivant :
algorithme égalité(T1: []int, T2: []int) → int
pour chaque x de T1:
si nombre(T1, x) ≠ nombre(T2, x):
rendre Faux
rendre Vrai
On ne peut rendre Vrai que si tous les éléments de T1 sont dans T2 avec un même nombre d'occurrences. Donc uniquement s'il existe une permutation $\sigma$ de $[0, n-1]$ telle que $T[i] = T[\sigma(i)]$ pour tout $i \in [0, n-1]$.
Suppression de valeurs
Donnez et prouvez un algorithme itératif de signature :
supprime(T: []int, v: int) → []int
Qui rend un nouveau tableau contenant la restriction de t aux valeurs différentes de v.
Vous pourrez utiliser l'algorithme nombre.
corrigé
corrigé
Pour que l'algorithme fonctionne, il faut commencer par connaître la taille du tableau à créer et donc compter le nombre d’occurrences de v dans T. Ensuite, il sera possible de remplir le tableau.
algorithme supprime(T: []int, v: int) → []int
ref T2 ← []int{longueur: taille t.longueur - nombre(T, v)}
ref j ← 0
pour chaque i de [0, t.longueur[:
si T[i] ≠ v:
T2[j] ← t[i]
j ← j + 1
rendre T2
La finitude du programme est claire puisque l'on n'a que des boucles de type pour chaque.
On commence par créer un tableau permettant exactement de stocker tous les éléments de T différents de v. Ce que fera la boucle. On peut prouver celle-ci par l'invariant :
Invariant de boucle : à la fin de chaque itération,
T2contient la restriction desi + 1premiers éléments deTdifférents dev.
La preuve de l'invariant est évidente et permet de prouver l'algorithme en se plaçant à la fin de la dernière itération.
Et maintenant la version récursive :
Donnez et prouvez un algorithme récursif de signature :
supprime_rec(T: []int, v: int) → []int
Qui rend un nouveau tableau contenant la restriction de T aux valeurs différentes de v.
Vous pourrez utiliser l'algorithme concaténation de la question précédente.
corrigé
corrigé
algorithme supprime_rec(t: []int, v: int) → []int
si t.longueur == 0:
rendre t
ref t2 ← []int{longueur: t.longueur - 1}
pour i de [0, t2.longueur[:
t2[i] ← t[i + 1]
si t[0] == v:
rendre concaténation([], supprime_rec(t2, v))
sinon:
rendre concaténation([t[0]], supprime_rec(t2, v))
Il n'y a qu'une seule récursion par algorithme et la taille du tableau passé en paramètre est strictement plus petite : il y a un nombre fini de récursions puisque la condition terminale est basée sur une taille nulle de tableau.
Pour la correction une récurrence immédiate sur la taille du tableau nous permet de conclure.
Retournement d'un tableau
Donnez et prouvez un algorithme récursif de signature :
retournement_indice(t: []int, i: int) → ∅
Qui modifie le tableau passé en entrée (il ne rend rien !) de telle sorte que les éléments $t[j]$ et $t[t.\mbox{longueur} - 1 - j]$ soient échangés pour tous $i \leq j < t.\mbox{longueur} - i$
corrigé
corrigé
algorithme retournement_indice(T: []int, i: int) → ∅
si T.longueur - 1 - i > i:
ref temp ← T[i]
T[i] ← T[T.longueur - 1 - i]
T[T.longueur - 1 - i] ← temp
retournement_indice(T, i + 1)
Les lignes 3-5 permettent d'échanger les valeurs t[i] et t[t.longueur - 1 - i] du tableau. On peut aussi écrire t[i], t[t.longueur - 1 - i] ← t[t.longueur - 1 - i], t[i], comme on le ferait en python, si l'on autorise le fait d'affecter 2 valeurs en même temps.
La finitude est claire puisque :
- il n'y a qu'un appel récursif
- chaque appel se rapproche strictement de la condition d'arrêt
La correction vient du fait que l'on échange bien des valeurs symétriques du tableau par rapport à l'indice du milieu du tableau qui est ici la partie entière de $t.\mbox{longueur} - 1/ 2$.
Lorsque l'on manipule des indices de tableau il faut toujours :
- attentivement vérifier que tout se passe bien.
- faire attention aux divisions entières.
Encapsulez la fonction récursive retournement_indice(t: []int, i: int) → ∅ pour qu'elle puisse retourner un tableau passé en paramètre.
corrigé
corrigé
fonction retournement_indice(T: []int, i: int) → ∅
... # code de la fonction récursive
algorithme retournement(T: []int) → ∅
retournement_indice(T, 0)
L'algorithme retournement_indice(T: []int, i: int) → ∅ est sous forme de récursion terminale, transformez-le en programme itératif.
En déduire un algorithme itératif permettant de retourner un tableau.
corrigé
corrigé
algorithme retournement_indice(t: []int, i: int) → ∅
ref temp int
tant que (t.longueur - 1 - i > i):
temp ← t[i]
t[i] ← t[t.longueur - 1 - i]
t[t.longueur - 1 - i] ← temp
i ← i + 1
Et donc en simplifiant l'écriture :
algorithme retournement(t: []int, i: int) → ∅
tant que (t.longueur - 1 - i > i):
t[i], t[t.longueur - 1 - i] ← t[t.longueur - 1 - i], t[i]
i ← i + 1
Palindromes
Donnez et prouvez un algorithme itératif de signature :
palindrome(s: str) → bool
Qui rend Vrai si le tableau est un palindrome.
corrigé
corrigé
algorithme palindrome(s: str) → bool
pour chaque i de [0, s.longueur // 2[:
si s[i] ≠ s[T.longueur - 1 - i]:
rendre Faux
rendre Vrai
La finitude et l'exactitude sont évidentes puisque l'on ne fait qu'implémenter la définition.
Et maintenant la version récursive :
Donnez et prouvez un algorithme récursif de signature :
palindrome_rec(s: str, i: int) → bool
Qui vérifie si :
sest un palindrome si $i=0$,s[i:-i]est un palindrome pour tout $1 \leq i \leq s.\text{longueur}-1$
On considérera qu'un tableau vide (ce qui est le cas si $s.\text{longueur} - 1 - i ≤ i$) est un palindrome.
corrigé
corrigé
algorithme palindrome_rec(s: str, i: int) → bool
si s.longueur - 1 - i ≤ i:
rendre Vrai
si s[i] ≠ s[s.longueur - 1 - i]:
rendre Faux
rendre palindrome_rec(s, i + 1)
Il faut montrer que l'algorithme rend Vrai si $s[j] = s[s.\mbox{longueur} - 1 - j]$ pour tous $i \leq j < s.\mbox{longueur} - 1 - i$
La première condition est la condition d'arrêt de la récursion puisque dans ce cas-là $i$ a dépassé la moitié du tableau.
Puis on utilise l'équation de récurrence :
palindrome(s) = (s[0] == s[-1]) ET palindrome(s[1:-1])
Encapsulez la fonction récursive palindrome_rec(s: str, i: int) → bool dans une fonction permettant de savoir si une chaîne de caractères est un palindrome.
corrigé
corrigé
fonction palindrome_rec(s: str, i: int) → bool
si s.longueur - 1 - i ≤ i:
rendre Vrai
si s[i] ≠ s[s.longueur - 1 - i]:
rendre Faux
rendre palindrome_rec(s, i + 1)
algorithme palindrome(T: str) → bool
rendre palindrome_rec(s, 0)
L'algorithme palindrome_rec(s: str, i: int) → bool devrait être écrit sous une forme de récursion terminale, transformez-le en programme itératif.
corrigé
corrigé
algorithme palindrome_pas_rec(s: str, i: int) → bool
tant que (s.longueur - 1 - i > i):
si s[i] ≠ s[s.longueur - 1 - i]:
rendre Faux
i ← i + 1
rendre Vrai
Construire un algorithme sous forme terminale avec un accumulateur
Écrivez sous la forme d'une récursion terminale l'algorithme de signature :
algorithme puissance_2(m: int, n: int) → int
Qui rend le plus grand entier de la forme $2^p \cdot m$ plus petit que $n$.
corrigé
corrigé
algorithme puissance_2(m: int, n: int) → int:
si 2 * m ≥ n:
rendre m
sinon:
rendre f(2 * m, n)
C'est bien une récursion terminale puisque le code est équivalent à :
fonction c(m: int, n: int) → bool:
rendre 2 * m ≥ n
fonction g(m: int, n: int) → int:
rendre m
algorithme puissance_2(m: int, n: int) → int:
si c(m, n) == Vrai:
rendre g(m, n)
sinon:
rendre f(2 * m, n)
Avec le type A étant un couple d'entiers et B le type entier.
La récursion terminale ne fait aucun calcul en propre, elle envoie de nouveaux paramètres. Cela semble très restrictif. Par exemple notre fonction factorielle récursive n'est pas sous forme terminale puisque l'on fait un calcul en plus de la récursion :
algorithme factorielle(n: int) → int:
si n == 1: # condition d'arrêt
rendre 1
ref f ← factorielle(n-1)
rendre n * f
Le rendre récursif terminal est impossible en ne gardant qu'un seul paramètre. L'astuce est d'ajouter un paramètre, appelé accumulateur qui va transmettre à la fonction récursive les calculs intermédiaires, dans notre cas le début du calcul de la factorielle. Dans notre cas :
fonction factorielle_term(n: int, acc: int) → int:
si n == 1:
rendre acc
rendre factorielle(n-1, n * acc)
algorithme factorielle(n: int # n > 1
) → int:
rendre factorielle_term(n, 1)
Montrer que l'algorithme précédent :
- est bien sous la forme terminale
- calcule bien la factorielle
corrigé
corrigé
Il est clairement sous la forme récursive terminale. Il finit car chaque récursion se rapproche strictement de la condition d'arrêt.
Enfin, acc va contenir la factorielle comme le prouve une récursion triviale sur $n$.
La récursion terminale est un moyen efficace d'implémenter des suites arithmétiques ou géométriques. Montrez-le :
Donnez et prouvez un algorithme écrit sous la forme d'une récursion terminale (avec accumulateur) permettant de calculer tout élément $u_n$ d'une suite arithmétique définie telle que :
corrigé
corrigé
fonction u_n_term(n: int,
u0: int,
r: int,
acc: int) → int:
si n == 0:
rendre u0 + acc
rendre u_n_term(n-1, u0, r, r + acc)
algorithme u_n(n : int,
u0: int,
r : int
) → int:
rendre u_n_term(n, u0, r, 0)
Transformez l'algorithme récursif précédent en un algorithme itératif (l'accumulateur devient une variable).
corrigé
corrigé
Le but de la récursion terminale est de facilement transformer un algorithme récursif en un algorithme itératif car la récursion est une boucle tant que.
algorithme u_n(n : int,
u0: int,
r : int
) → int:
ref acc ← 0
tant que n > 0:
acc ← r + acc
n ← n - 1
rendre u0 + acc
Transformer un algorithme récursif en un algorithme avec une récursion terminale revient à ajouter des variables à un programme récursif (ses variables sont ses paramètres).
La technique de l'accumulateur (ie. l'ajout de variables) est fondamentale pour la création d'algorithmes récursifs.
Fonction 91 de McCarthy
Terminons cette partie par une bizarrerie algorithmique comme on les aime. Elle vient de John McCarthy, informaticien de renom et créateur du langage Lisp en 1958. Lisp est le premier langage fonctionnel et a été le deuxième langage de programmation au monde, deux ans après le Fortran.
Définition :
Implémentez un algorithme récursif qui mime la définition.
corrigé
corrigé
algorithme M(n: int) → int:
si n > 100:
rendre n - 10
sinon:
ref x ← M(n + 11)
rendre M(x)
Mais est-ce que ce programme se termine ? Rien n'est moins sûr.
Le programme de la question précédente n'est pas forcément un algorithme puisqu'on ne sait pas si les récursions vont s'arrêter. Avant de répondre à cette question, montrons que l'on peut transformer cet algorithme en un programme itératif en appliquant les techniques de la récursion terminale :
Montrez que $M^k(n) = g(n, k)$ avec :
En déduire une version itérative pour résoudre McCarthy.
corrigé
corrigé
algorithme M(n: int) → int:
ref c ← 1
tant que c ≠ 0:
si n > 100:
n ← n - 10
c ← c - 1
sinon:
n ← n + 11
c ← c + 1
rendre n
On n'est pas vraiment sûr que cette fonction soit bien définie et donc que le programme ci-dessus soit un algorithme ! Montrons-le.
Démontrez que pour tout entier $n$ tel que $90 \leq n < 101$, on a $M(n) = M(n+1)$ et que l'on arrive à cette égalité en un nombre fini de récursions.
corrigé
corrigé
Comme on peut écrire cette fonction comme une récursion terminale, l'écriture itérative est aisée : Pour $90 \leq n < 101$, on a :
On arrive à ce résultat en 1 récursion : la ligne 5 de l'algorithme en pseudocode de la question précédente.
Déduire de l'exercice précédent que pour tout $n < 101$, il existe $k\geq 1$ et $90 \leq n' < 101$ tels que $M(n) = M^k(n')$.
corrigé
corrigé
Donnez la valeur de $M(n)$ pour tout $n< 101$ et en déduire que la fonction de McCarthy est bien définie pour tout entier positif (ie. le calcul se fait en un nombre fini de récursions).
corrigé
corrigé
On a $M(91) = M(92) = \dots = M(101) = 91$ et pour tout $n < 90$ il existera $k > 1$ et $91\leq n'< 101$ tels que $M(n) = M^k(n') = 91$
Comme il faut un nombre fini d'itérations pour passer de $M(n)$ à $M(n+1)$ on en déduit qu'il faut bien un nombre fini d'itérations pour calculer $M(91)$ (disons $I$), et donc également pour calculer $M^k(91)$ quel que soit $k>0$ (il en faut $k\cdot I$).
En déduire un algorithme itératif ultra simple pour la calculer.
corrigé
corrigé
algorithme M(n: int) → int:
si n > 100:
rendre n - 10
sinon:
rendre 91
Dans le même ordre d'idées que la fonction de Takeuchi, c'est un leurre.