Le but de cette séance est de vous aider à maîtriser des notions importantes en informatique.
Un assistant de programmation intelligent tel que GitHub Copilot, que vous avez peut-être déjà installé, pourra vous fournir une solution à ces exercices uniquement à partir d’un nom de fichier judicieusement choisi.
Pour des raisons de formation, nous vous conseillons de désactiver d’abord ces outils.
À la fin de l’activité pratique, nous vous suggérons de retravailler l’exercice avec ces outils activés.
Suivre ces deux étapes améliorera vos compétences à la fois de manière fondamentale et pratique.
De plus, nous vous fournissons les solutions aux exercices.
Assurez-vous de les consulter uniquement après avoir trouvé une solution aux exercices, à des fins de comparaison !
Même si vous êtes sûr que votre solution est correcte, veuillez les regarder, car elles fournissent parfois des éléments supplémentaires que vous auriez pu manquer.
Contenu de l’activité
1 — Le problème du sac à dos 0-1
Le problème est le suivant (citée de Comer et al.).
Un voleur qui cambriolent un magasin veut prendre la charge la plus précieuse qui peut
être portée dans un sac à dos capable de porter au maximum
$W$ livres de
butin. Le voleur peut choisir de prendre n’importe quel sous-ensemble de $n$ objets dans le
magasin. Le $i$-ième objet vaut $v_i$ dollars et pèse $w_i$ livres, où
$v_i$ et $w_i$ sont des entiers. Quels objets le voleur doit-il prendre ?
Remarque : Le problème est appelé problème du sac à dos 0-1 car le voleur peut soit prendre un objet, soit le laisser, mais ne peut pas prendre une fraction d’un objet.
Pour cet exercice, nous allons créer un fichier knapsack.py et implémenter diverses solutions au problème du sac à dos 0-1.
En parallèle, nous créerons un fichier test_knapsack.py pour tester les fonctions implémentées dans knapsack.py.
Nous vérifierons non seulement la justesse des fonctions mais aussi leur efficacité.
Comme paramètre de base, nous considérerons les valeurs suivantes :
Le poids maximum que le sac à dos peut porter : $W = 50$.
La liste des poids des objets : $weights = [10, 20, 30]$.
La liste des valeurs des objets : $values = [60, 100, 120]$.
Deux autres ensembles de paramètres seront considérés pour les tests :
Le premier ensemble de paramètres est suffisamment petit pour vous permettre de calculer les résultats à la main.
Les deux autres ensembles sont plus grands et nécessiteront l’implémentation d’algorithmes efficaces et de ressentir la complexité de chaque approche.
1.1 — Solution à la main
Oubliez le programme, prenez un stylo et du papier et considérez le premier exemple de test.
Listez toutes les combinaisons possibles d’objets qui peuvent être prises, et pour chacune d’elles, calculez le poids total et la valeur totale.
Puis, déterminez la combinaison qui maximise la valeur totale sans dépasser le poids maximum que le sac à dos peut porter.
Cette solution vous aidera à comprendre le problème et les résultats attendus.
1.2 — Une solution récursive
La première fonction à implémenter est une solution récursive au problème du sac à dos 0-1.
La fonction doit avoir la signature suivante :
defknapsack_recursive(W: int, weights: List[int], values: List[int]) -> int:
""" Recursive solution to the 0-1 knapsack problem.
In:
- W: the maximum weight the knapsack can carry
- weights: the list of weights of the items
- values: the list of values of the items
Out:
- the maximum value that can be carried in the knapsack
"""pass
La solution récursive est simplement basée sur la propriété de sous-structure optimale du problème :
si la charge la plus précieuse inclut l’objet $i$, alors la charge restante doit être la charge la plus précieuse pesant au plus $W - w_i$ que le voleur peut prendre parmi les objets originaux, à l’exclusion de l’objet $i$.
Conseils
Utilisez une fonction auxiliaire récursive qui inclut un paramètre supplémentaire $n$ indiquant l’objet courant à vérifier.
Le cas de base de la récursivité est qu’il n’y a plus d’objets à étudier ou plus d’espace dans le sac à dos.
Les appels récursifs correspondent au cas où l’objet courant est trop lourd pour être ajouté au sac à dos et aux cas où l’objet est pris ou non. La meilleure solution de ces deux dernières possibilités doit être retournée.
Correction
defknapsack_recursive(W: int, weights: list[int], values: list[int]) -> int:
""" Recursive solution to the 0-1 knapsack problem.
In:
- W: the maximum weight the knapsack can carry
- weights: the list of weights of the items
- values: the list of values of the items
Out:
- the maximum value that can be carried in the knapsack
"""defknapsack_recursive_helper(W: int, weights: list[int], values: list[int], n: int) -> int:
""" Helper function to solve the 0-1 knapsack problem recursively.
In:
- W: the maximum weight the knapsack can carry
- weights: the list of weights of the items
- values: the list of values of the items
- n: the current item to pick or not (corresponds to weights[n-1] and values[n-1])
Out:
- the maximum value that can be carried in the knapsack
"""# base case no items to check or no more space leftif n ==0or W ==0:
return0# if the current item is too heavy, skip itif weights[n -1] > W:
return knapsack_recursive_helper(W, weights, values, n -1)
else:
# return the maximum of two cases:# 1. the current item is included# 2. the current item is not includedreturn max(values[n -1] + knapsack_recursive_helper(W - weights[n -1], weights, values, n -1),
knapsack_recursive_helper(W, weights, values, n -1))
return knapsack_recursive_helper(W, weights, values, len(weights))
#test case 1W =15weights = [12, 7, 11, 8, 9]
values = [24, 13, 23, 15, 16]
print(knapsack_recursive(W, weights, values)) # 28#test case 2W =30weights = [2, 3, 5, 7, 1, 4, 6, 8, 10, 9, 2, 3, 5, 7, 1]
values = [10, 15, 7, 8, 4, 12, 6, 11, 20, 18, 5, 9, 14, 16, 3]
print(knapsack_recursive(W, weights, values)) # 90
publicclassKnapsack {
publicstaticintknapsackRecursive(int W, int[] weights, int[] values) {
return knapsackRecursiveHelper(W, weights, values, weights.length);
}
privatestaticintknapsackRecursiveHelper(int W, int[] weights, int[] values, int n) {
// base case no items to check or no more space leftif (n == 0 || W == 0) {
return 0;
}
// if the current item is too heavy, skip itif (weights[n - 1]> W) {
return knapsackRecursiveHelper(W, weights, values, n - 1);
} else {
// return the maximum of two cases:// 1. the current item is included// 2. the current item is not includedreturn Math.max(values[n - 1]+ knapsackRecursiveHelper(W - weights[n - 1], weights, values, n - 1),
knapsackRecursiveHelper(W, weights, values, n - 1));
}
}
publicstaticvoidmain(String[] args) {
// test case 1int W1 = 15;
int[] weights1 = {12, 7, 11, 8, 9};
int[] values1 = {24, 13, 23, 15, 16};
System.out.println(knapsackRecursive(W1, weights1, values1)); // 28// test case 2int W2 = 30;
int[] weights2 = {2, 3, 5, 7, 1, 4, 6, 8, 10, 9, 2, 3, 5, 7, 1};
int[] values2 = {10, 15, 7, 8, 4, 12, 6, 11, 20, 18, 5, 9, 14, 16, 3};
System.out.println(knapsackRecursive(W2, weights2, values2)); // 90 }
}
1.3 — Une solution récursive avec mémoïsation
Une deuxième étape est d’implémenter une solution récursive avec mémoïsation.
La fonction doit avoir la signature suivante :
defknapsack_recursive_memo(W: int, weights: List[int], values: List[int]) -> int:
""" Recursive solution with memoization to the 0-1 knapsack problem.
In:
- W: the maximum weight the knapsack can carry
- weights: the list of weights of the items
- values: the list of values of the items
Out:
- the maximum value that can be carried in the knapsack
"""pass
La solution récursive avec mémoïsation consiste à stocker les résultats des sous-problèmes dans une table pour éviter de les recalculer.
Correction
defknapsack_recursive_memo(W: int, weights: list[int], values: list[int]) -> int:
""" Recursive solution with memoization to the 0-1 knapsack problem.
In:
- W: the maximum weight the knapsack can carry
- weights: the list of weights of the items
- values: the list of values of the items
Out:
- the maximum value that can be carried in the knapsack
"""# Create a table to store the results of the subproblems table = [[-1for _ in range(W +1)] for _ in range(len(values) +1)]
defknapsack_recursive_with_memo(W: int, weights: list[int], values: list[int], n: int) -> int:
""" Helper function to solve the 0-1 knapsack problem recursively with memoization.
In:
- W: the maximum weight the knapsack can carry
- weights: the list of weights of the items
- values: the list of values of the items
- n: the number of items
Out:
- the maximum value that can be carried in the knapsack
"""# If the result is not already computedif table[n][W] ==-1:
# Base case: no item to take or no space left in the knapsackif n ==0or W ==0:
table[n][W] =0# If the weight of the nth item is more than the knapsack capacity, then this item cannot be included in the optimal solutionelif weights[n -1] > W:
table[n][W] = knapsack_recursive_with_memo(W, weights, values, n -1)
else:
# Return the maximum of two cases: table[n][W] = max(values[n -1] + knapsack_recursive_with_memo(W - weights[n -1], weights, values, n -1),
knapsack_recursive_with_memo(W, weights, values, n -1))
# Return the resultreturn table[n][W]
return knapsack_recursive_with_memo(W, weights, values, len(weights))
# Test the functionassert knapsack_recursive_memo(50, [10, 20, 30], [60, 100, 120]) ==220
publicclassKnapsack {
publicstaticintknapsackRecursiveMemo(int W, int[] weights, int[] values) {
int n = values.length;
int[][] table =newint[n + 1][W + 1];
for (int i = 0; i <= n; i++) {
for (int w = 0; w <= W; w++) {
table[i][w]=-1;
}
}
return knapsackRecursiveWithMemo(W, weights, values, n, table);
}
privatestaticintknapsackRecursiveWithMemo(int W, int[] weights, int[] values, int n, int[][] table) {
if (table[n][W]==-1) {
if (n == 0 || W == 0) {
table[n][W]= 0;
} elseif (weights[n - 1]> W) {
table[n][W]= knapsackRecursiveWithMemo(W, weights, values, n - 1, table);
} else {
table[n][W]= Math.max(values[n - 1]+ knapsackRecursiveWithMemo(W - weights[n - 1], weights, values, n - 1, table),
knapsackRecursiveWithMemo(W, weights, values, n - 1, table));
}
}
return table[n][W];
}
publicstaticvoidmain(String[] args) {
int W = 50;
int[] weights = {10, 20, 30};
int[] values = {60, 100, 120};
assert knapsackRecursiveMemo(W, weights, values) == 220;
// Test case 1 W = 15;
weights =newint[]{12, 7, 11, 8, 9};
values =newint[]{24, 13, 23, 15, 16};
long start = System.nanoTime();
System.out.println(knapsackRecursiveMemo(W, weights, values)); // 28 System.out.printf("Time taken with memo: %.6f seconds%n", (System.nanoTime() - start) / 1e9);
// Test case 2 W = 30;
weights =newint[]{2, 3, 5, 7, 1, 4, 6, 8, 10, 9, 2, 3, 5, 7, 1};
values =newint[]{10, 15, 7, 8, 4, 12, 6, 11, 20, 18, 5, 9, 14, 16, 3};
start = System.nanoTime();
System.out.println(knapsackRecursiveMemo(W, weights, values)); // 28 System.out.printf("Time taken with memo: %.6f seconds%n", (System.nanoTime() - start) / 1e9);
}
}
1.4 — Une solution par programmation dynamique
Une troisième étape est d’implémenter une solution par programmation dynamique au problème du sac à dos 0-1.
La fonction doit avoir la signature suivante :
defknapsack_dp(W: int, weights: List[int], values: List[int], n: int) -> int:
""" Dynamic programming solution to the 0-1 knapsack problem.
In:
- W: the maximum weight the knapsack can carry
- weights: the list of weights of the items
- values: the list of values of the items
- n: the number of items
Out:
- the maximum value that can be carried in the knapsack
"""pass
Contrairement à la solution avec mémoïsation, la solution par programmation dynamique remplit la table de bas en haut.
Conseils
Pour implémenter la solution par programmation dynamique, suivez ces étapes :
Initialiser la table : Créez une matrice de dimensions (n+1) x (W+1) où n est le nombre d’objets et W le poids maximum que le sac à dos peut porter. Initialisez toutes les valeurs à 0.
Remplir la table : Parcourez chaque objet et chaque capacité de poids, en remplissant la table selon les conditions suivantes :
Si le poids de l’objet courant est supérieur à la capacité actuelle, la valeur est la même que celle sans inclure l’objet courant.
Sinon, la valeur est le maximum entre inclure l’objet courant ou ne pas l’inclure.
Retourner le résultat : La valeur à table[n][W] sera la valeur maximale pouvant être portée dans le sac à dos.
Correction
defknapsack_dp(W: int, weights: list[int], values: list[int], n: int) -> int:
""" Dynamic programming solution to the 0-1 knapsack problem.
In:
- W: the maximum weight the knapsack can carry
- weights: the list of weights of the items
- values: the list of values of the items
- n: the number of items
Out:
- the maximum value that can be carried in the knapsack
"""# Create a matrix to store the results of the subproblems table = [[0for _ in range(W +1)] for _ in range(n +1)]
# for each item in the list# the first row is for the case of no item, so we start at 1for i in range(1, n+1):
# for each weight in the knapsackfor j in range(1, W+1):
# if the weight of the item is greater than the current weight# note that i starts at 1, so the weight of the item is weights[i-1] if weights[i -1] > j:
# the value is the same as the value without including the current item table[i][j] = table[i -1][j]
else:
# the value is the maximum of either including the current item or not including it table[i][j] = max(table[i -1][j], values[i -1] + table[i -1][j - weights[i -1]])
return table[n][W]
# Test the functionassert knapsack_dp(50, [10, 20, 30], [60, 100, 120], 5) ==220
import java.util.Arrays;
publicclassKnapsackDP {
publicstaticintknapsackDP(int W, int[] weights, int[] values, int n) {
// Create a table to store the results of the subproblemsint[][] table =newint[n+1][W + 1];
// for each item in the list// the first row is for the case of no item, so we start at 1for (int i = 0; i <= values.length; i++) {
// for each weight in the knapsack from W to the (weight in cell i) - 1for (int j = 0; j <= weights.length; w++) {
// if the weight of the item is greater than the current weight// note that i starts at 1, so the weight of the item is weights[i-1]if(weights[i - 1]> j){
// the value is the same as the value without including the current item table[i][j]= table[i - 1][j] }else{
// the value is the maximum of either including the current item or not including it table[i][j]= max(table[i - 1][j], values[i - 1]+ table[i - 1][j - weights[i - 1]])
}
}
}
return table[n][W];
}
publicstaticvoidmain(String[] args) {
// Test the functionassert knapsackDP(50, newint[]{10, 20, 30}, newint[]{60, 100, 120}, 5) == 220;
// test case 1int W = 15;
int[] weights = {12, 7, 11, 8, 9};
int[] values = {24, 13, 23, 15, 16};
long start = System.nanoTime();
System.out.println(knapsackDP(W, weights, values, 5)); // 28long elapsed = System.nanoTime() - start;
System.out.printf("Time taken without memo: %.6f seconds%n", elapsed / 1e9);
W = 30;
weights =newint[]{2, 3, 5, 7, 1, 4, 6, 8, 10, 9, 2, 3, 5, 7, 1};
values =newint[]{10, 15, 7, 8, 4, 12, 6, 11, 20, 18, 5, 9, 14, 16, 3};
start = System.nanoTime();
System.out.println(knapsackDP(W, weights, values, 15)); // 28 elapsed = System.nanoTime() - start;
System.out.printf("Time taken with memo: %.6f seconds%n", elapsed / 1e9);
}
}
1.5 — Une solution gloutonne
La dernière étape est d’implémenter une solution gloutonne au problème du sac à dos 0-1.
La fonction doit avoir la signature suivante :
defknapsack_greedy(W: int, weights: List[int], values: List[int]) -> int:
""" Greedy solution to the 0-1 knapsack problem.
In:
- W: the maximum weight the knapsack can carry
- weights: the list of weights of the items
- values: the list of values of the items
Out:
- the maximum value that can be carried in the knapsack
"""pass
La solution gloutonne consiste à sélectionner l’objet avec le ratio valeur/poids maximal jusqu’à ce que le sac à dos soit plein.
Correction
defknapsack_greedy(W: int, weights: List[int], values: List[int], n: int) -> int:
""" Greedy solution to the 0-1 knapsack problem.
In:
- W: the maximum weight the knapsack can carry
- weights: the list of weights of the items
- values: the list of values of the items
- n: the number of items
Out:
- the maximum value that can be carried in the knapsack
"""# Create a list of tuples (value, weight, value-to-weight ratio) items = [(values[i], weights[i], values[i] / weights[i]) for i in range(n)]
# Sort the items by value-to-weight ratio in descending order items.sort(key=lambda x: x[2], reverse=True)
# Initialize the total value and the remaining weight total_value =0 remaining_weight = W
# Iterate through the itemsfor item in items:
# If the item can be included in the knapsackif item[1] <= remaining_weight:
total_value += item[0]
remaining_weight -= item[1]
return total_value
# Test the functionassert knapsack_greedy(50, [10, 20, 30], [60, 100, 120], 3) ==220
import java.util.*;
publicclassKnapsackGreedy {
publicstaticintknapsackGreedy(int W, int[] weights, int[] values) {
// Create a list of items with value, weight, and value-to-weight ratio List<Item> items =new ArrayList<>();
for (int i = 0; i < values.length; i++) {
items.add(new Item(values[i], weights[i]));
}
// Sort the items by value-to-weight ratio in descending order items.sort((a, b) -> Double.compare(b.ratio, a.ratio));
// Initialize the total value and the remaining weightint totalValue = 0;
int remainingWeight = W;
// Iterate through the itemsfor (Item item : items) {
// If the item can be included in the knapsackif (item.weight<= remainingWeight) {
totalValue += item.value;
remainingWeight -= item.weight;
}
}
return totalValue;
}
staticclassItem {
int value;
int weight;
double ratio;
Item(int value, int weight) {
this.value= value;
this.weight= weight;
this.ratio= (double) value / weight;
}
}
publicstaticvoidmain(String[] args) {
// Test the functionint W = 50;
int[] weights = {10, 20, 30};
int[] values = {60, 100, 120};
System.err.println(knapsackGreedy(W, weights, values));
// assert knapsackGreedy(W, weights, values) == 220; }
}
Questions :
Que pouvez-vous dire de la solution gloutonne comparée aux autres solutions ?
Maintenant, si vous considérez le problème du sac à dos fractionnaire, quelle serait la solution gloutonne ?
2 — Optimisez vos solutions
Ce que vous pouvez faire maintenant est d’utiliser des outils d’IA tels que GitHub Copilot ou ChatGPT, soit pour générer la solution, soit pour améliorer la première solution que vous avez proposée !
Essayez de faire cela pour tous les exercices ci-dessus, pour voir les différences avec vos solutions.
Pour aller plus loin
3 — Le problème du sac à dos fractionnaire
Pour aller au-delà
Il semble que cette section soit vide !
Y a-t-il quelque chose que vous auriez aimé voir ici ?
Faites-le nous savoir sur le serveur Discord !
Peut-être que nous pourrons l’ajouter rapidement.
Sinon, cela nous aidera à améliorer le cours pour l’année prochaine !