1) En notant à chaque ligne l'état de
bits après l'appel, puis le nombre de bits modifiés :
[1, 0, 0, 0],
1 ;
[0, 1, 0, 0],
2 ;
[1, 1, 0, 0],
1 ;
[0, 0, 1, 0],
3 ;
[1, 0, 1, 0],
1 ;
[0, 1, 1, 0],
2 ;
[1, 1, 1, 0],
1 ;
[0, 0, 0, 1],
4.
Le coût total de ces huit appels vaut
1+2+1+3+1+2+1+4=15, soit moins de deux écritures par appel, alors que le huitième appel à lui seul en a coûté quatre.
2) Le
k-ième appel part de l'entier
k−1. Notons
t le plus petit indice dont le bit vaut
0 dans l'écriture binaire de
k−1 ; les bits d'indices
0,…,t−1 valent donc tous
1. Un tel indice existe : la précondition garantit que le compteur ne déborde pas, donc l'écriture de
k−1 sur la longueur de la liste comporte au moins un
0.
La boucle
while s'arrête au premier indice où le bit vaut
0 : elle effectue donc exactement
t tours, écrivant
t zéros, puis l'instruction qui la suit écrit un
1 à l'indice
t. Le nombre de bits modifiés vaut
t+1.
Montrons que
t=v2(k). Par définition de
t, les bits d'indices
0 à
t−1 de
k−1 valent
1 et celui d'indice
t vaut
0, donc il existe un entier naturel
a tel que
k−1=(∑i=0t−12i)+2t+1a=(2t−1)+2t+1a.
Alors
k=2t+2t+1a=2t(1+2a), et
1+2a est impair. Par unicité de la décomposition,
v2(k)=t.
Le
k-ième appel modifie donc exactement
v2(k)+1 bits. Les huit valeurs de la question 1 le confirment :
1,2,1,3,1,2,1,4 sont bien les
v2(k)+1 pour
k de
1 à
8.
3) D'après la question 2,
T(m)=∑k=1m(v2(k)+1).
Soit
k≥1. Un entier
j≥0 vérifie «
2j divise
k » si et seulement si
j≤v2(k), donc ces entiers
j sont exactement
0,1,…,v2(k) : il y en a
v2(k)+1. Ainsi
v2(k)+1=card{j∈N : 2j divise k}.
Donc
T(m) est le nombre de couples
(j,k) tels que
j≥0,
1≤k≤m et
2j divise
k. Comptons-les cette fois à
j fixé, ce qui revient à intervertir deux sommes finies (thème Sommes et produits finis, bloc Fondements 1) : les multiples de
2j compris entre
1 et
m sont
2j,2×2j,…, et ils sont au nombre de
⌊2jm⌋.
Donc
T(m)=∑j≥0⌊2jm⌋. Dès que
2j>m, le terme est nul : seuls les indices
j tels que
2j≤m contribuent, et ils sont en nombre fini.
4)
Minoration. Tous les termes de la somme sont positifs et celui d'indice
j=0 vaut
⌊m⌋=m, donc
T(m)≥m.
Majoration. Notons
J le plus grand entier
j tel que
2j≤m ; les termes d'indice
j>J sont nuls. Comme
⌊x⌋≤x pour tout réel
x,
T(m)=∑j=0J⌊2jm⌋≤∑j=0J2jm=m∑j=0J(21)j=m(2−2J1)≤2m,
en utilisant la somme des termes d'une suite géométrique de raison
21.
Ainsi
m≤T(m)≤2m pour tout
m≥1. Avec
c=1,
C=2 et
m0=1, on conclut
T(m)=Θ(m).
Le coût amorti d'un appel,
mT(m), est donc compris entre
1 et
2 : il est
Θ(1). Incrémenter un compteur binaire coûte, en moyenne sur la suite des appels, moins de deux écritures de bits.
Vérification. On mesure
T(8)=15,
T(100)=197,
T(1000)=1994 et
T(4096)=8191, soit des coûts amortis de
1,875,
1,97,
1,994 et
1,9998 : ils restent bien sous
2, dont ils s'approchent.
5) L'appel qui fait passer le compteur de
2p−1 à
2p modifie
v2(2p)+1=p+1 bits, car tous les bits d'indices
0 à
p−1 valent
1 et doivent être remis à zéro.
Posons
p=⌊log2m⌋. Comme
2p≤m, cet appel a bien lieu parmi les
m premiers, et il coûte
⌊log2m⌋+1 écritures, c'est-à-dire
Θ(logm).
Aucune contradiction. La garantie de la question 4 porte sur le coût
total de la suite des
m appels, pas sur chacun d'eux. Un appel peut coûter cher pourvu que ces accidents soient assez rares pour que leur somme reste linéaire, et c'est exactement ce que mesure la somme de la question 3 : les appels dont le coût atteint ou dépasse
t+1 sont ceux dont l'indice est multiple de
2t, et ils sont au nombre de
⌊2tm⌋, donc deux fois plus rares chaque fois que
t augmente de
1.
Pourquoi « moyenne » serait impropre. Aucune loi de probabilité n'intervient ici : on n'a rien tiré au hasard, et la suite des
m appels est parfaitement déterminée. Le coût amorti est un coût total rapporté au nombre d'opérations, garanti dans le pire des cas sur cette suite ; la complexité en moyenne, elle, est une espérance sous une loi annoncée sur les entrées. Confondre les deux, c'est prêter à l'une la garantie de l'autre.
6) Il suffit d'ajouter un bit lorsque la boucle a parcouru toute la liste, c'est-à-dire lorsque tous les bits valaient
1.
def incrementer_extensible(bits):
"""Ajoute 1 au compteur binaire bits, modifié sur place, en l'allongeant d'un bit
s'il déborde, et renvoie le nombre de bits modifiés.
Précondition : bits est une liste de 0 et de 1."""
j = 0
modifies = 0
while j < len(bits) and bits[j] == 1:
bits[j] = 0
modifies = modifies + 1
j = j + 1
if j < len(bits):
bits[j] = 1
else:
bits.append(1)
modifies = modifies + 1
return modifies
Le nombre d'écritures de bits est
inchangé : l'
append remplace exactement l'écriture du bit d'indice
j, qui n'existait pas encore, et le raisonnement des questions 2 à 4 s'applique mot pour mot. Le coût en écritures reste
T(m)=∑j≥0⌊2jm⌋, donc
Θ(m).
Ce qui s'ajoute est le coût propre des agrandissements de la liste. Un débordement se produit exactement lorsque tous les bits valent
1 avant l'appel, c'est-à-dire lorsque le compteur vaut
2ℓ−1 où
ℓ est la longueur courante : en partant de la liste vide, cela arrive aux appels d'indices
1,2,4,8,…, donc
⌊log2m⌋+1 fois au cours des
m premiers appels. Chacun exécute un
append, dont le coût est
Θ(1) amorti.
Le coût total vérifie donc
T(m)≤Ctot(m)≤λ(T(m)+⌊log2m⌋+1)+μ pour deux constantes. Comme
⌊log2m⌋+1≤m et
T(m)≤2m, on obtient
m≤Ctot(m)≤3λm+μ, donc
Ctot(m)=Θ(m) : le coût amorti reste constant.
Vérification. En partant de la liste vide,
m appels donnent bien un compteur qui représente
m, de longueur
⌊log2m⌋+1, et le coût total mesuré vaut
1,
3,
4,
15,
197 et
1994 pour
m valant
1,
2,
3,
8,
100 et
1000, c'est-à-dire les valeurs de
T(m) trouvées plus haut.
À retenir : un coût amorti se démontre en majorant le coût
total d'une suite d'opérations, ici en comptant les couples d'une somme double, et non en examinant une opération isolée.