0Pricing
DSA Interview Prep · Leçon

Optimisation de l’espace pour la DP 2D

Réduisez l’espace de LCS et de la distance d’édition de O(mn) à O(min(m,n)) en ne conservant que les lignes courante et précédente du tableau de DP.

Optimisation de l’espace pour la DP 2D est une leçon DSA Interview Prep gratuite sur CoddyKit. Ceci est la leçon 4 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage DSA Interview Prep, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours DSA Interview Prep comprend 4 leçons au total.

Pourquoi l’espace est important dans le DP 2D

Une table DP 2D pour des chaînes de longueur 1000 nécessite 1000×1000 = 1 000 000 cellules, soit environ 8 MB pour des entiers sur 64 bits. Pour des séquences plus longues (alignement de l’ADN, comparaison de textes avec diff), cela devient impraticable. L’observation clé est que la plupart des relations de récurrence du DP 2D ne consultent que la ligne actuelle et la ligne précédente, de sorte que la table entière peut être compressée en un ou deux tableaux 1D. C’est le principe fondamental de l’optimisation de l’espace du DP 2D.

# Full 2D DP: O(mn) space
# LCS for 1000-char strings
m, n = 1000, 1000
dp_2d_size = m * n * 8  # bytes (64-bit ints)
print(f'2D table: {dp_2d_size:,} bytes = {dp_2d_size//1024} KB')

# 1D rolling array: O(n) space
dp_1d_size = n * 8
print(f'1D array: {dp_1d_size:,} bytes = {dp_1d_size} bytes')
print(f'Space saving: {dp_2d_size // dp_1d_size}x')

Schéma du tableau glissant

Le schéma du tableau glissant remplace la table 2D complète par un tableau 1D représentant la ligne précédente. Lors du calcul de la ligne i, vous mettez à jour chaque cellule j en utilisant la valeur actuelle dp[j] (qui contient encore dp[i-1][j] de la ligne précédente) et la valeur de dp[j-1] qui vient d’être mise à jour (c’est-à-dire dp[i][j-1]). Une variable diagonal capture dp[i-1][j-1] avant son remplacement. Ce schéma s’applique au LCS, à la distance d’édition et à la plupart des problèmes de DP 2D.

# Rolling array template for 2D DP
# Before update: dp[j] holds dp[i-1][j] (previous row)
# After update: dp[j] holds dp[i][j] (current row)

def rolling_array_template(grid):
    m, n = len(grid), len(grid[0])
    dp = [0] * (n + 1)  # represents one row
    for i in range(1, m + 1):
        diag = 0  # stores dp[i-1][j-1] before overwrite
        for j in range(1, n + 1):
            temp = dp[j]  # save dp[i-1][j] before overwriting
            # compute dp[i][j] using dp[j] (above) and dp[j-1] (left) and diag
            dp[j] = diag + dp[j] + dp[j-1]  # placeholder logic
            diag = temp
    return dp[n]

LCS avec un espace en O(min m,n)

Pour le LCS, assurez-vous que text1 est la chaîne la plus courte (ainsi, n est petit). Allouez un tableau 1D de taille n+1. Traitez les lignes une par une. À chaque cellule : enregistrez temp = dp[j] (il s’agit de dp[i-1][j]). Ensuite : si les caractères correspondent, dp[j] = diag + 1 ; sinon, dp[j] = max(dp[j], dp[j-1]). Enfin, définissez diag = temp. Après le traitement de toutes les lignes, dp[n] contient la longueur du LCS.

def lcs_space_opt(text1, text2):
    # Ensure text2 is the shorter one
    if len(text1) < len(text2):
        text1, text2 = text2, text1
    m, n = len(text1), len(text2)
    dp = [0] * (n + 1)
    for i in range(1, m + 1):
        diag = 0
        for j in range(1, n + 1):
            temp = dp[j]  # dp[i-1][j]
            if text1[i-1] == text2[j-1]:
                dp[j] = diag + 1
            else:
                dp[j] = max(dp[j], dp[j-1])
            diag = temp
    return dp[n]

