Problème de la somme de sous-ensemble
Dans la leçon sur l'addition et la soustraction, un enseignant d'une école primaire utilise un ensemble de valeurs, puis donne aux élèves une valeur somme et leur demande s'ils peuvent former cette valeur à partir de l'ensemble donné.
Ce problème est un classique de la programmation dynamique et appartient à la classe des problèmes NP-Complet.
Énoncé du problème
Entrée
- La première ligne contient deux valeurs : somme et n.
- La deuxième ligne contient n valeurs qui forment l'ensemble.
Sortie
Ouis'il existe un tel sous-ensemble, sinonNon.
15 5 7 2 8 6 9
Oui
0 3 2 5 6
Oui
15 4 1 2 7 3
Non
- 1 ≤ n ≤ 10³
- 0 ≤ somme ≤ 10⁴
- Les valeurs de l'ensemble sont des entiers positifs
Solution naïve — Récursive
L'idée est d'explorer tous les sous-ensembles possibles de manière récursive. Pour chaque élément, on a exactement deux choix :
- Inclure le dernier élément → somme requise devient
somme - vals[n-1], nombre d'éléments devientn - 1 - Ignorer le dernier élément → somme requise reste
somme, nombre d'éléments devientn - 1
solution(n, somme) :solution(5, 15)
├── solution(4, 15) [ignorer 9]
│ ├── solution(3, 15) [ignorer 6]
│ └── solution(3, 9) [inclure 6] → 6+9=15 ✓
└── solution(4, 6) [inclure 9]
└── ...si somme == 0 → retourner true // sous-ensemble vide
si n == 0 → retourner false // ensemble vide, somme non atteinte
si vals[n-1] > somme → ignorer cet élément (impossible de l'inclure)#include <iostream>
#include <vector>
using namespace std;
bool solution(vector<int> vals, int n, int somme) {
// Cas de base
if (somme == 0) return true;
if (n == 0) return false;
// Si le dernier élément est supérieur à somme, ignorez-le
if (vals[n - 1] > somme)
return solution(vals, n - 1, somme);
// Deux choix : inclure ou ignorer le dernier élément
return solution(vals, n - 1, somme) // ignorer
|| solution(vals, n - 1, somme - vals[n - 1]); // inclure
}
int main() {
int somme, n, val;
cin >> somme >> n;
vector<int> valeurs;
for (int i = 0; i < n; i++) {
cin >> val;
valeurs.push_back(val);
}
solution(valeurs, n, somme) ? cout << "Oui" : cout << "Non";
return 0;
}import sys
def solution(vals, n, somme):
# Cas de base
if somme == 0: return True
if n == 0: return False
# Si le dernier élément est supérieur à somme, ignorez-le
if vals[n - 1] > somme:
return solution(vals, n - 1, somme)
# Deux choix : inclure ou ignorer le dernier élément
return (solution(vals, n - 1, somme) # ignorer
or solution(vals, n - 1, somme - vals[n - 1])) # inclure
somme, n = map(int, sys.stdin.readline().strip().split())
valeurs = list(map(int, sys.stdin.readline().strip().split()))
print("Oui" if solution(valeurs, n, somme) else "Non")Oui
Complexité temporelle : O(2ⁿ) — exponentielle.
Complexité spatiale : O(n) — profondeur de la pile d'appels.
Le problème est NP-Complet : il n'existe pas de solution polynomiale connue. Cependant, pour des valeurs de somme raisonnables, la programmation dynamique offre une solution pseudo-polynomiale efficace.
Solution — Programmation Dynamique (Bottom-Up)
Pour résoudre le problème en temps pseudo-polynomial, on utilise la programmation dynamique avec un tableau 2D.
DP[n+1][somme+1] où :DP[i][j] = true s'il existe un sous-ensemble de vals[0..i-1] dont la somme vaut exactement j.DP[i][j]:- Si
j < vals[i-1]: impossible d'inclure l'élément i →DP[i][j] = DP[i-1][j] - Si
j ≥ vals[i-1]: on peut inclure ou ignorer →DP[i][j] = DP[i-1][j] OR DP[i-1][j - vals[i-1]]
DP[i][0] = truepour tout i (somme 0 → sous-ensemble vide)DP[0][j] = falsepour j ≥ 1 (ensemble vide, somme non nulle impossible)
Illustration — vals = {7, 2, 8, 6, 9}, somme = 15
| i \ j | 0 | 1 | 2 | 6 | 7 | 8 | 9 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|
| 0 ({}) | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| 1 ({7}) | ✓ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ |
| 2 ({7,2}) | ✓ | ✗ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ |
| 5 ({7,2,8,6,9}) | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
DP[5][15] = true → Oui
#include <iostream>
#include <vector>
using namespace std;
bool solution(vector<int> &vals, int n, int somme) {
// Tableau DP de taille (n+1) x (somme+1)
bool DP[n + 1][somme + 1];
// Initialisation : somme = 0 → toujours vrai (sous-ensemble vide)
for (int i = 0; i <= n; i++)
DP[i][0] = true;
// Initialisation : ensemble vide, somme > 0 → toujours faux
for (int j = 1; j <= somme; j++)
DP[0][j] = false;
// Remplissage Bottom-Up
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= somme; j++) {
if (j < vals[i - 1])
// Impossible d'inclure vals[i-1], copier l'état précédent
DP[i][j] = DP[i - 1][j];
else
// Inclure OU ignorer vals[i-1]
DP[i][j] = DP[i - 1][j] || DP[i - 1][j - vals[i - 1]];
}
}
return DP[n][somme];
}
int main() {
int somme, n, val;
cin >> somme >> n;
vector<int> valeurs;
for (int i = 0; i < n; i++) {
cin >> val;
valeurs.push_back(val);
}
solution(valeurs, n, somme) ? cout << "Oui" : cout << "Non";
return 0;
}import sys
def solution(vals, n, somme):
# Tableau DP de taille (n+1) x (somme+1), initialisé à False
DP = [[False] * (somme + 1) for _ in range(n + 1)]
# somme = 0 → toujours vrai (sous-ensemble vide)
for i in range(n + 1):
DP[i][0] = True
# ensemble vide, somme > 0 → toujours faux (déjà False par défaut)
for j in range(1, somme + 1):
DP[0][j] = False
# Remplissage Bottom-Up
for i in range(1, n + 1):
for j in range(1, somme + 1):
if j < vals[i - 1]:
DP[i][j] = DP[i - 1][j] # ignorer obligatoirement
else:
DP[i][j] = (DP[i - 1][j] # ignorer
or DP[i - 1][j - vals[i - 1]]) # inclure
return DP[n][somme]
somme, n = map(int, sys.stdin.readline().strip().split())
valeurs = list(map(int, sys.stdin.readline().strip().split()))
print("Oui" if solution(valeurs, n, somme) else "Non")Oui
| Critère | Solution récursive naïve | Programmation dynamique |
|---|---|---|
| Temps | O(2ⁿ) — exponentiel | O(n × somme) — pseudo-polynomial |
| Espace | O(n) — pile d'appels | O(n × somme) — tableau 2D |
| Sous-problèmes recalculés | Oui — nombreux recalculs | Non — mémoïsation implicite |
vector<bool> dp(somme + 1, false);
dp[0] = true;
for (int val : vals)
for (int j = somme; j >= val; j--) // parcours à rebours !
dp[j] = dp[j] || dp[j - val];
return dp[somme];Cette variante est dite optimisation de l'espace du Subset Sum DP.- Le problème de la somme de sous-ensemble consiste à déterminer s'il existe un sous-ensemble d'éléments dont la somme égale une valeur cible.
- La solution récursive naïve a une complexité exponentielle O(2ⁿ).
- La programmation dynamique permet de résoudre le problème en temps pseudo-polynomial O(somme × n).
- L'approche dynamique utilise un tableau 2D booléen où DP[i][j] indique s'il est possible d'obtenir la somme j avec les i premiers éléments.
- Ce problème est NP-Complet dans le cas général, mais la solution dynamique est efficace pour des valeurs de somme modérées.
- Une optimisation mémoire permet de réduire l'espace à O(somme) avec un tableau 1D parcouru à rebours.
Discussion (0)
Soyez le premier à laisser un commentaire !
Laisser un commentaire
Votre commentaire sera visible après modération.