Complexité
1. Complexité
La complexité d’un algorithme mesure son efficacité en fonction de la taille de son entrée, soit en nombre d’étapes élémentaires (complexité en temps), soit en mémoire utilisée (complexité en espace).
a) Complexité dans le pire des cas
- On s’intéresse souvent à la complexité dans le pire des cas, c’est-à-dire le nombre maximal d’opérations pour une entrée de taille .
- Exemple : la fonction qui cherche un
1dans un tableau a une complexité linéaire dans le pire cas (aucun1ou1en dernière position).
La complexité dans le pire des cas ignore les cas favorables pour garantir une borne supérieure.
b) Compter approximativement et asymptotiquement
- On utilise la notation pour exprimer une borne supérieure asymptotique, en négligeant les constantes et termes de moindre importance.
- Formule :
- Si , alors .
Notation de Landau : comparaisons asymptotiques
| Notation | Signification | Formule | Interprétation |
|---|---|---|---|
| domine | pour grand | croît au plus comme | |
| même ordre | et | et croissent de façon comparable | |
| négligeable devant | croît strictement moins vite que |
c) Quelques ordres de grandeur (approximation du temps en secondes pour opérations/s)
| 10 | <1s | <1s | <1s | <1s | <1s | <1s | <4s |
| 30 | <1s | <1s | <1s | <1s | <1s | 18min | 10^25 ans |
| 50 | <1s | <1s | <1s | <1s | 11min | 36 ans | ∞ |
| 100 | <1s | <1s | <1s | 1s | 12.9 ans | 10^{17} | ∞ |
| 1000 | <1s | <1s | 1s | 18min | ∞ | ∞ | ∞ |
| 1000000 | 1s | 20s | 12 jours | 31710 ans | ∞ | ∞ | ∞ |
2. Exemples et pièges
a) Tri insertion
- Algorithme simple : on cherche le plus petit élément et on le place en tête, puis on recommence.
- Complexité : .
b) Complexité cachée
- Certaines opérations paraissent simples mais sont coûteuses selon la structure de données.
- Exemple en Python :
c in Sest si est une liste.c in Sest si est un dictionnaire (table de hachage).
- Toujours prendre en compte la structure de données pour évaluer la complexité.
3. Diviser pour régner
Technique consistant à diviser récursivement un problème en sous-problèmes plus petits, à les résoudre, puis à combiner les résultats.
a) Tri fusion
- Divise un tableau en deux moitiés, trie récursivement chaque moitié, puis fusionne.
- La fusion de deux tableaux triés est linéaire en la taille totale.
- Complexité récursive :
- Résolution :
b) Recherche dichotomique (à compléter dans la suite du cours)
- Recherche dans un tableau trié en divisant l’espace de recherche par deux à chaque étape.
- Complexité en temps : .
Diviser pour régner
1. Recherche dichotomique
- Principe : Diviser le tableau en deux à chaque étape en comparant l'élément central avec l'élément recherché.
- Complexité : car à chaque appel récursif, la taille du problème est divisée par 2.
- Algorithme (récursif) :
- Si l'intervalle est vide, retourner False.
- Sinon, comparer l'élément central avec l'élément recherché.
- Rechercher dans la moitié gauche ou droite selon le résultat.
2. Master Theorem
Pour une récurrence de la forme :
avec , la complexité asymptotique est :
| Cas | Condition | Complexité |
|---|---|---|
| 1 | ||
| 2 | ||
| 3 |
Exemples :
- Tri fusion : , , , , → cas 2 →
- Recherche dichotomique : , , , , → cas 2 →
3. Multiplication matricielle
- Addition de matrices :
- Produit naïf : triple boucle →
a) Diviser pour régner (sans astuce)
- Diviser chaque matrice en 4 sous-matrices
- Produit exprimé en 8 sous-produits
- Récurrence :
- Par Master Theorem, , cas 3 → (pas d'amélioration)
b) Strassen (avec astuce)
- Réduit à 7 sous-produits au lieu de 8, définis par :
- Produit final :
- Récurrence :
- , donc (meilleur que )
4. Enveloppe convexe
- Définition : L'enveloppe convexe d'un ensemble est le plus petit ensemble convexe contenant .
- Convexité : Pour tout , le segment est entièrement dans .
a) Marche de Jarvis
- Principe :
- Choisir le point le plus à gauche (et plus haut en cas d'égalité).
- Trouver le point qui forme l'angle le plus grand avec le segment précédent.
- Répéter jusqu'à revenir au point de départ.
- Complexité : dans le pire cas (quand tous les points sont sur l'enveloppe).
b) QuickHull (Diviser pour régner)
- Principe :
- Trouver les points extrêmes (gauche) et (droite).
- Diviser les points en deux ensembles selon leur position par rapport au segment .
- Pour chaque côté, trouver le point le plus éloigné du segment, qui appartient à l'enveloppe.
- Former un triangle avec et exclure les points à l'intérieur.
- Répéter récursivement sur les segments et .
- Complexité :
- Pire cas : (ex. points alignés).
- Cas moyen (points bien répartis) : → par Master Theorem.
5. Fonction auxiliaire QuickHull : séparation des points
- But : Séparer un ensemble de points en deux sous-ensembles à gauche et à droite d'un segment et trouver les points les plus éloignés de ce segment dans chaque sous-ensemble.
- Complexité :
- Méthode :
- Calculer la différence de pente (produit vectoriel) pour déterminer le côté.
- Calculer la distance (valeur absolue de la différence de pente) pour trouver le point le plus éloigné.
À retenir : Le paradigme diviser pour régner permet de réduire la complexité de certains algorithmes en divisant le problème en sous-problèmes plus petits, résolus récursivement, puis combinés. Le Master Theorem est l'outil clé pour analyser ces complexités récursives.
Graphes et parcours
1. Définitions fondamentales
- Graphe : un graphe est un ensemble de sommets et un ensemble d’arêtes .
- Graphe orienté : les arêtes ont une direction .
- Graphe non-orienté : pour toute arête , aussi.
2. Représentation des graphes
| Structure de données | Description | Complexité typique |
|---|---|---|
| Matrice d’adjacence | Matrice où si arête existe, sinon 0. |
- Vérifier arête :
- Lister voisins :
- Ajouter arête :
- Ajouter sommet : (recréation matrice) | | Listes d’adjacence | Pour chaque sommet, liste des voisins directs. |
- Vérifier arête : (nombre de voisins)
- Lister voisins :
- Ajouter arête :
- Ajouter sommet : | | Tables de hachage | Pour chaque sommet, table de hachage des voisins. |
- Vérifier arête : amorti
- Lister voisins : amorti
- Ajouter arête : amorti
- Ajouter sommet : amorti |
3. Opérations principales sur un graphe
- arete(i,j) : existe-t-il une arête de vers ?
- voisins(i) : liste des sommets accessibles directement depuis .
- ajouter_arete(i,j), ajouter_sommet(i), enlever_arete(i,j), enlever_sommet(i).
4. Parcours en largeur (BFS)
- Explore les sommets par couches successives à partir d’un sommet .
- Utilise une file (FIFO) pour gérer les sommets à visiter.
- Calcule la distance minimale en nombre d’arêtes entre et chaque sommet accessible.
Algorithme (simplifié) :
- Initialiser pour tout , .
- Enfiler dans la file .
- Tant que non vide :
- Dépiler .
- Pour chaque voisin de non visité :
- .
- Enfiler .
Complexité : .
Le parcours en largeur explore chaque arête au plus une fois.
5. Parcours en profondeur (DFS)
- Explore un chemin jusqu’à un cul-de-sac avant de revenir en arrière.
- Peut être implémenté récursivement ou itérativement (avec une pile LIFO).
Algorithme récursif (simplifié) :
- Marquer comme visité.
- Pour chaque voisin non visité de , appeler récursivement DFS sur .
Temps de parcours :
- On enregistre deux temps pour chaque sommet :
- : début de visite
- : fin de visite
- Les intervalles sont soit disjoints, soit inclus l’un dans l’autre (relation d’ancêtre-descendant dans l’arbre de parcours).
Complexité : .
6. Parcours en profondeur itératif
- Même principe que DFS récursif mais avec une pile.
- Empiler les voisins non visités, dépiler pour visiter.
7. Tri topologique
- Applicable aux graphes orientés sans cycle.
- Trouve un ordre linéaire des sommets tel que toutes les arêtes vont de gauche à droite.
- Propriété clé : si est accessible depuis , alors dans un DFS.
- Tri topologique obtenu en ordonnant les sommets par ordre décroissant de .
8. Résumé des complexités des opérations selon la structure
| Opération | Matrice d’adjacence | Listes d’adjacence | Tables de hachage (amorti) |
|---|---|---|---|
| arete(i,j) | |||
| voisins(i) | |||
| ajouter_arete(i,j) | |||
| ajouter_sommet(i) | |||
| enlever_arete(i,j) | |||
| enlever_sommet(i) |
À retenir : Les parcours en largeur et en profondeur permettent d’explorer efficacement un graphe en , et le tri topologique s’appuie sur les temps de fin de visite du DFS pour ordonner les sommets d’un graphe acyclique orienté.
Backtracking
1. Backtracking
Le backtracking est une méthode algorithmique pour résoudre des problèmes combinatoires en explorant toutes les solutions possibles, tout en abandonnant rapidement les branches qui ne peuvent pas mener à une solution valide.
2. Problème des n-reines
Objectif : Placer reines sur un échiquier sans qu'elles ne se menacent (pas sur la même ligne, colonne ou diagonale).
a) Parcours naïf des possibilités
- Tester toutes les positions possibles pour chaque reine.
- Complexité très élevée :
- L'algorithme explore toutes les combinaisons de positions, ce qui est inefficace.
b) Optimisation avec contraintes
- Chaque ligne contient exactement une reine.
- Pour la -ème reine, on ne choisit que parmi les colonnes non occupées.
- Complexité améliorée :
- L'algorithme évite de revisiter plusieurs fois les mêmes positions.
c) Arbre des possibilités
- Le backtracking correspond à un parcours en profondeur de l'arbre des configurations.
- Dès qu'une contrainte est violée, on prune le sous-arbre.
- Exemple : pour , on réduit les branches de 24 () à 6.
À retenir : Le backtracking explore un arbre de décisions en abandonnant les branches invalides pour réduire la complexité.
3. Application au Sudoku
- Grille à remplir avec chiffres 1 à 9.
- Contraintes :
- Chaque ligne, colonne, et sous-carré contient chaque chiffre une seule fois.
- Algorithme :
- Parcourir les cases vides une à une.
- Essayer chaque valeur possible.
- Si une contradiction apparaît, revenir en arrière (backtracking).
- Exploration uniquement des branches valides, évitant ainsi l'explosion combinatoire.
4. Programmation Dynamique (rappel lié)
- Permet d'éviter les calculs redondants en mémorisant les résultats intermédiaires.
- Exemple classique : suite de Fibonacci.
- Calcul naïf récursif : temps exponentiel.
- Calcul itératif avec mémorisation : temps linéaire .
5. Exemple de formule récursive : Plus longue sous-suite commune (PLSC)
- Soient deux mots et .
- Définition récursive de la longueur de la plus longue sous-suite commune :
-
Conditions initiales : .
-
Calcul via tableau dynamique de taille où sont les longueurs des mots.
À retenir : Le backtracking explore toutes les solutions possibles en abandonnant rapidement les branches invalides, ce qui permet de résoudre efficacement des problèmes combinatoires complexes comme les n-reines ou le Sudoku.
Programmation Dynamique
1. Plus Longue Sous-Suite Commune (PLSC)
- Définition : La PLSC entre deux mots et est la plus longue séquence de caractères apparaissant dans les deux mots dans le même ordre, mais pas nécessairement de façon contiguë.
- Tableau de programmation dynamique : représente la longueur de la PLSC entre les préfixes et .
- Conditions initiales : et pour tout .
- Relation de récurrence :
- Complexité : où , .
- Reconstruction d’une solution : On remonte dans le tableau depuis en suivant les règles :
- Diagonale si ,
- Sinon déplacement vers la case avec la même valeur parmi ou .
- Exemple : Pour et , la PLSC est "rthme".
2. Floyd-Warshall : Tous les Plus Courts Chemins
- Graphe pondéré : avec donnant le poids des arêtes.
- Objectif : Trouver la distance minimale entre tous les couples de sommets.
- Relation de récurrence (par sommets ajoutés) :
- Soit la distance minimale entre et en ne passant que par les sommets .
- Alors :
- Algorithme :
- Initialiser si , sinon .
- Itérer de 1 à en mettant à jour selon la relation ci-dessus.
- Complexité : .
- Autre formulation (plus classique) :
3. Mémoïsation
- Principe : Stocker les résultats des appels récursifs pour éviter les recalculs.
- Exemple : Calcul de la suite de Fibonacci avec mémoïsation.
- Gain : Réduit le nombre d’appels récursifs de à .
- Implémentation : Utilisation d’un dictionnaire ou tableau pour mémoriser les résultats déjà calculés.
4. Dominos sur une ligne
-
Problème : Compter le nombre de façons de remplir un rectangle avec des dominos et .
-
Approche : Utiliser plusieurs fonctions récursives dépendantes représentant différentes configurations partielles :
Fonction Description Nombre de manières de remplir un rectangle complet Nombre de manières de remplir avec une case supplémentaire en bas à droite Nombre de manières de remplir avec deux cases empilées verticalement en bas à droite Nombre de manières de remplir avec une case supplémentaire en haut à droite Nombre de manières de remplir avec deux cases empilées verticalement en haut à droite -
Relations de récurrence (avec symétries , ) :
-
Conditions initiales :
-
Calcul : On remplit les tableaux et pour .
À retenir : La programmation dynamique repose sur la décomposition d’un problème en sous-problèmes qui se recoupent, en stockant leurs solutions pour éviter les recalculs, ce qui permet de résoudre efficacement des problèmes complexes comme la PLSC, les plus courts chemins (Floyd-Warshall) ou le comptage de configurations combinatoires (dominos).
Réductions et NP-complétude
1. Encodage et équivalence d'encodages
- Encodage : fonction qui transforme un objet en mot binaire.
- Encodages équivalents : deux encodages sont équivalents s'il existe des fonctions calculables en temps linéaire telles que :
- Exemple : différentes représentations d'une formule booléenne (chaîne de caractères, liste de tables de hachage, liste de listes) sont équivalentes.
- Encodages non équivalents : ex. encodage binaire vs unaire des entiers, passage de binaire à unaire est exponentiel.
2. Problème (langage)
- Un problème est un sous-ensemble .
- Résoudre = trouver un algorithme tel que pour tout mot :
- Exemples classiques :
- Sat : formule booléenne sous forme normale conjonctive est-elle satisfiable ?
- SubsetSum : existe-t-il un sous-ensemble de dont la somme vaut ?
- Tiling : peut-on tuiler un carré avec un ensemble de tuiles respectant les couleurs des bords ?
- Plsc : existe-t-il une sous-séquence commune de longueur au moins entre deux mots ?
3. Réduction polynomiale
- Soient deux problèmes .
- se réduit à (notation ) s'il existe une fonction calculable en temps polynomial telle que :
- Conséquences :
- Si est résoluble en temps polynomial, alors aussi.
- Si n'a pas d'algorithme sous-exponentiel, alors non plus.
- La réduction est transitive : si et alors .
4. Exemples de réductions
| Réduction | Idée clé | Complexité |
|---|---|---|
| Sat SubsetSum | Construire un ensemble d'entiers et un objectif codant la satisfaction de la formule. Chaque variable correspond à deux entiers et , avec contraintes pour choisir une seule valeur et satisfaire toutes les clauses. | Construction en temps polynomial |
| Sat 3Sat | Transformer une clause avec plus de 3 littéraux en plusieurs clauses à 3 littéraux en introduisant des variables auxiliaires. | Transformation linéaire, taille multipliée par au plus |
| Sat LongestPath | Construire un graphe où chaque variable correspond à deux chemins, et où la longueur du plus long chemin encode la satisfaction de . | Construction polynomiale |
5. Classe NP
- Un problème est dans NP s'il existe une fonction de vérification calculable en temps polynomial telle que :
- est un certificat (solution candidate) vérifiable en temps polynomial.
- Exemple : pour Sat, est une assignation des variables, vérifiable en temps linéaire.
6. NP-complétude
- Un problème est NP-complet si :
- Tout problème se réduit à :
- Les problèmes NP-complets sont les plus difficiles de NP : résoudre un NP-complet en temps polynomial implique que tous les problèmes de NP sont résolubles en temps polynomial.
À retenir : La NP-complétude caractérise les problèmes les plus difficiles à résoudre dans NP, mais dont la solution peut être vérifiée rapidement. Les réductions polynomiales permettent de comparer la difficulté relative des problèmes.