print(lcs_space_opt('ABCBDAB', 'BDCABA'))  # 4
print(lcs_space_opt('AGGTAB', 'GXTXAYB')) # 4

Distance d’édition avec un espace en O(n)

La distance d’édition utilise le même schéma glissant. Le tableau 1D initial représente la ligne 0 : dp[j] = j (insérer j caractères). Pour chaque ligne i, définissez dp[0] = i (supprimer i caractères) et enregistrez diag = dp[0] avant la mise à jour. Dans la boucle interne, enregistrez temp = dp[j], calculez la nouvelle valeur à partir de l’insertion (dp[j-1]+1), de la suppression (dp[j]+1) et du remplacement (diag + cost), puis définissez diag = temp.

def edit_dist_opt(s, t):
    m, n = len(s), len(t)
    dp = list(range(n + 1))   # row 0: dp[0][j] = j
    for i in range(1, m + 1):
        diag = dp[0]           # dp[i-1][0] before dp[0] update
        dp[0] = i              # dp[i][0] = i
        for j in range(1, n + 1):
            temp = dp[j]       # dp[i-1][j]
            cost = 0 if s[i-1] == t[j-1] else 1
            dp[j] = min(
                dp[j-1] + 1,  # insert
                dp[j] + 1,    # delete
                diag + cost   # replace or match
            )
            diag = temp
    return dp[n]

print(edit_dist_opt('horse', 'ros'))  # 3
print(edit_dist_opt('intention', 'execution'))  # 5

Somme minimale d’un chemin avec un espace O(n)

Pour calculer la somme minimale d’un chemin sur une grille, le tableau glissant 1D commence par les sommes préfixes de la première ligne (un seul chemin permet d’atteindre chaque cellule de la première ligne). Pour chaque ligne suivante, mettez à jour de gauche à droite : dp[j] avant la mise à jour contient la valeur de la ligne précédente (dp[i-1][j]), tandis que dp[j-1], qui vient d’être mis à jour, contient la valeur de la cellule de gauche. Aucune diagonale n’est nécessaire ici, car la somme minimale d’un chemin ne requiert pas la cellule diagonale.

def min_path_sum_opt(grid):
    m, n = len(grid), len(grid[0])
    dp = [float('inf')] * n
    dp[0] = 0
    for i in range(m):
        # Update first column (only from above)
        dp[0] += grid[i][0]
        for j in range(1, n):
            # min of above (dp[j] = old) and left (dp[j-1] = updated)
            dp[j] = grid[i][j] + min(dp[j], dp[j-1])
    return dp[n-1]

grid = [[1,3,1],[1,5,1],[4,2,1]]
print(min_path_sum_opt(grid))  # 7

Quand l’accès à la diagonale est nécessaire

Tous les problèmes de DP 2D ne peuvent pas être compressés avec un simple tableau glissant, car certains nécessitent l’élément diagonal dp[i-1][j-1] après que dp[j] a été écrasé. La correction est toujours la même : enregistrez temp = dp[j] avant de le mettre à jour, puis utilisez cette valeur comme diag pour le calcul de la colonne suivante. Cette anticipation d’une cellule permet de gérer proprement toutes les récurrences à trois directions (LCS, distance d’édition).

# Recap: the diagonal save pattern
# Without it: dp[j-1] updated (left) and dp[j] about to be overwritten
# With it:

def show_diagonal_pattern(s1, s2):
    n = len(s2)
    dp = [0] * (n + 1)
    for ch1 in s1:
        diag = 0  # was dp[i-1][0] = 0 for LCS
        for j, ch2 in enumerate(s2, 1):
            temp = dp[j]  # SAVE before overwrite
            if ch1 == ch2:
                dp[j] = diag + 1  # use saved diagonal
            else:
                dp[j] = max(dp[j], dp[j-1])
            diag = temp  # advance diagonal
    return dp[n]

print(show_diagonal_pattern('ABCBDAB', 'BDCABA'))  # 4

Optimisation de l’espace du sac à dos 2D

