Le concept de sous-problèmes superposés

23 Mar 2023 23 Mar 2023 1043 vues ESSADDOUKI Mostafa 11 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

Le concept de sous-problèmes superposés (Overlapping Subproblems)

Définition : Sous-problèmes superposés

Le concept de sous-problèmes superposés fait référence à une situation dans laquelle un problème peut être décomposé en sous-problèmes plus petits, mais certains de ces sous-problèmes sont résolus plusieurs fois lors de la résolution du problème initial. Cette répétition de sous-problèmes identiques est une caractéristique essentielle qui permet d'appliquer la programmation dynamique pour résoudre efficacement le problème.

Principe fondamental

Dans un problème avec des sous-problèmes superposés, la solution optimale au problème principal dépend des solutions optimales à un ensemble de sous-problèmes. Comme ces sous-problèmes sont résolus plusieurs fois, leur solution peut être stockée et réutilisée pour réduire le temps de calcul global.

Devise : "Ne calcule jamais deux fois la même chose"

Visualisation du concept

Sans superposition (Diviser pour régner)

Les sous-problèmes sont indépendants :

      Problème
      /       \
  Sous-Pb A  Sous-Pb B
     (unique)   (unique)
            

Exemple : Tri fusion (Merge Sort)

Avec superposition (Programmation dynamique)

Les sous-problèmes se chevauchent :

      Problème
      /       \
  Sous-Pb A  Sous-Pb B
      \       /
     Sous-Pb C (partagé)
            

Exemple : Fibonacci, plus court chemin

Exemple classique : La suite de Fibonacci

 Superposition évidente

Fibonacci : L'exemple parfait

La suite de Fibonacci est définie par :

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n-1) + F(n-2) pour n ≥ 2

Pour calculer le n-ième terme, on doit calculer les termes précédents, qui sont eux-mêmes calculés à partir des termes encore plus petits.

Arbre d'appels récursifs pour F(5)

F(5)
├── F(4)
│   ├── F(3)
│   │   ├── F(2)
│   │   │   ├── F(1)
│   │   │   └── F(0)
│   │   └── F(1)
│   └── F(2)
│       ├── F(1)
│       └── F(0)
└── F(3)
    ├── F(2)
    │   ├── F(1)
    │   └── F(0)
    └── F(1)

Les sous-problèmes en orange sont calculés plusieurs fois :
F(3) : 2 fois
F(2) : 3 fois
F(1) : 5 fois
F(0) : 3 fois

Total d'appels : 15
Appels uniques : 5
        
Problème de la récursion naïve

Dans la version récursive naïve, le nombre d'appels croît de façon exponentielle (O(2ⁿ)). Pour n=40, cela représente des milliards d'appels !

def fibonacci_naif(n):
    if n < 2:
        return n
    return fibonacci_naif(n-1) + fibonacci_naif(n-2)

# Pour n=40, environ 331 millions d'appels !
# Pour n=50, plus de 20 milliards d'appels !

Solution avec mémoisation (exploitation de la superposition)

def fibonacci_memo(n, memo={}):
    """
    Version avec mémoisation qui stocke les résultats intermédiaires
    pour éviter de recalculer les mêmes sous-problèmes
    """
    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]

# Dès que F(3) est calculé, il est stocké et réutilisé
# Complexité : O(n) temps, O(n) mémoire

Tableau des appels avec et sans optimisation

nAppels sans mémoisationAppels avec mémoisationGain
1017719
2021,89139561×
302,692,5375945,636×
40331,160,281794,191,000×
5020,365,011,07499205,700,000×

Exemple 2 : Plus court chemin dans un graphe

 Graphes

Plus court chemin

Pour déterminer le plus court chemin entre deux nœuds, il peut être nécessaire de résoudre plusieurs sous-problèmes similaires pour différents nœuds intermédiaires. Ces sous-problèmes se superposent naturellement.

Exemple concret : Chemin de A à D

Graphe :
A → B (5)
A → C (3)
B → C (1)
B → D (6)
C → D (2)

