The answer of Martin Valgur allows for early exits when the target sub is already exceeded. This assumes that the input has no negative numbers.
This approach can however be extended to cover for negative values as well:
- In a preprocessing phase register for each index what the least and most possible sum can be from that point onwards
- Continue a search also after having found a good combination halfway, as it might be it could be extended further.
I would also avoid creating new lists at every recursive call. You could delay this until you really have a valid combination, and only then create a new list for it.
def subset_sum(numbers, target):
partial = [] # Commonly shared in recursive search tree
# Preprocessing to allow early exit, also when there are negative values in the input
acc = 0
least = [acc := acc + min(val, 0) for val in reversed(numbers)][::-1]
acc = 0
most = [acc := acc + max(val, 0) for val in reversed(numbers)][::-1]
n = len(numbers)
def recur(i, target):
if i >= n or not (least[i] <= target <= most[i]):
if target == 0:
# Provide a new list, so that if consumer mutates it, it does not affect us
yield partial[:]
return # No hope of reaching zero anymore
yield from recur(i + 1, target) # Combis without numbers[i]
partial.append(numbers[i]) # Mutate
yield from recur(i + 1, target - numbers[i])
partial.pop() # Backtrack
return recur(0, target)
# Example extended with values -6 and -10.
print(list(subset_sum([1, 2, 3, 7, 7, 9, -6, -10], 10)))
For an extended example (including values -6 and -10), this outputs the following list:
[
[7, 9, -6],
[7, 9, -6],
[3, 7],
[3, 7],
[3, 7, 7, 9, -6, -10],
[2, 7, 7, -6],
[1, 9],
[1, 3, 7, 9, -10],
[1, 3, 7, 9, -10],
[1, 2, 7],
[1, 2, 7],
[1, 2, 7, 7, 9, -6, -10],
[1, 2, 3, 7, 7, -10]
]