Le problème du sac à dos 0/1 bénéficie lui aussi d’une optimisation de l’espace. La table 2D complète a pour dimensions (n_items+1) × (capacity+1). Le tableau glissant la réduit à O(capacity). La différence essentielle par rapport à LCS et à la distance d’édition est la suivante : parcourez la dimension de la capacité en sens inverse (de la plus grande à la plus petite). Cela garantit que chaque élément est compté au plus une fois ; un parcours dans le sens direct permettrait de sélectionner un même élément plusieurs fois.

def knapsack_01(weights, values, capacity):
    dp = [0] * (capacity + 1)
    for w, v in zip(weights, values):
        # Reverse order: prevents using the same item twice
        for c in range(capacity, w - 1, -1):
            dp[c] = max(dp[c], dp[c - w] + v)
    return dp[capacity]

weights = [1, 3, 4, 5]
values  = [1, 4, 5, 7]
cap = 7
print(knapsack_01(weights, values, cap))  # 9 (items 3+4: weight 3+4=7, value 4+5=9)

Parcours dans le sens direct ou inverse

Il est essentiel de savoir dans quel sens parcourir la boucle interne : en sens inverse pour le sac à dos 0/1 (chaque élément est utilisé au plus une fois ; consulter les états précédents empêche de le réutiliser), et dans le sens direct pour le sac à dos illimité (chaque élément peut être réutilisé ; consulter les états déjà mis à jour permet plusieurs utilisations). Une erreur à ce niveau transforme silencieusement un problème 0/1 en problème illimité, ou l’inverse. Vérifiez toujours la contrainte avant de choisir le sens du parcours.

# 0/1 Knapsack: each item used AT MOST ONCE → iterate reverse
def knapsack_01_demo(weights, values, cap):
    dp = [0] * (cap + 1)
    for w, v in zip(weights, values):
        for c in range(cap, w-1, -1):  # REVERSE
            dp[c] = max(dp[c], dp[c-w] + v)
    return dp[cap]

# Unbounded Knapsack: items can be reused → iterate forward
def knapsack_unbounded(weights, values, cap):
    dp = [0] * (cap + 1)
    for c in range(1, cap + 1):
        for w, v in zip(weights, values):
            if c >= w:
                dp[c] = max(dp[c], dp[c-w] + v)  # FORWARD
    return dp[cap]

print(knapsack_01_demo([2,3],[3,4],5))     # 7
print(knapsack_unbounded([2,3],[3,4],5))   # 8 (use weight-2 twice: 3+3=6? or 4+... )

Chemins uniques avec un espace O(n)

Pour les chemins uniques, toute la table peut être remplacée par une seule ligne. Initialisez toutes les cellules à 1 (la première ligne). Pour chaque ligne suivante, mettez à jour de gauche à droite : dp[j] += dp[j-1]. Aucune diagonale n’est nécessaire, car la récurrence utilise uniquement la cellule située au-dessus (dp[j], sa valeur avant la mise à jour) et celle située à gauche (dp[j-1], déjà mise à jour). Il s’agit de la compression 2D→1D la plus simple.

def unique_paths_opt(m, n):
    dp = [1] * n  # first row: all 1s
    for i in range(1, m):
        for j in range(1, n):
            dp[j] += dp[j-1]  # above (dp[j]) + left (dp[j-1])
    return dp[n-1]

# With obstacles
def unique_paths_obstacles_opt(grid):
    m, n = len(grid), len(grid[0])
    dp = [0] * n
    dp[0] = 1
    for i in range(m):
        if grid[i][0] == 1: dp[0] = 0  # blocked column
        for j in range(1, n):
            if grid[i][j] == 1: dp[j] = 0  # blocked
            else: dp[j] += dp[j-1]
    return dp[n-1]

print(unique_paths_opt(3, 7))  # 28
print(unique_paths_obstacles_opt([[0,0,0],[0,1,0],[0,0,0]]))  # 2

Tampon de deux lignes pour les récurrences complexes