Question : Plus court chemin de A à D ?

Sous-problèmes :
- d(A,D) = min( d(A,B) + d(B,D), d(A,C) + d(C,D) )
- d(B,D) = min( d(B,C) + d(C,D), 6 )
- d(C,D) = 2
- d(B,C) = 1
- d(A,B) = 5
- d(A,C) = 3

Superposition : d(C,D) est utilisé à la fois dans d(A,D) et d(B,D) !

Algorithme de Floyd-Warshall : Stocke les distances entre toutes les paires de nœuds, exploitant la superposition pour réutiliser les résultats intermédiaires.

def floyd_warshall(graphe):
    """
    Algorithme de Floyd-Warshall pour les plus courts chemins entre toutes les paires
    """
    n = len(graphe)
    dist = [[float('inf')] * n for _ in range(n)]
    
    # Initialisation
    for i in range(n):
        dist[i][i] = 0
        for j in range(n):
            if graphe[i][j] != 0:
                dist[i][j] = graphe[i][j]
    
    # Programmation dynamique : exploitation des sous-problèmes superposés
    for k in range(n):  # Nœud intermédiaire
        for i in range(n):
            for j in range(n):
                # Si passer par k donne un chemin plus court
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
    
    return dist

Exemple 3 : Plus longue sous-séquence commune (LCS)

 Chaînes

LCS - Superposition évidente

Le problème de la plus longue sous-séquence commune entre deux chaînes présente de nombreux sous-problèmes superposés.

Pour deux chaînes X = "ABCBDAB" et Y = "BDCABA", la relation de récurrence est :

LCS(i, j) = 0 si i=0 ou j=0
LCS(i, j) = 1 + LCS(i-1, j-1) si X[i-1] = Y[j-1]
LCS(i, j) = max(LCS(i-1, j), LCS(i, j-1)) sinon

Arbre d'appels montrant la superposition

LCS(4,5)  (i=4, j=5)
    ├── LCS(3,5)  (sans X[3])
    │   ├── LCS(2,5)
    │   └── LCS(3,4)
    └── LCS(4,4)  (sans Y[4])
        ├── LCS(3,4)  ← MÊME SOUS-PROBLÈME QUE CI-DESSUS !
        └── LCS(4,3)

LCS(3,4) apparaît deux fois ! Sans mémoisation, il serait calculé deux fois.

Matrice de programmation dynamique pour LCS

        B  D  C  A  B  A
      0  0  0  0  0  0  0
    A 0  0  0  0  1  1  1
    B 0  1  1  1  1  2  2
    C 0  1  1  2  2  2  2
    B 0  1  1  2  2  3  3
    D 0  1  2  2  2  3  3
    A 0  1  2  2  3  3  4
    B 0  1  2  2  3  4  4

Chaque cellule dépend des cellules voisines (i-1,j-1), (i-1,j), (i,j-1).
En remplissant la matrice de haut en bas et de gauche à droite,
chaque sous-problème n'est résolu qu'une seule fois !

Exemple 4 : Nombre de chemins dans une grille

Problème

Combien de chemins uniques de (0,0) à (m,n) en se déplaçant uniquement vers la droite ou vers le bas ?

La relation de récurrence est :

chemins(i, j) = 1 si i=0 ou j=0
chemins(i, j) = chemins(i-1, j) + chemins(i, j-1) sinon

Visualisation de la superposition pour une grille 3×3

Position (2,2)
    ├── (1,2)
    │   ├── (0,2) = 1
    │   └── (1,1)
    │       ├── (0,1) = 1
    │       └── (1,0) = 1
    └── (2,1)
        ├── (1,1) ← MÊME SOUS-PROBLÈME QUE CI-DESSUS !
        └── (2,0) = 1

(1,1) apparaît deux fois !

Sans optimisation, la complexité est exponentielle. Avec programmation dynamique, on remplit une matrice :

    1  1  1
    1  2  3
    1  3  6
    

Comment reconnaître des sous-problèmes superposés ?

