Traduire du code en mathématiques
Avec une sémantique opérationnelle (plus ou moins) formelle , vous pouvez traduire littéralement le code (pseudo-) d'un algorithme en une expression mathématique qui vous donne le résultat, à condition que vous puissiez manipuler l'expression sous une forme utile. Cela fonctionne bien pour les mesures de coût additives telles que le nombre de comparaisons, les échanges, les instructions, les accès à la mémoire, les cycles de certaines machines abstraites, etc.
Exemple: Comparaisons dans Bubblesort
Considérons cet algorithme qui trie un tableau donné A
:
bubblesort(A) do 1
n = A.length; 2
for ( i = 0 to n-2 ) do 3
for ( j = 0 to n-i-2 ) do 4
if ( A[j] > A[j+1] ) then 5
tmp = A[j]; 6
A[j] = A[j+1]; 7
A[j+1] = tmp; 8
end 9
end 10
end 11
end 12
Supposons que nous voulions effectuer l'analyse de l'algorithme de tri habituel, à savoir compter le nombre de comparaisons d'éléments (ligne 5). Nous notons immédiatement que cette quantité ne dépend pas du contenu du tableau A
, mais de sa longueur . Nous pouvons donc traduire littéralement les boucles (imbriquées) en sommes (imbriquées); la variable de boucle devient la variable de somme et la plage est reportée. On a:nfor
Ccmp(n)=∑i=0n−2∑j=0n−i−21=⋯=n(n−1)2=(n2) ,
où est le coût pour chaque exécution de la ligne 5 (que nous comptons).1
Exemple: échanges dans Bubblesort
Je noterai par le sous-programme qui consiste en lignes vers et par les coûts d'exécution de ce sous-programme (une fois).Pi,ji
j
Ci,j
Maintenant, disons que nous voulons compter les échanges , c’est la fréquence à laquelle est exécutée. Il s’agit d’un "bloc de base", c’est un sous-programme qui est toujours exécuté de manière atomique et a un coût constant (ici, ). La sous-traitance de ces blocs est une simplification utile que nous appliquons souvent sans réfléchir ni en parler. 1P6,81
Avec une traduction similaire à celle ci-dessus, nous arrivons à la formule suivante:
Cswaps(A)=∑i=0n−2∑j=0n−i−2C5,9(A(i,j)) .
( i , j ) P 5 , 9A(i,j) indique l'état du tableau avant la -ième itération de .(i,j)P5,9
Notez que j'utilise au lieu de comme paramètre; on verra bientôt pourquoi. Je n’ajoute pas et tant que paramètres de car les coûts ne dépendent pas d’eux ici (dans le modèle de coûts uniformes , c’est-à-dire); en général, ils pourraient juste.n i j C 5 , 9AnijC5,9
Il est clair que les coûts de dépendent du contenu de (les valeurs et , plus précisément), nous devons donc en tenir compte. Nous sommes maintenant confrontés à un défi: comment "décompresser" ? Eh bien, nous pouvons rendre la dépendance sur le contenu de explicite: A C 5 , 9 AP5,9AA[j]
A[j+1]
C5,9A
C5,9(A(i,j))=C5(A(i,j))+{10,A(i,j)[j]>A(i,j)[j+1],else .
Pour tout tableau d'entrée donné, ces coûts sont bien définis, mais nous voulons un énoncé plus général. nous devons faire des hypothèses plus fortes. Laissez-nous enquêter sur trois cas typiques.
Le pire des cas
En regardant la somme et en notant que , nous pouvons trouver une limite supérieure triviale pour le coût:C5,9(A(i,j))∈{0,1}
Cswaps(A)≤∑i=0n−2∑j=0n−i−21=n(n−1)2=(n2) .
Mais cela peut-il arriver , c’est-à-dire qu’un est atteint pour cette limite supérieure? Il se trouve que oui: si nous entrons un tableau d’éléments distincts par paires inversement triés, chaque itération doit effectuer un swap¹. Par conséquent, nous avons calculé le nombre exact de swaps de Bubblesort dans le pire des cas .A
Le meilleur des cas
Inversement, il existe une limite inférieure triviale:
Cswaps(A)≥∑i=0n−2∑j=0n−i−20=0 .
Cela peut aussi arriver: sur un tableau déjà trié, Bubblesort n'exécute pas un échange.
Le cas moyen
Le pire et le meilleur des cas ouvrent un fossé considérable. Mais quel est le nombre typique de swaps? Afin de répondre à cette question, nous devons définir ce que signifie "typique". En théorie, nous n'avons aucune raison de préférer une entrée à une autre et nous supposons donc généralement une distribution uniforme de toutes les entrées possibles, c'est-à-dire que chaque entrée est également probable. Nous nous limitons aux tableaux avec des éléments distincts par paires et supposons ainsi le modèle de permutation aléatoire .
Ensuite, nous pouvons réécrire nos coûts comme ceci²:
E[Cswaps]=1n!∑A∑i=0n−2∑j=0n−i−2C5,9(A(i,j))
Nous devons maintenant aller au-delà de la simple manipulation des sommes. En examinant l'algorithme, nous constatons que chaque échange supprime exactement une inversion de (nous échangeons seulement les voisins³). Autrement dit, le nombre de swaps effectuées sur est exactement le nombre de retournements de . Ainsi, nous pouvons remplacer les deux sommes intérieures et obtenirAAinv(A)A
E[Cswaps]=1n!∑Ainv(A) .
Heureusement pour nous, il a été déterminé que le nombre moyen d’inversions est
E[Cswaps]=12⋅(n2)
qui est notre résultat final. Notez que cela représente exactement la moitié du coût le plus défavorable.
- Notez que l'algorithme a été soigneusement formulé pour que "la dernière" itération avec
i = n-1
la boucle externe qui ne fait jamais rien ne soit pas exécutée.
- " " est une notation mathématique pour "valeur attendue", qui est juste la moyenne.E
- Nous apprenons en cours de route qu'aucun algorithme qui permute seulement les éléments voisins peut être asymptotiquement plus rapide que Bubblesort (même en moyenne) - le nombre d'inversions est une limite inférieure pour tous ces algorithmes. Ceci s'applique par exemple au tri par insertion et au tri par sélection .
La méthode générale
Nous avons vu dans l'exemple que nous devons traduire la structure de contrôle en mathématiques; Je présenterai un ensemble typique de règles de traduction. Nous avons également vu que le coût d'un sous-programme donné peut dépendre de l' état actuel , c'est-à-dire (approximativement) des valeurs actuelles des variables. Comme l'algorithme modifie (généralement) l'état, la méthode générale est un peu lourde à noter. Si vous commencez à vous sentir confus, je vous suggère de revenir à l'exemple ou de créer le vôtre.
Nous notons avec l’état actuel (imaginez-le comme un ensemble d’assignations de variables). Lorsque nous exécutons un programme qui commence dans l’état , nous nous retrouvons dans l’état (à condition que se termine).ψP
ψψ/PP
Déclarations individuelles
S;
Si vous ne qu'une seule déclaration , vous lui affectez des coûts . Ce sera généralement une fonction constante.CS(ψ)
Expressions
Si vous avez une expression E
de la forme E1 ∘ E2
(par exemple, une expression arithmétique ∘
pouvant être addition ou multiplication, vous additionnez les coûts de manière récursive:
CE(ψ)=c∘+CE1(ψ)+CE2(ψ) .
Notez que
- le coût d'opération peut ne pas être constant mais dépend des valeurs de et etc∘E1E2
- l'évaluation d'expressions peut changer l'état dans de nombreuses langues,
vous devrez donc peut-être faire preuve de souplesse avec cette règle.
Séquence
Étant donné un programme P
sous forme de séquence de programmes Q;R
, vous ajoutez les coûts à
CP(ψ)=CQ(ψ)+CR(ψ/Q) .
Les conditions
Étant donné un programme P
de la forme if A then Q else R end
, les coûts dépendent de l'état:
CP(ψ)=CA(ψ)+{CQ(ψ/A)CR(ψ/A),A evaluates to true under ψ,else
En général, l’évaluation A
peut très bien changer l’état, d’où la mise à jour des coûts des différentes branches.
Boucles For
Étant donné un programme P
du formulaire for x = [x1, ..., xk] do Q end
, attribuer des coûts
CP(ψ)=cinit_for+∑i=1kcstep_for+CQ(ψi∘{x:=xi})
où est l'état avant le traitement de la valeur , c'est-à-dire après l'itération avec étant défini sur , ..., .ψiQ
xi
x
x1
xi-1
Notez les constantes supplémentaires pour la maintenance de la boucle; la variable de boucle doit être créée ( ) et attribuer ses valeurs ( ). Ceci est pertinent puisquecinit_forcstep_for
- le calcul de la prochaine
xi
peut être coûteux et
- une
for
boucle avec un corps vide (par exemple, après avoir simplifié dans le meilleur des cas avec un coût spécifique) n'a pas de coût nul si elle effectue des itérations.
Alors que les boucles
Étant donné un programme P
du formulaire while A do Q end
, attribuer des coûts
CP(ψ) =CA(ψ)+{0CQ(ψ/A)+CP(ψ/A;Q),A evaluates to false under ψ, else
En inspectant l'algorithme, cette récurrence peut souvent être bien représentée sous la forme d'une somme similaire à celle de for-loops.
Exemple: considérons cet algorithme court:
while x > 0 do 1
i += 1 2
x = x/2 3
end 4
En appliquant la règle, on obtient
C1,4({i:=i0;x:=x0}) =c<+{0c+=+c/+C1,4({i:=i0+1;x:=⌊x0/2⌋}),x0≤0, else
avec des coûts constants, pour les instructions individuelles. Nous supposons implicitement que ceux-ci ne dépendent pas de l'état (les valeurs de et ); Cela peut être vrai ou non dans la "réalité": pensez aux débordements!c…i
x
Nous devons maintenant résoudre cette récurrence pour . Nous notons que ni le nombre d'itérations ni le coût du corps de la boucle ne dépendent de la valeur de , nous pouvons donc le supprimer. Il nous reste cette récurrence:C1,4i
C1,4(x)={c>c>+c+=+c/+C1,4(⌊x/2⌋),x≤0, else
Cela résout avec des moyens élémentaires de
C1,4(ψ)=⌈log2ψ(x)⌉⋅(c>+c+=+c/)+c> ,
réintroduire symboliquement l'état complet; si , alors .ψ={…,x:=5,…}ψ(x)=5
Appels de procédure
Étant donné un programme P
de la forme M(x)
pour un paramètre (s) x
où M
est une procédure avec paramètre (nommé) p
, attribuer des coûts
CP(ψ)=ccall+CM(ψglob∘{p:=x}) .
Notez à nouveau la constante supplémentaire (qui peut en fait dépendre de !). Les appels de procédure sont coûteux en raison de la manière dont ils sont mis en œuvre sur de vraies machines, et dominent parfois même le temps d'exécution (par exemple, évaluer naïvement la récurrence des numéros de Fibonacci).ccallψ
Je passe sous silence certaines questions sémantiques que vous pourriez avoir avec l'état ici. Vous voudrez faire la distinction entre l'état global et les appels locaux à la procédure. Supposons que nous ne transmettons état global ici et M
obtient un nouvel état local, initialisé en définissant la valeur de p
la x
. De plus, x
peut être une expression que nous supposons (généralement) être évaluée avant de la transmettre.
Exemple: considérons la procédure
fac(n) do
if ( n <= 1 ) do 1
return 1 2
else 3
return n * fac(n-1) 4
end 5
end
Selon les règles, on obtient:
Cfac({n:=n0})=C1,5({n:=n0})=c≤+{C2({n:=n0})C4({n:=n0}),n0≤1, else=c≤+{creturncreturn+c∗+ccall+Cfac({n:=n0−1}),n0≤1, else
Notez que nous ne tenons pas compte de l'état global, car il est fac
clair qu'aucun accès à aucun. Cette récurrence particulière est facile à résoudre pour
Cfac(ψ)=ψ(n)⋅(c≤+creturn)+(ψ(n)−1)⋅(c∗+ccall)
Nous avons présenté les fonctionnalités linguistiques que vous rencontrerez dans un pseudo-code typique. Méfiez-vous des coûts cachés lorsque vous analysez un pseudo-code de haut niveau. en cas de doute, dépliez-vous. La notation peut sembler lourde et est certainement une question de goût; les concepts énumérés ne peuvent cependant pas être ignorés. Cependant, avec une certaine expérience, vous pourrez voir immédiatement quelles parties de l'état sont pertinentes pour quelle mesure de coût, par exemple la "taille du problème" ou le "nombre de sommets". Le reste peut être supprimé - cela simplifie considérablement les choses!
Si vous pensez maintenant que cela est beaucoup trop compliqué, être conseillé: il est ! Déduire les coûts exacts des algorithmes dans tout modèle si proche des machines réelles pour permettre des prédictions d'exécution (même relatives) est une tâche ardue. Et cela ne prend même pas en compte la mise en cache et autres effets pervers sur de vraies machines.
Par conséquent, l'analyse algorithmique est souvent simplifiée au point d'être mathématiquement traitable. Par exemple, si vous n'avez pas besoin de coûts exacts, vous pouvez surestimer ou sous-estimer à tout moment (pour les limites supérieures et inférieures): réduire l'ensemble des constantes, supprimer les conditions, simplifier les sommes, etc.
Une note sur le coût asymptotique
Ce que vous trouverez habituellement dans la littérature et sur le Web, c'est l'analyse "Big-Oh". Le terme approprié est l' analyse asymptotique , ce qui signifie qu'au lieu de calculer les coûts exacts comme nous l'avons fait dans les exemples, vous ne donnez les coûts que jusqu'à un facteur constant et dans la limite (grosso modo, "pour Big ").n
Ceci est (souvent) juste puisque les déclarations abstraites ont des coûts (généralement inconnus) en réalité, selon la machine, le système d'exploitation et d' autres facteurs, et à court runtimes peut être dominé par le système d'exploitation mise en place du processus en premier lieu et ainsi de suite. Donc, vous obtenez une perturbation, de toute façon.
Voici comment l'analyse asymptotique est liée à cette approche.
Identifiez les opérations dominantes (qui induisent des coûts), c’est-à-dire les opérations qui se produisent le plus souvent (jusqu’à facteurs constants). Dans l'exemple Bubblesort, un choix possible est la comparaison de la ligne 5.
Vous pouvez également lier toutes les constantes des opérations élémentaires à leur maximum (en haut), respectivement. leur minimum (par le bas) et effectuer l’analyse habituelle.
- Effectuez l'analyse en utilisant les comptes d'exécution de cette opération en tant que coût.
- Lors de la simplification, autorisez les estimations. Veillez à ne permettre les estimations à partir d'en haut que si votre objectif est une limite supérieure ( ), respectivement. d'en bas si vous voulez des limites inférieures ( ).OΩ
Assurez-vous de bien comprendre la signification des symboles Landau . Rappelez-vous que de telles limites existent pour les trois cas ; utiliser n'implique pas une analyse dans le pire des cas.O
Lectures complémentaires
L'analyse des algorithmes présente de nombreux défis et astuces. Voici quelques lectures recommandées.
Il existe de nombreuses questions liées à l'analyse d'algorithme qui utilisent des techniques similaires à celle-ci.