Somme cible avec signes positifs et négatifs
Transformez le problème d’affectation target-sum en un sac à dos fondé sur la différence de sommes de sous-ensembles, et résolvez-le en O(n × sum).
Somme cible avec signes positifs et négatifs 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.
Le problème de la somme cible
Étant donné un tableau d’entiers nums et un entier target, attribuez un signe + ou - à chaque nombre afin que l’expression obtenue s’évalue à target. Renvoyez le nombre de façons distinctes de procéder. Par exemple, avec nums=[1,1,1,1,1] et target=3, il existe 5 façons de faire (choisir 4 éléments positifs et 1 élément négatif, à des positions différentes).
Force brute : énumération par DFS
Une approche DFS attribue à chaque nombre le signe + ou - et effectue des appels récursifs, en renvoyant le nombre de nœuds feuilles qui atteignent target. Elle est correcte, mais sa complexité temporelle est O(2^n) — elle est donc exponentielle. Pour n=20, cela représente plus d’un million d’appels récursifs. Il est utile de commencer par mentionner l’approche DFS, puis de passer rapidement à l’optimisation par DP.
def findTargetSumWays_dfs(nums, target):
count = [0]
def dfs(i, current_sum):
if i == len(nums):
if current_sum == target:
count[0] += 1
return
dfs(i+1, current_sum + nums[i])
dfs(i+1, current_sum - nums[i])
dfs(0, 0)
return count[0]
print(findTargetSumWays_dfs([1,1,1,1,1], 3)) # 5DFS avec mémoïsation
Ajoutez une mémoïsation à la DFS : l’état est (index, current_sum). Comme current_sum peut varier de -total à +total, il existe O(n × total) états distincts. Avec la mémoïsation, la DFS s’exécute en temps et en espace O(n × total). Cette approche fonctionne et reste valable en entretien, mais le DP fondé sur la transformation est plus élégant et plus économe en espace.
from functools import lru_cache
def findTargetSumWays_memo(nums, target):
total = sum(nums)
@lru_cache(maxsize=None)
def dp(i, remaining):
if i == len(nums):
return 1 if remaining == 0 else 0
return dp(i+1, remaining - nums[i]) + dp(i+1, remaining + nums[i])
return dp(0, target)
print(findTargetSumWays_memo([1,1,1,1,1], 3)) # 5Transformation mathématique
Soit P l’ensemble des nombres auxquels le signe + est attribué, et N l’ensemble de ceux auxquels le signe - est attribué. Alors : sum(P) - sum(N) = target et sum(P) + sum(N) = total. En additionnant, on obtient : 2 × sum(P) = target + total, donc sum(P) = (target + total) / 2. Le problème se réduit à : compter les sous-ensembles de nombres dont la somme vaut (target + total) / 2. Il s’agit exactement de la variante « compter les sous-ensembles » du sac à dos 0/1.
# sum(P) - sum(N) = target
# sum(P) + sum(N) = total
# => 2*sum(P) = target + total
# => sum(P) = (target + total) / 2
# Count subsets with sum = new_target = (target + total) // 2
print('Reduction: count subsets summing to (target + total) // 2')Vérifications de validité avant le DP
Avant d’exécuter le DP, vérifiez les points suivants : (1) target + total doit être pair (sinon sum(P) n’est pas un entier, ce qui est impossible) ; (2) abs(target) > total signifie que la cible est impossible à atteindre, même si tous les signes sont orientés dans le même sens. Si l’une de ces vérifications échoue, renvoyez immédiatement 0. Ces vérifications traitent proprement les cas limites sans nécessiter de cas particuliers dans la boucle du DP.
def findTargetSumWays(nums, target):
total = sum(nums)
if (target + total) % 2 != 0:
return 0 # sum(P) would be non-integer
if abs(target) > total:
return 0 # impossible to reach
new_target = (target + total) // 2
# Count subsets summing to new_target
dp = [0] * (new_target + 1)
dp[0] = 1
for num in nums:
for c in range(new_target, num - 1, -1):
dp[c] += dp[c - num]
return dp[new_target]
print(findTargetSumWays([1,1,1,1,1], 3)) # 5Suivi d’un petit exemple
Pour nums=[1,1,1,1,1], target=3 : total=5, new_target=(3+5)//2=4. Nous comptons les sous-ensembles dont la somme vaut 4 parmi [1,1,1,1,1]. Il s’agit de C(5,4)=5 (choisir 4 uns comme positifs et le cinquième comme négatif : 1+1+1+1-1=3). Le DP renvoie correctement 5. La transformation associe élégamment le problème d’attribution de signes à un problème standard de comptage de sous-ensembles.
Gestion des zéros dans le tableau de nombres
Si nums contient des zéros, attribuer le signe + ou - à un zéro ne modifie pas la somme. Chaque zéro double le nombre d’affectations valides. Le DP gère cela naturellement : lors du traitement de num=0, la boucle interne range(new_target, -1, -1) s’exécute de new_target jusqu’à 0, et dp[c] += dp[c - 0] = dp[c] double toutes les sommes atteignables. Aucun traitement particulier n’est nécessaire si vous utilisez range(new_target, num-1, -1), qui commence à new_target et descend jusqu’à 0 lorsque num=0.
# With zeros: each zero doubles the count
print(findTargetSumWays([0, 0, 1], 1)) # 4
# Assignments: +0+0+1, +0-0+1, -0+0+1, -0-0+1 = all give sum 1Comparaison des complexités
La DFS par force brute est en O(2^n). La DFS avec mémoïsation utilise un temps et un espace O(n × total). Le DP 1D fondé sur la transformation s’exécute en temps O(n × new_target) et utilise un espace O(new_target), où new_target ≤ total. Le DP 1D utilise beaucoup moins d’espace que la mémoïsation, car la transformation élimine la dimension correspondant à l’indice.
Lien avec d’autres problèmes de sac à dos
La somme cible relie plusieurs concepts du sac à dos : elle commence comme un problème d’attribution, se transforme en somme de sous-ensembles (comme Partition Equal Subset Sum) et utilise le même modèle de parcours inverse du sac à dos 0/1, mais avec du comptage (comme dans Changement de monnaie II). Maîtriser ces liens permet de classer rapidement les nouveaux problèmes d’entretien en fonction de leur ressemblance structurelle avec des modèles connus.
Cas limites et remarques pour les entretiens
Cas importants : (1) target = total : une seule façon (tous les signes sont positifs) ; (2) target = -total : une seule façon (tous les signes sont négatifs) ; (3) target = 0 avec uniquement des zéros : la réponse est 2^n ; (4) total très élevé mais n petit — la taille du tableau de DP 1D est limitée par total/2. En entretien, expliquez oralement la transformation avant de coder : c’est l’idée non évidente qui distingue les meilleurs candidats.
Alternative de DP 2D sans transformation
Sans la transformation, définissez dp[i][s] comme le nombre de façons d’attribuer des signes aux i premiers nombres pour atteindre la somme s. La somme pouvant être négative, décalez-la de total : utilisez dp[i][s + total]. Cela nécessite un tableau 2D de taille (n+1) × (2*total+1). Bien que correcte, cette solution utilise davantage d’espace et est plus difficile à coder rapidement sous la pression d’un entretien que le sac à dos 1D obtenu après transformation.
Vérification rapide
Testez 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 : la somme cible transforme l’attribution de signes en un comptage de sous-ensembles dont la somme vaut (target + total) / 2, le parcours inverse du sac à dos 0/1 1D compte les sous-ensembles en temps O(n × new_target) et en espace O(new_target), et les vérifications de validité anticipées (somme impaire, |target| > total) évitent une exécution inutile du DP. Ensuite, nous entrerons dans le domaine des plus courts chemins avec l’algorithme de Dijkstra et une file de priorité.
Questions Fréquemment Posées
La leçon « Somme cible avec signes positifs et négatifs » est-elle gratuite ?
Oui — le texte complet de « Somme cible avec signes positifs et négatifs » 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 « Somme cible avec signes positifs et négatifs » ?
Transformez le problème d’affectation target-sum en un sac à dos fondé sur la différence de sommes de sous-ensembles, et résolvez-le en O(n × sum). 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 « Somme cible avec signes positifs et négatifs » ?
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
- Sac à dos 0/1 et optimisation de l’espace
- Sac à dos illimité et rendu de monnaie II
- Partition en sous-ensembles de somme égale
- Somme cible avec signes positifs et négatifs