Indices de superposition
  • Récursion avec répétitions : L'arbre d'appels récursifs contient les mêmes sous-arbres à plusieurs endroits.
  • Relation de récurrence avec "max" ou "min" : Typique des problèmes d'optimisation où plusieurs chemins mènent aux mêmes états.
  • Nombre limité d'états : Le nombre de paramètres qui définissent un sous-problème est petit.
  • Mémorisation facile : On peut identifier un sous-problème par une clé (indices, position, etc.).

Familles de problèmes avec superposition naturelle

FamilleExemplesParamètres des sous-problèmes
SéquencesFibonacci, LCS, édition distanceIndices de début/fin dans les séquences
GrillesChemins dans une grille, robotCoordonnées (x, y)
GraphesPlus court chemin, Floyd-WarshallPaires de nœuds, nœuds intermédiaires
Sacs à dosKnapsack 0/1, subset sumCapacité restante, objets restants
ArbresDiamètre d'arbre, colorationNœuds, sous-arbres

Contre-exemple : Problèmes sans superposition

Quand n'y a-t-il PAS de superposition ?

Tous les problèmes récursifs n'ont pas des sous-problèmes superposés. L'approche "diviser pour régner" (divide and conquer) crée généralement des sous-problèmes indépendants.

Exemple : Tri fusion (Merge Sort)

tri_fusion([5,2,4,7,1,3,6,8])
├── tri_fusion([5,2,4,7])      ← Sous-problème A
│   ├── tri_fusion([5,2])
│   └── tri_fusion([4,7])
└── tri_fusion([1,3,6,8])      ← Sous-problème B (complètement indépendant de A)
    ├── tri_fusion([1,3])
    └── tri_fusion([6,8])

Aucun sous-problème n'est partagé entre les deux moitiés !

Dans le tri fusion, chaque sous-problème est unique. Il n'y a pas de gain à mémoriser les résultats car ils ne seront jamais réutilisés.

CaractéristiqueDiviser pour régnerProgrammation dynamique
Relation entre sous-problèmesIndépendantsSuperposés (dépendants)
Réutilisation des résultatsAucuneEssentielle
Structure de l'arbre d'appelsArbre binaire équilibréGraphe acyclique orienté (DAG)
ExemplesMerge sort, Quick sort, Recherche binaireFibonacci, LCS, Knapsack
 Exercice pratique

Identifier les sous-problèmes superposés

 Niveau : Intermédiaire

Pour chacun des problèmes suivants, déterminez s'ils présentent des sous-problèmes superposés et justifiez votre réponse :

  1. Problème A : Calcul de la factorielle n! (récursif).
  2. Problème B : Recherche dichotomique dans un tableau trié.
  3. Problème C : Algorithme de Dijkstra pour le plus court chemin depuis une source unique.
  4. Problème D : Problème du voyageur de commerce (TSP) - recherche de la tournée minimale.
  5. Problème E : Multiplication de chaînes de matrices (trouver le parenthésage optimal).
Points clés à retenir
  • Les sous-problèmes superposés sont une caractéristique essentielle des problèmes qui peuvent être résolus efficacement par programmation dynamique.
  • La superposition signifie que le même sous-problème est résolu plusieurs fois dans l'algorithme récursif naïf.
  • L'exemple classique est Fibonacci : F(3) est calculé plusieurs fois dans le calcul de F(5).
  • La programmation dynamique exploite la superposition en stockant les résultats des sous-problèmes pour les réutiliser (mémoisation ou tabulation).
  • La superposition transforme une complexité exponentielle en complexité polynomiale.
  • Les problèmes de type diviser pour régner (tri fusion, recherche dichotomique) n'ont généralement pas de superposition.
  • Pour identifier la superposition, dessinez l'arbre d'appels récursifs et cherchez les sous-arbres identiques.
  • La superposition combinée à la sous-structure optimale (vue dans le cours précédent) donne les deux conditions nécessaires pour appliquer la programmation dynamique.

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.