Définition de la programmation dynamique

23 Mar 2023 23 Mar 2023 1446 vues ESSADDOUKI Mostafa 9 min de lecture
Complexité algorithmique
1 Introduction à l'analyse des algorithmes 2 Notations asymptotiques 3 Opérations élémentaires et modèles de coût 4 Complexité temporelle et spatiale 5 Méthode de comptage des pas 6 Méthode de comptage des pas pour les boucles
Diviser pour régner & algorithmes de tri
7 Rappel sur l'approche récursive 8 Diviser pour régner 9 la recherche dichotomique 10 Tri par fusion 11 Tri rapide 12 Analyse des fonctions récursives
Les arbres binaires
13 Introduction aux arbres binaires 14 Définitions récursives des arbres 15 Propriétés des arbres binaires 16 Types d'arbre binaire 17 Parcours en profondeur des arbres binaires 18 Parcours en largeur des arbres binaires 19 Exercices corrigés sur les arbres - TD 1 20 Exercices corrigés sur les arbres - TD 2 21 Exercices corrigés sur les arbres - TD 3 22 DS - Arbres binaires de recherche équilibrés (AVL) 23 DS - Codage de Huffman - Compression de données
Algorithmes gloutons
24 Introduction aux algorithmes gloutons 25 Problème de la sélection d'activités 26 Problème de séquencement des tâches 27 Problème du Sac à Dos fraction
Programmation dynamique
28 Introduction à la programmation dynamique 29 Le concept de sous-structure optimale 30 Le concept de sous-problèmes superposés 31 Méthodes de la programmation dynamique 32 Différence entre la programmation dynamique, l'approche diviser pour régner, et les algorithmes gloutons 33 Calculer les nombres de catalan en C++ et Python 34 Calculer le coefficient binomial 35 Le nombre de façons pour construire un mur de dimension 4*N 36 Défi de conversion de mots 37 Décomposition de phrases à partir d'un dictionnaire 38 La collection de pièces dans un labyrinthe 39 Nombre de façons de regrouper les étudiants 40 Compter tous les chemins possibles dans une grille MxN
Méta heuristique
41 Algorithmes heuristiques et métaheuristiques 42 Algorithme de Recuit simulé 43 Algorithme Colonies de fourmis
Théorie des graphes
44 Introduction et notions fondamentales 45 Chemins, cycles et connexité 46 Représentations des graphes 47 Parcours de graphes 48 Algorithme de Dijkstra 49 Algorithme A* 50 Algorithme de Bellman-Ford 51 Algorithme Floyd-Warshall 52 Coloration des graphes
Base de données et SQL
53 Introduction au langage SQL 54 Concepts de base de SGBDR 55 Syntaxes de différentes instructions SQL 56 Création et suppression d'une base de données 57 Opérateurs SQL 58 Les contraintes en SQL 59 Création et suppression des tables en SQL 60 Insertion et modifications des enregistrements 61 Extraction des données - SELECT 62 Filtrer les données - WHERE 63 Recherche de motifs - LIKE 64 Trier les données - ORDER BY 65 Les jointures en SQL - JOIN 66 Fonctions d'agrégation en SQL - SUM, COUNT, AVG, MIN et MAX 67 Organiser des données identiques en groupes - GROUP BY et HAVING 68 Les sous-requêtes en SQL 69 Les fonctions SQL de manipulation de date 70 DS - langage SQL 71 Exercices corrigés de langage SQL
Introduction à l'intelligence artificielle
Introduction à la théorie des jeux
Préparation aux concours
72 Réseau de distribution d'eau 73 Arbre d'expression arithmétique 74 Exploration d'une grotte souterraine

Introduction à la programmation dynamique

Définition : Programmation dynamique

La programmation dynamique est une méthode algorithmique utilisée pour résoudre des problèmes d'optimisation, en particulier ceux qui présentent une structure de sous-problèmes imbriqués ou une nature récursive. Elle s'appuie sur la décomposition d'un problème complexe en sous-problèmes plus petits, en stockant les solutions intermédiaires pour éviter de les recalculer à chaque étape.

Principe fondamental

La programmation dynamique repose sur deux concepts clés :

  • Sous-structure optimale : Une solution optimale du problème global contient des solutions optimales à ses sous-problèmes.
  • Sous-problèmes superposés : Les mêmes sous-problèmes sont résolus plusieurs fois dans l'approche récursive naïve.

En exploitant ces propriétés, la programmation dynamique évite les calculs redondants et réduit considérablement la complexité temporelle.

Les deux approches de la programmation dynamique

La programmation dynamique suit deux approches principales :

 Approche descendante

Top-down (Mémoïsation)

Cette approche commence par résoudre le problème initial et s'appuie sur la résolution récursive des sous-problèmes. Elle utilise une structure de données (comme un tableau ou un dictionnaire) pour stocker les résultats des sous-problèmes déjà résolus, évitant ainsi les calculs redondants.