Lorsque la récurrence nécessite des cellules provenant de deux lignes précédentes ou davantage (par exemple, dans certaines variantes de DP sur intervalles ou dans des réductions de DP 3D), utilisez un tampon de deux lignes : conservez les tableaux prev et curr, puis échangez-les après chaque ligne. Vous obtenez ainsi un espace O(2n) = O(n). Pour les récurrences qui remontent de k lignes, conservez k tableaux dans un tampon circulaire. Cette méthode généralise le modèle du tableau glissant à une ligne.

def lcs_two_row_buffer(s1, s2):
    m, n = len(s1), len(s2)
    prev = [0] * (n + 1)  # dp[i-1]
    curr = [0] * (n + 1)  # dp[i]
    for i in range(1, m + 1):
        curr[0] = 0
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                curr[j] = prev[j-1] + 1
            else:
                curr[j] = max(prev[j], curr[j-1])
        prev, curr = curr, prev  # swap (curr becomes prev)
    return prev[n]  # after swap, prev holds the last computed row

print(lcs_two_row_buffer('ABCBDAB', 'BDCABA'))  # 4

Quand l’optimisation de l’espace est impossible

L’optimisation de l’espace n’est pas toujours possible. Si vous devez reconstruire la solution optimale, et pas seulement sa valeur, vous avez généralement besoin de la table complète pour effectuer le retour arrière. Parmi les solutions possibles : (1) stocker une table de décisions distincte de même taille ; (2) utiliser l’algorithme de Hirschberg, qui calcule LCS en O(mn) avec un espace O(min(m,n)), reconstruction comprise, en divisant récursivement le problème au niveau du point médian ; (3) accepter un espace O(mn) lorsque la reconstruction est nécessaire.

# When reconstruction needed: must keep full table or use Hirschberg
# Hirschberg's idea: compute LCS length in O(n) space at midpoint of s1,
# recurse on left and right halves. O(mn) time, O(n) space + reconstruction.

# For interview: mention the trade-off
# 'I can reduce to O(n) space if only the value is needed.
#  To also reconstruct the sequence, I need the full O(mn) table
#  or a more complex divide-and-conquer approach.'

print('Space opt: O(n) for length only')
print('Full table: O(mn) needed for reconstruction')

Vérification rapide

Évaluez votre compréhension des concepts de structures de données & algorithmes — préparation aux entretiens de programmation présentés dans cette leçon.

Récapitulatif de la leçon

Dans cette leçon, vous avez appris que : les tables DP 2D peuvent être compressées en un espace O(n) à l’aide d’un tableau 1D glissant lorsque seule la ligne précédente est nécessaire, le modèle de la variable diagonale (enregistrer temp avant l’écrasement) gère les récurrences qui nécessitent dp[i-1][j-1], et le sac à dos 0/1 parcourt la capacité en sens inverse, tandis que le sac à dos illimité la parcourt dans le sens direct. Nous allons maintenant étudier le gabarit du retour arrière : choisir, explorer, annuler — le fondement des algorithmes de recherche exhaustive.

Questions Fréquemment Posées

La leçon « Optimisation de l’espace pour la DP 2D » est-elle gratuite ?

Oui — le texte complet de « Optimisation de l’espace pour la DP 2D » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours DSA Interview Prep, passe à CoddyKit PRO. Le cours DSA Interview Prep comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Optimisation de l’espace pour la DP 2D » ?

Réduisez l’espace de LCS et de la distance d’édition de O(mn) à O(min(m,n)) en ne conservant que les lignes courante et précédente du tableau de DP. Tu pratiques DSA Interview Prep avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer DSA Interview Prep ?

Aucune expérience préalable n'est requise. DSA Interview Prep sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 4 sur 4.

Combien de temps prend la leçon « Optimisation de l’espace pour la DP 2D » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon DSA Interview Prep ?

Oui. Chaque leçon DSA Interview Prep inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Chemins uniques et somme minimale des chemins sur des grilles
  2. Plus longue sous-séquence commune
  3. Distance d’édition (Levenshtein)
  4. Optimisation de l’espace pour la DP 2D
← Retour à DSA Interview Prep