mots clés : algorithmique, programmation Python
🔎 Voir les exercices similaires ↓
Antilles-guyane – Bac NSI 2026 – Jour 1 Énoncé de l'exercice 2
Cet exercice porte sur l’algorithmique et la programmation Python.
La recherche d’une plus longue sous-liste commune à deux listes est un problème connu sous le nom de « Longest Common Subsequence » (LCS).
Les éléments d’une plus longue sous-liste commune sont des éléments communs aux deux listes initiales, où ils apparaissent dans le même ordre, mais pas nécessairement de façon contiguë. Ainsi, les listes L_1 = [9, 3, 7, 5, 8] et L_2 = [9, 7, 8, 3, 7, 3] ont notamment en commun les sous-listes [], [9], [3], [9, 7], [9, 3, 7] ou [9, 7, 8]. Les plus longues sous-listes communes à ces deux listes sont [9, 3, 7] et [9, 7, 8], de longueur commune égale à 3.
En Python, il est possible d’obtenir la concaténée de deux valeurs de types list en utilisant +. Par exemple, [1, 2] + [3, 4] s’évalue à [1, 2, 3, 4] et [] + [1] s’évalue à [1].
Partie A
Dans cette partie, on s’attache à déterminer une LCS selon la stratégie de « recherche exhaustive ». La première fonction implémentée construit toutes les sous-listes d’une liste passée en paramètre, en appliquant l’algorithme suivant :
Fonction sous_listes(L: list, accumulateur: list[list])
-> list[list]
Début
| Si L est vide alors
| | renvoyer l'accumulateur (initialement égal à [[]])
| Sinon
| | Extraire e le dernier élément de L
| | Créer L_t une liste temporaire vide
| | Ajouter dans L_t chaque sous-liste présente dans
| | l'accumulateur, après lui avoir ajouté, en tête, e
| | Renvoyer le résultat d'un nouvel appel à la fonction
| | avec pour paramètres
| | - la liste diminuée de cet élément e
| | - l'union des éléments de l'accumulateur et de la
| | liste temporaire
| FinSi
Fin
Recopier et compléter le tableau suivant, en indiquant la valeur des paramètres lors des appels récursifs, pour un appel initial sous_listes([9, 3, 7], [[]]).
| Évolution des paramètres lors des appels récursifs | ||
| Appel | liste | accumulateur |
| appel initial | [9, 3, 7] | [[]] |
| 1er sous-appel | [9, 3] | [[], [7]] |
| 2nd sous-appel | ||
| dernier sous-appel | ||
La fonction suivante implémente l’algorithme étudié précédemment.
def sous_listes(liste: list, accumulateur = [[]]):
if len(liste) == 0:
return accumulateur
element = liste.pop()
tmp = []
for sous_liste in accumulateur:
tmp.append(...) # ligne 7
return sous_listes(...) # ligne 8
Recopier et compléter les lignes 7 et 8 du code de la fonction sous_listes.
On implémente la fonction est_extraite ci-dessous qui renvoie True si une sous-liste peut être extraite d’une liste donnée et False sinon.
def est_extraite(sous_liste: list, liste: list):
index_ss_liste, index_liste = 0, 0
while index_ss_liste < len(sous_liste) and index_liste < len(liste):
if sous_liste[index_ss_liste] == liste[index_liste]:
index_ss_liste += 1
index_liste += 1
return index_ss_liste == len(sous_liste)
On implémente la fonction lcs_force_brute ci-dessous qui détermine une plus longue sous-liste commune à deux listes données et qui renvoie cette liste.
def lcs_force_brute(liste_1: list, liste_2: list):
tmp = sous_listes(liste_1)
resultat, maxi = [], 0
for element in tmp:
if est_extraite(element, liste_2) and len(element) > maxi:
resultat = element
maxi = len(element)
return resultat
Expliquer pourquoi l’exécution de la fonction lcs_force_brute risque de prendre beaucoup de temps selon la longueur des listes passées en paramètre. Justifier.
Partie B
Soient m et n deux entiers naturels, m′ et n′ des entiers tels que 0 ≤ m′ < m et 0 ≤ n′ < n. On suppose, dans cette partie, disposer d’une fonction lcs qui, pour deux listes quelconques L_1 et L_2, de tailles respectives m et n, et des entiers m′ et n′ passés en paramètres, renvoie une plus longue sous-liste commune à :
la sous-liste de L_1 constituée des m′ premiers éléments ;
et la sous-liste de L_2 constituée des n′ premiers éléments.
Ainsi,
lcs([9, 3, 7, 5, 8], 0, [9, 7, 8, 3, 7, 3], 0) est une plus longue sous-liste commune à deux listes vides, soit [] ;
lcs([9, 3, 7, 5, 8], 3, [9, 7, 8, 3, 7, 3], 2) est une plus longue sous-liste commune à [9, 3, 7] et [9, 7], soit la sous-liste [9, 7].
Indiquer ce que renvoie l’appel suivant :
lcs([9, 3, 7, 5, 8], 1, [9, 7, 8, 3, 7, 3], 1)
On suppose, dans cette question, que L_1 et L_2 sont des listes non vides dont les derniers éléments sont identiques : m ≥ 1, n ≥ 1, L_1[m-1] et L_2[n-1] égaux.
Exemple : m = 5, n = 3, L_1 = [9, 3, 7, 5, 8] et L_2 = [9, 7, 8].
Écrire la relation de récurrence pour obtenir une plus longue sous-liste commune aux deux listes L_1 et L_2 à partir du résultat de lcs(L_1, m-1, L_2, n-1).
A contrario, lorsque les derniers éléments des listes sont différents (m ≥ 1, n ≥ 1, L_1[m-1] et L_2[n-1] distincts), une plus longue sous-suite commune aux deux listes L_1 et L_2 s’obtient en choisissant une plus grande liste parmi les résultats de lcs(L_1, m-1, L_2, n) et lcs(L_1, m, L_2, n-1).
Par exemple, avec m = 5, n = 4, L_1 = [9, 3, 7, 5, 8] et L_2 = [9, 7, 8, 3], on compare les longueurs d’une LCS de [9, 3, 7, 5] et L_2 = [9, 7, 8, 3] (soit [9, 7]) et d’une LCS de L_1 = [9, 3, 7, 5, 8] et [9, 7, 8] (soit [9, 7, 8]), ce qui permet d’obtenir la LCS [9, 7, 8] pour L_1 et L_2.
On implémente donc la fonction suivante qui prend en paramètres deux listes et qui renvoie une plus longue sous-liste commune :
def lcs_top_down(liste_x: list, liste_y: list):
def aux(liste_x, index_x, liste_y, index_y):
if (index_x < 0) or (index_y < 0):
return []
if liste_x[index_x] == liste_y[index_y]:
return aux(liste_x, index_x-1, liste_y, index_y-1) + [liste_x[index_x]]
resultat_1 = aux(liste_x, index_x, liste_y, index_y-1)
resultat_2 = aux(liste_x, index_x-1, liste_y, index_y)
if len(resultat_1) >= len(resultat_2):
return resultat_1
return resultat_2
return aux(liste_x, len(liste_x)-1, liste_y, len(liste_y)-1)
Justifier que la fonction aux contenue dans la fonction lcs_top_down est récursive.
Détailler le risque d’un tel choix dans la présente situation, lorsque les listes L_1 et L_2 sont très grandes.
Proposer une solution pour éviter le problème évoqué à la question 7.
Antilles-guyane – Bac NSI 2026 – Jour 1 Correction de l'exercice 2
Partie A – Recherche exhaustive
1. Évolution des paramètres
| Appel | liste | accumulateur |
|---|---|---|
| appel initial | [9, 3, 7] | [[]] |
| 1er sous-appel | [9, 3] | [[], [7]] |
| 2nd sous-appel | [9] | [[], [7], [3], [3, 7]] |
| dernier sous-appel | [] | [[], [7], [3], [3, 7], [9], [9, 7], [9, 3], [9, 3, 7]] |
2. Lignes 7 et 8 de sous_listes
def sous_listes(liste: list, accumulateur = [[]]):
if len(liste) == 0:
return accumulateur
element = liste.pop()
tmp = []
for sous_liste in accumulateur:
tmp.append([element] + sous_liste) # ligne 7
return sous_listes(liste, accumulateur + tmp) # ligne 8
3. Pourquoi lcs_force_brute est lente
Une liste de m éléments possède 2ᵐ sous-listes (chaque élément est pris ou non). La fonction les construit toutes, puis appelle est_extraite pour chacune, avec un coût proportionnel à la longueur n de liste_2.
Le coût est donc de l’ordre de 2ᵐ × n : il est exponentiel. Chaque élément ajouté à liste_1 double le temps de calcul (et la mémoire occupée par les sous-listes).
Partie B – Approche récursive
4. Valeur de lcs([9, 3, 7, 5, 8], 1, [9, 7, 8, 3, 7, 3], 1)
On cherche une LCS de [9] et de [9] : l’appel renvoie [9].
5. Relation de récurrence (derniers éléments égaux)
lcs(L_1, m, L_2, n) = lcs(L_1, m-1, L_2, n-1) + [L_1[m-1]]
Le dernier élément commun termine une plus longue sous-liste commune : on le place à la fin d’une LCS des listes privées de leur dernier élément.
6. aux est récursive
La fonction aux s’appelle elle-même (lignes 6, 7 et 8), avec des indices plus petits. Le cas de base (ligne 3 : un indice négatif, c’est-à-dire une liste vide) arrête les appels.
7. Risque de ce choix pour de très grandes listes
Temps de calcul exponentiel : lorsque les derniers éléments diffèrent, aux fait deux appels, qui recalculent à de nombreuses reprises les mêmes sous-problèmes aux(i, j). Le nombre d’appels peut aller jusqu’à environ 2ᵐ⁺ⁿ.
Dépassement de la pile d’appels : la profondeur de récursion peut atteindre m + n. Python limite cette profondeur (1000 par défaut) : au-delà, le programme s’arrête avec une erreur RecursionError.
8. Solution : la programmation dynamique
On évite les calculs répétés en mémorisant les résultats des sous-problèmes. Le mieux ici est une version ascendante (bottom-up), itérative, qui remplit un tableau t où t[i][j] est une LCS des i premiers éléments de liste_x et des j premiers de liste_y : plus d’appels récursifs, donc plus de risque de dépassement de pile, et un coût en O(m × n) cases.
def lcs_bottom_up(liste_x, liste_y):
m, n = len(liste_x), len(liste_y)
t = [[[] for j in range(n + 1)] for i in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if liste_x[i - 1] == liste_y[j - 1]:
t[i][j] = t[i - 1][j - 1] + [liste_x[i - 1]]
elif len(t[i][j - 1]) >= len(t[i - 1][j]):
t[i][j] = t[i][j - 1]
else:
t[i][j] = t[i - 1][j]
return t[m][n]
▶ Animation : le tableau de la programmation dynamique pour la LCS
La case de la ligne i et de la colonne j contient la longueur d’une LCS des i premiers éléments de liste_x et des j premiers de liste_y. Si les deux éléments sont égaux, on prend la case en diagonale + 1 (flèche ↖), sinon la plus grande des cases à gauche et au-dessus. On remonte ensuite les flèches pour lire la LCS.
Une erreur, un oubli ? Écrivez-moi : frederic.peuriere@lfcl.pt
🔎 Exercices sur des thèmes similaires
-
Asie 2026 – jour 1 – exercice 1
BDD, SQL, programmation Python, algorithmique — Difficulté : 🔥🔥🔥🔥🔥 -
Nouvelle Calédonie 2023 – jour 2 – exercice 2
programmation Python, récursivité — Difficulté : 🔥🔥🔥🔥🔥 -
Métropole 2025 – jour 1 – exercice 2
algorithmique, structures de données, processus — Difficulté : 🔥🔥🔥🔥🔥 -
Asie 2025 – jour 2 – exercice 1
POO, algorithmique — Difficulté : 🔥🔥🔥🔥🔥 -
Polynésie 2025 – jour 2 – exercice 1
POO, algorithmique , programmation — Difficulté : 🔥🔥🔥🔥🔥 -
Asie 2024 – jour 1 – exercice 1
programmation, POO, algorithmique — Difficulté : 🔥🔥🔥🔥🔥