Caractéristiques :

  • Récursive (appels récursifs)
  • Cache (mémorisation) des résultats
  • Ne calcule que les sous-problèmes nécessaires
  • Plus naturelle à implémenter à partir de la solution récursive

Exemple avec Fibonacci :

def fibonacci_memo(n, memo={}):
    if n in memo:
        return memo[n]
    if n < 2:
        return n
    memo[n] = fibonacci_memo(n-1, memo) + fibonacci_memo(n-2, memo)
    return memo[n]
 Approche ascendante

Bottom-up (Tabulation)

Cette approche consiste à résoudre d'abord les sous-problèmes les plus simples et à progresser vers la résolution du problème initial. Elle construit une table de solutions aux sous-problèmes en ordre croissant de complexité, en utilisant les résultats précédents pour résoudre les problèmes suivants.

Caractéristiques :

  • Itérative (boucles)
  • Tableau rempli systématiquement
  • Calcule tous les sous-problèmes (même ceux non nécessaires)
  • Souvent plus efficace (pas d'overhead de récursion)

Exemple avec Fibonacci :

def fibonacci_tabu(n):
    if n < 2:
        return n
    tab = [0] * (n + 1)
    tab[1] = 1
    for i in range(2, n + 1):
        tab[i] = tab[i-1] + tab[i-2]
    return tab[n]

Comparaison Top-down vs Bottom-up

CaractéristiqueTop-down (Mémoïsation)Bottom-up (Tabulation)
NatureRécursiveItérative
Ordre de calculÀ la demande (lazy)Systématique (eager)
StockageCache partiel (seulement les valeurs calculées)Tableau complet (souvent toutes les valeurs)
RisquesDépassement de pile (récursion profonde)Calcul inutile de sous-problèmes non nécessaires
Facilité d'implémentationPlus simple à partir de la version récursiveParfois plus complexe à concevoir

Applications et importance de la programmation dynamique

La programmation dynamique est un outil puissant et polyvalent pour résoudre divers problèmes d'optimisation et de recherche. Ses applications et son importance sont largement reconnues dans de nombreux domaines :

Informatique et algorithmique

La programmation dynamique est couramment utilisée pour concevoir des algorithmes efficaces pour résoudre des problèmes complexes :

  • Recherche du plus court chemin (Floyd-Warshall, Bellman-Ford)
  • Plus longue sous-séquence commune (LCS)
  • Problème du sac à dos (Knapsack)
  • Multiplication de chaînes de matrices
  • Partitionnement de nombres
  • Algorithme de Viterbi (traitement du langage)
Planification et ordonnancement

Dans des domaines tels que la gestion de projets, la logistique et les systèmes de production :

  • Optimisation des tâches de planification
  • Ordonnancement avec contraintes de temps
  • Gestion des ressources limitées
  • Optimisation des coûts de production
Intelligence artificielle

La programmation dynamique est un élément essentiel de nombreuses techniques d'IA :

  • Planification de trajectoires pour les robots
  • Apprentissage par renforcement (équation de Bellman)
  • Résolution de processus de décision markoviens (MDP)
  • Recherche heuristique (algorithme A*)
Finance et économie

La programmation dynamique est appliquée à divers problèmes financiers :

  • Gestion de portefeuille
  • Évaluation d'options (modèle de Black-Scholes)
  • Modélisation de décisions d'investissement
  • Optimisation sous contraintes budgétaires
Bioinformatique

Dans le domaine du vivant, la programmation dynamique est essentielle :

  • Alignement de séquences (Needleman-Wunsch, Smith-Waterman)
  • Recherche de motifs dans l'ADN
  • Prédiction de structures de protéines
Recherche opérationnelle

Optimisation de problèmes concrets :

  • Gestion des stocks
  • Optimisation des réseaux de transport
  • Allocation de ressources

Problèmes classiques résolus par programmation dynamique

ProblèmeDescriptionComplexité
Suite de FibonacciCalcul du n-ième terme de la suiteO(n)
Plus long chemin dans une grilleNombre de chemins de (0,0) à (m,n)O(m × n)
Plus longue sous-séquence commune (LCS)Trouver la plus longue séquence commune à deux chaînesO(m × n)
Sac à dos (Knapsack)Maximiser la valeur d'objets sans dépasser la capacitéO(n × capacité)
Multiplications de chaînes de matricesTrouver le parenthésage optimal pour minimiser les opérationsO(n³)
Plus courts chemins (Floyd-Warshall)Distances entre toutes les paires de sommetsO(n³)
Édition distance (Levenshtein)Nombre minimum d'opérations pour transformer une chaîne en une autreO(m × n)
Découpe de tiges (Rod cutting)Maximiser le profit de la découpe d'une tigeO(n²)

Exemple détaillé : Problème de la découpe de tiges (Rod cutting)

 Problème classique

Découpe de tiges

Énoncé : On dispose d'une tige de longueur n et d'un tableau de prix p[i] pour une tige de longueur i. Déterminer la façon de découper la tige pour maximiser le revenu total.

Exemple : Pour n = 4 et les prix :

  • Longueur 1 : 1€
  • Longueur 2 : 5€
  • Longueur 3 : 8€
  • Longueur 4 : 9€

La solution optimale est de découper en deux morceaux de longueur 2 (5€ + 5€ = 10€) plutôt que de vendre la tige entière (9€).

Solution récursive naïve

def rod_cutting_naif(n, prix):
    """
    Version récursive naïve - très inefficace (exponentielle)
    """
    if n == 0:
        return 0
    
    revenu_max = -1
    for i in range(1, n + 1):
        revenu_max = max(revenu_max, prix[i] + rod_cutting_naif(n - i, prix))
    
    return revenu_max

# Prix indicés de 1 à n (prix[1] = prix pour longueur 1)
prix = [0, 1, 5, 8, 9]  # prix[0] est ignoré
print(rod_cutting_naif(4, prix))  # 10

Solution avec mémoïsation (Top-down)

def rod_cutting_memo(n, prix, memo=None):
    """
    Version avec mémoïsation - O(n²)
    """
    if memo is None:
        memo = {}
    
    if n == 0:
        return 0
    
    if n in memo:
        return memo[n]
    
    revenu_max = -1
    for i in range(1, n + 1):
        revenu_max = max(revenu_max, prix[i] + rod_cutting_memo(n - i, prix, memo))
    
    memo[n] = revenu_max
    return revenu_max

prix = [0, 1, 5, 8, 9]
print(rod_cutting_memo(4, prix))  # 10

Solution avec tabulation (Bottom-up)

def rod_cutting_tabu(n, prix):
    """
    Version avec tabulation - O(n²)
    """
    # Tableau des revenus maximaux pour chaque longueur
    r = [0] * (n + 1)
    
    # Calcul itératif des solutions
    for j in range(1, n + 1):
        revenu_max = -1
        for i in range(1, j + 1):
            revenu_max = max(revenu_max, prix[i] + r[j - i])
        r[j] = revenu_max
    
    return r[n]

prix = [0, 1, 5, 8, 9]
print(rod_cutting_tabu(4, prix))  # 10

# Version qui donne aussi la solution (les découpes)
def rod_cutting_with_solution(n, prix):
    r = [0] * (n + 1)
    s = [0] * (n + 1)  # Stocke la taille du premier morceau
    
    for j in range(1, n + 1):
        revenu_max = -1
        for i in range(1, j + 1):
            if revenu_max < prix[i] + r[j - i]:
                revenu_max = prix[i] + r[j - i]
                s[j] = i
        r[j] = revenu_max
    
    # Reconstruction de la solution
    decoupes = []
    while n > 0:
        decoupes.append(s[n])
        n -= s[n]
    
    return r[0], decoupes

revenu, decoupes = rod_cutting_with_solution(4, [0, 1, 5, 8, 9])
print(f"Revenu maximal : {revenu}, Découpes : {decoupes}")  # [2, 2]

Avantages de la programmation dynamique

  • Réduction de la complexité temporelle : Transforme des algorithmes exponentiels en algorithmes polynomiaux.
  • Évitement des calculs redondants : Grâce à la mémorisation ou à la tabulation, chaque sous-problème n'est résolu qu'une seule fois.
  • Solution exacte : Contrairement aux heuristiques, la programmation dynamique garantit une solution optimale.
  • Applicabilité large : Peut être appliquée à une grande variété de problèmes d'optimisation.
  • Structure claire : La relation de récurrence fournit un cadre mathématique précis.

Limitations de la programmation dynamique

Points de vigilance
  • Consommation mémoire : Le stockage des résultats intermédiaires peut nécessiter beaucoup de mémoire (compromis temps-mémoire).
  • Problèmes sans sous-structure optimale : La programmation dynamique ne peut pas être appliquée si cette propriété n'est pas vérifiée.
  • Dimensionnalité : Pour les problèmes avec de nombreux paramètres, la table peut devenir trop grande (fléau de la dimension).
  • Complexité de conception : Trouver la bonne relation de récurrence et les bons états peut être difficile.
  • Non applicable aux problèmes NP-difficiles : Pour les problèmes NP-difficiles comme le TSP, la programmation dynamique reste exponentielle (bien que meilleure que la force brute).
Points clés à retenir
  • La programmation dynamique est une méthode d'optimisation pour les problèmes avec sous-structure optimale et sous-problèmes superposés.
  • Deux approches : top-down (mémoïsation) et bottom-up (tabulation).
  • La mémoïsation est récursive avec cache ; la tabulation est itérative avec tableau.
  • La programmation dynamique réduit la complexité temporelle d'exponentielle à polynomiale.
  • Applications dans de nombreux domaines : algorithmique, intelligence artificielle, finance, bioinformatique, etc.
  • Problèmes classiques : Fibonacci, LCS, sac à dos, plus court chemin, découpe de tiges.
  • Compromis temps-mémoire : la programmation dynamique utilise plus de mémoire pour gagner du temps.
  • Nécessite de bien identifier les états et la relation de récurrence.

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.