Programmation dynamique - TP 2¶
Ce TP applique la méthode du chapitre sur la programmation dynamique : récurrence naïve, mémoïsation, tabulation puis reconstruction.
Subset-Sum¶
On s'intéresse ici au problème SUBSET-SUM : étant donné un ensemble d'entiers
\(v_0, \dots, v_{n-1} \in \mathbb{N}^*\) et un entier \(T \in \mathbb{N}\) cible,
existe-t-il \(\{ i_1, \dots, i_k\} \subset \range{0}{n-1}\) tel que \(T = v_{i_1} +
\dots + v_{i_k}\) ? Autrement dit, existe-t-il un sous-ensemble des valeurs de
somme \(T\) ?
On va considérer les valeurs dans un tableau values et pour représenter un
sous-ensemble, un tableau subset de même taille de booléens (True ou
False) qui représentera l'ensemble des values[i] pour lesquels subset[i]
vaut True.
Question 1
Écrire une fonction get_subset telle que
get_subset(values, subset) renvoie la liste des éléments du sous-ensemble.
Corrigé
def get_subset(values, subset):
s = []
for i in range(len(values)):
if subset[i]:
s.append(values[i])
return s
Tests :
assert(get_subset([1,2,3,4,5],
[True,False,True,True,False]) == [1,3,4])
assert(get_subset([1,2], [False,False]) == [])
assert(get_subset([1,2], [True,True]) == [1,2])
"OK"
Résolution par backtracking¶
On va résoudre récursivement ce problème en énumérant tous les sous-ensembles.
\(\mathop{Recherche}(i, S, T)\) :
- si \(T = 0\), on renvoie le sous-ensemble \(S\) ;
- si \(i \ge n\), on échoue ;
- si \(v_i \le T\), on lance \(\mathop{Recherche}(i+1, S \cup \{v_i\}, T-v_i)\) :
- si cela a réussi, on renvoie ce sous-ensemble ;
- on renvoie le résultat de \(\mathop{Recherche}(i+1, S, T)\).
Il suffit donc de lancer \(\mathop{Recherche}(0, \emptyset, T)\) pour essayer d'obtenir une décomposition.
Question 2
Écrire une fonction subsetsum_bt telle que
subsetsum_bt(values, i, subset, target)
renvoie un sous-ensemble de values permettant d'obtenir target
s'il en existe un et renvoie None sinon.
Corrigé
def subsetsum_bt(values, i, subset, target):
if target == 0:
return get_subset(values, subset)
if i >= len(values):
return None
if values[i] <= target and not subset[i]:
subset[i] = True
v = subsetsum_bt(values, i+1, subset, target-values[i])
if v != None:
return v
subset[i] = False
return subsetsum_bt(values, i+1, subset, target)
On peut tester cette fonction ainsi avec \(0\) et \(\emptyset\) (un tableau de
False).
subsetsum_bt([1,3,5,7],0,[False,False,False,False],10)
subsetsum_bt([1,3,5,7],0,[False,False,False,False],15)
subsetsum_bt([1,3,5,7],0,[False,False,False,False],2)
On obtient respectivement [3, 7], [3, 5, 7] et None.
Tests :
assert(subsetsum_bt([1,3,5],0,[False,False,False],4) == [1,3])
assert(subsetsum_bt([1,3,5,7],0,[False,False,False,False],10) == [3,7])
"OK"
On remarque que cet algorithme va énumérer tous les sous-ensembles dans le pire cas, donc il est en \(O(2^n)\) pour \(n\) valeurs. On ne connaît pas d'algorithme de complexité polynomiale pour ce problème : il s'agit d'un problème dit NP-complet, et en trouver un permettrait de résoudre le fameux problème P = NP.
On va essayer d'utiliser la programmation dynamique ici.
Problème de décision¶
On va se restreindre à une version plus simple ici où se pose la question de savoir si c'est possible ou non d'atteindre \(T\) mais sans chercher explicitement le sous-ensemble qui le réalise.
Question 3
Écrire une fonction subsetsum_rec_aux telle que
subsetsum_rec_aux(values, target, i) renvoie un booléen indiquant si le
nombre target peut être atteint par un sous-ensemble de valeurs dans
values d'indice \(\ge i\).
Corrigé
def subsetsum_rec_aux(values, target, i):
if target == 0:
return True
if i >= len(values):
return False
b = subsetsum_rec_aux(values, target, i+1)
if values[i] <= target:
b = b or subsetsum_rec_aux(values, target-values[i], i+1)
return b
Tests :
assert(subsetsum_rec_aux([1,3,4], 0, 0))
assert(subsetsum_rec_aux([1,3,4], 4, 0))
assert(subsetsum_rec_aux([1,3,4], 1, 0))
assert(subsetsum_rec_aux([1,3,4], 8, 0))
assert(not subsetsum_rec_aux([1,3,4], 2, 0))
assert(not subsetsum_rec_aux([1,3,4], 9, 0))
"OK"
Question 4
Écrire une fonction subsetsum_rec telle que subsetsum_rec(values, target)
renvoie le booléen indiquant si target peut être écrit comme la somme d'un
sous-ensemble des valeurs dans values en utilisant subsetsum_rec_aux.
Corrigé
def subsetsum_rec(values, target):
return subsetsum_rec_aux(values, target, 0)
Tests :
assert(subsetsum_rec([1,3,4], 0))
assert(subsetsum_rec([1,3,4], 4))
assert(subsetsum_rec([1,3,4], 1))
assert(subsetsum_rec([1,3,4], 8))
assert(not subsetsum_rec([1,3,4], 2))
assert(not subsetsum_rec([1,3,4], 9))
"OK"
Question 5
Écrire une fonction summable qui renvoie la liste croissante des nombres
qu'on peut atteindre pour un ensemble de valeurs donné.
Corrigé
def summable(values):
s = sum(values)
return [ k for k in range(s+1) if subsetsum_rec(values, k) ]
Tests :
assert(summable([1,3,4]) == [0,1,3,4,5,7,8])
assert(summable([1,2,6,18]) == [0, 1, 2, 3, 6, 7, 8,
9, 18, 19, 20, 21, 24, 25, 26, 27])
"OK"
Mémoïsation¶
Question 6
Écrire une fonction subsetsum_memo_aux telle que
subsetsum_memo_aux(values, target, i, cache) renvoie un booléen indiquant
si le nombre target peut être atteint par un sous-ensemble de valeurs dans
values d'indice \(\ge i\) en utilisant le dictionnaire cache pour
mémoriser les valeurs des couples (target, i).
Corrigé
def subsetsum_memo_aux(values, target, i, cache):
if (target, i) in cache:
return cache[(target, i)]
if target == 0:
b = True
elif i >= len(values):
b = False
else:
b = subsetsum_memo_aux(values, target, i+1, cache)
if values[i] <= target:
b = b or subsetsum_memo_aux(values, target-values[i], i+1, cache)
cache[(target, i)] = b
return b
Tests :
assert(subsetsum_memo_aux([1,3,4], 0, 0, {}))
assert(subsetsum_memo_aux([1,3,4], 4, 0, {}))
assert(subsetsum_memo_aux([1,3,4], 1, 0, {}))
assert(subsetsum_memo_aux([1,3,4], 8, 0, {}))
assert(not subsetsum_memo_aux([1,3,4], 2, 0, {}))
assert(not subsetsum_memo_aux([1,3,4], 9, 0, {}))
assert(not subsetsum_memo_aux([ 2*i for i in range(100) ], 265, 0, {}))
"OK"
Question 7
Écrire une fonction subsetsum_memo telle que subsetsum_memo(values, target)
renvoie le booléen indiquant si target peut être écrit comme la somme d'un
sous-ensemble des valeurs dans values en utilisant subsetsum_memo_aux.
Corrigé
def subsetsum_memo(values, target):
return subsetsum_memo_aux(values, target, 0, {})
Tests :
assert(subsetsum_memo([1,3,4], 0))
assert(subsetsum_memo([1,3,4], 4))
assert(subsetsum_memo([1,3,4], 1))
assert(subsetsum_memo([1,3,4], 8))
assert(not subsetsum_memo([1,3,4], 2))
assert(not subsetsum_memo([1,3,4], 9))
assert(not subsetsum_memo([ 2*i for i in range(100) ], 265))
"OK"
Tabulation¶
Pour résoudre ce problème par tabulation, on va devoir utiliser des matrices.
Question 8
Écrire une fonction matrix telle que matrix(n, m, v) renvoie
une matrice, i.e. un tableau de tableaux, de n tableaux de m valeurs v.
Par exemple, matrix(3, 2, 0) doit renvoyer [[0,0],[0,0],[0,0]].
Corrigé
def matrix(n,m,v):
mat = []
for i in range(n):
mat.append([v] * m)
return mat
Tests :
assert(matrix(2,3,0) == [[0,0,0],[0,0,0]])
assert(matrix(3,2,0) == [[0,0],[0,0],[0,0]])
assert(matrix(1,1,'a') == [['a']])
m = matrix(2,2,0)
m[0][0] = 1
assert(m == [[1,0],[0,0]])
"OK"
Question 9
Écrire une fonction subsetsum_tab telle que subsetsum_tab(values, target)
renvoie le booléen indiquant si target peut être écrit comme la somme d'un
sous-ensemble des valeurs dans values en utilisant une matrice de booléens
pour tabuler.
Corrigé
Ici b[i][t] indique si t peut être atteint avec les valeurs d'indice
\(\ge i\).
def subsetsum_tab(values, target):
n = len(values)
b = matrix(n+1, target+1, False)
b[n][0] = True
for i in reversed(range(n)):
b[i][0] = True
for t in range(1, target+1):
b[i][t] = b[i+1][t]
if t >= values[i]:
b[i][t] = b[i][t] or b[i+1][t-values[i]]
return b[0][target]
Tests :
assert(subsetsum_tab([1,3,4], 0))
assert(subsetsum_tab([1,3,4], 4))
assert(subsetsum_tab([1,3,4], 1))
assert(subsetsum_tab([1,3,4], 8))
assert(not subsetsum_tab([1,3,4], 2))
assert(not subsetsum_tab([1,3,4], 9))
assert(not subsetsum_tab([ 2*i for i in range(100) ], 265))
"OK"
Si on a \(n\) valeurs et une cible \(T\), l'algorithme tabulé utilise \(O(nT)\)
espace mémoire pour la table et la même complexité en temps pour la remplir. On
a donc un algorithme qui est polynomial en \(n\) pour résoudre SUBSET-SUM,
P = NP et je vais aller empocher le million de dollars de l'institut Clay.
Question 10
So long 👋
ou pas ?
Corrigé
Ce n'est pas une complexité polynomiale en la taille de l'entrée : l'entier \(T\) est représenté sur environ \(\log_2 T\) bits, et \(T\) est donc exponentiel en sa taille. Avec des valeurs et une cible de l'ordre de \(2^{n}\), la table a \(n 2^n\) cases. On dit que l'algorithme est pseudo-polynomial : il n'est efficace que lorsque \(T\) reste petit.
Questions annexes¶
Question 11
À l'aide de ces fonctions, déterminer le plus petit nombre qui ne soit pas la somme d'un sous-ensemble de l'ensemble de valeurs \(\{1, 2, 3, 5, 7, \dots, 97\}\) constitué des nombres premiers plus petits que 100 et de 1.
Note : comme l'ensemble de valeurs ne change pas, il est sûrement plus efficace de conserver un unique cache ou de reprendre la tabulation pour conserver la table.
Question 12
Reprendre la fonction subsetsum_tab pour qu'elle renvoie un sous-ensemble
dans le cas où la cible peut être écrite comme une somme, et None sinon.
Note : on pourra utiliser une matrice indiquant à l'indice \((i,t)\) si on utilise la \(i\)-ème valeur dans la décomposition de \(t\).
Corrigé
On reconstruit en partant de \((0, T)\) : si dec[i][v] est vrai, on prend
values[i] et on continue avec v - values[i]. On maintient ainsi
l'invariant « b[i][v] est vrai », ce qui garantit qu'on termine avec
v nul.
def subsetsum_tab(values, target):
n = len(values)
b = matrix(n+1, target+1, False)
dec = matrix(n+1, target+1, False)
b[n][0] = True
for i in reversed(range(n)):
b[i][0] = True
for t in range(1, target+1):
b[i][t] = b[i+1][t]
if t >= values[i]:
b[i][t] = b[i][t] or b[i+1][t-values[i]]
if b[i+1][t-values[i]]:
dec[i][t] = True
if not b[0][target]:
return None
v = target
subset = []
for i in range(n):
if dec[i][v]:
subset.append(values[i])
v = v-values[i]
return subset
Question 13
Si la liste de valeurs est en ordre croissant, que peut-on dire du cardinal du sous-ensemble renvoyé ?
Distance d'édition¶
Étant donné deux chaînes de caractères \(A\) et \(B\) de longueurs respectives \(n\) et \(m\), on veut transformer la chaîne \(A\) en la chaîne \(B\) en effectuant un minimum d'opérations parmi celles-ci :
- supprimer un caractère n'importe où ;
- insérer un nouveau caractère n'importe où ;
- changer un caractère en n'importe quel autre.
Ce nombre d'opérations est appelé la distance d'édition de \(A\) à \(B\).
Par exemple : chien \(\rightarrow\) chen \(\rightarrow\) chan \(\rightarrow\) chat
Question 14
Appliquer la méthodologie de la programmation dynamique pour calculer cette distance efficacement.
Question 15
En déduire une fonction renvoyant une suite de chaînes de longueur minimale pour passer d'une chaîne à une autre.
Exemple : pour chien et chat on obtiendra
['chien', 'chen', 'chan', 'chat'].