Based on the comments in the question, we can pick the same element in the intlist
multiple times to form a combination that sums up to the given target
.
Iterative solution
Here we step through the different values in the intlist
and build a new combination summing up to a given sum, and then adding new numbers in it.
def bestSum(target, intlist):
# initialize the dp dict
dp = {} # dp[combinationSum] = combination
for num in intlist:
dp[num] = [num]
# max steps we would need to arrive at a valid combination(if it exists) would be
# the combination containing only the min element in the intlist
# e.g. for target = 20 and intlist = [2,7], if we don't find a valid combination within
# 10 steps(all 2's), no valid answer would exist
for step in range(target//min(intlist) + 1):
for combinationSum, combination in dp.copy().items(): # avoid modifying the dict while itrating
for i in intlist:
newSum = combinationSum + i
if newSum == target:
# since we are taking a step one at a time, the first sum being equal to target
# will guarantee a shortest combination
return combination + [i]
if newSum > target:
# skip if newSum if over target(skip adding additional entries into dp to improve perf)
continue
if not dp.get(newSum):
# if the dp already has an non-zero length list in it = its length is going to be
# at least equal to new combination length or lower, so we don't need to overwrite it
dp[newSum] = combination + [i]
target, intlist = 20, [5,3,4,7]
print(bestSum(target, intlist))
prints
[5, 5, 5, 5]
for simplicity, here's what we would have in dp
at each step for intlist = [5,3,4,7]
and target of 20
-
Step : 0 - dp: {3: [3], 4: [4], 5: [5], 7: [7]}
Step : 1 - dp: {3: [3], 4: [4], 5: [5], 6: [3, 3], 7: [7], 8: [5, 3], 9: [5, 4], 10: [5, 5], 11: [4, 7], 12: [5, 7], 14: [7, 7]}
Step : 2 - dp: {3: [3], 4: [4], 5: [5], 6: [3, 3], 7: [7], 8: [5, 3], 9: [5, 4], 10: [5, 5], 11: [4, 7], 12: [5, 7], 13: [5, 5, 3], 14: [7, 7], 15: [5, 5, 5], 16: [5, 4, 7], 17: [5, 5, 7], 18: [4, 7, 7], 19: [5, 7, 7]}
Step : 3 - dp: {3: [3], 4: [4], 5: [5], 6: [3, 3], 7: [7], 8: [5, 3], 9: [5, 4], 10: [5, 5], 11: [4, 7], 12: [5, 7], 13: [5, 5, 3], 14: [7, 7], 15: [5, 5, 5], 16: [5, 4, 7], 17: [5, 5, 7], 18: [4, 7, 7], 19: [5, 7, 7], 20: [5, 5, 5, 5]}
Recursive Solution
If you want to stick with the recursive approach, you can make small changes to your original code to make it return all the combinations summing to the required target.
First of all, we don't need to make the function return True/False
, instead we can just return None
in case there is no valid combination that sums to the target value.
We want to collect all the combinations which sums to the required value so instead of returning the function on finding a valid combination what we should do is save it somewhere where we can access it later.
combinations = []
def bestSum(target, intlist, res=[]):
if (target == 0):
# found a valid combination
combinations.append(res) # collect the current combinations - THIS is what you were looking for
return
if (target < 0):
return
for i in intlist:
remainder = target - i
bestSum(remainder, intlist, res + [i])
target, intlist = 20, [5, 4, 3, 7]
bestSum(target, intlist)
print(combinations, len(combinations))
now all your combinations which sum to the target are written to combinations now, but this has a few problems..
Above code prints -
[[5, 5, 5, 5], [5, 5, 4, 3, 3], [5, 5, 3, 4, 3], [5, 5, 3, 3, 4], [5, 5, 3, 7], [5, 5, 7, 3], [5, 4, 5, 3, 3], [5, 4, 4, 4, 3], [5, 4, 4, 3, 4], [5, 4, 4, 7], [5, 4, 3, 5, 3], [5, 4, 3, 4, 4], [5, 4, 3, 3, 5], [5, 4, 7, 4], [5, 3, 5, 4, 3], [5, 3, 5, 3, 4], [5, 3, 5, 7], [5, 3, 4, 5, 3], [5, 3, 4, 4, 4], [5, 3, 4, 3, 5], [5, 3, 3, 5, 4], [5, 3, 3, 4, 5], [5, 3, 3, 3, 3, 3], [5, 3, 7, 5], [5, 7, 5, 3], [5, 7, 4, 4], [5, 7, 3, 5], [4, 5, 5, 3, 3], [4, 5, 4, 4, 3], [4, 5, 4, 3, 4], [4, 5, 4, 7], [4, 5, 3, 5, 3], [4, 5, 3, 4, 4], [4, 5, 3, 3, 5], [4, 5, 7, 4], [4, 4, 5, 4, 3], [4, 4, 5, 3, 4], [4, 4, 5, 7], [4, 4, 4, 5, 3], [4, 4, 4, 4, 4], [4, 4, 4, 3, 5], [4, 4, 3, 5, 4], [4, 4, 3, 4, 5], [4, 4, 3, 3, 3, 3], [4, 4, 7, 5], [4, 3, 5, 5, 3], [4, 3, 5, 4, 4], [4, 3, 5, 3, 5], [4, 3, 4, 5, 4], [4, 3, 4, 4, 5], [4, 3, 4, 3, 3, 3], [4, 3, 3, 5, 5], [4, 3, 3, 4, 3, 3], [4, 3, 3, 3, 4, 3], [4, 3, 3, 3, 3, 4], [4, 3, 3, 3, 7], [4, 3, 3, 7, 3], [4, 3, 7, 3, 3], [4, 7, 5, 4], [4, 7, 4, 5], [4, 7, 3, 3, 3], [3, 5, 5, 4, 3], [3, 5, 5, 3, 4], [3, 5, 5, 7], [3, 5, 4, 5, 3], [3, 5, 4, 4, 4], [3, 5, 4, 3, 5], [3, 5, 3, 5, 4], [3, 5, 3, 4, 5], [3, 5, 3, 3, 3, 3], [3, 5, 7, 5], [3, 4, 5, 5, 3], [3, 4, 5, 4, 4], [3, 4, 5, 3, 5], [3, 4, 4, 5, 4], [3, 4, 4, 4, 5], [3, 4, 4, 3, 3, 3], [3, 4, 3, 5, 5], [3, 4, 3, 4, 3, 3], [3, 4, 3, 3, 4, 3], [3, 4, 3, 3, 3, 4], [3, 4, 3, 3, 7], [3, 4, 3, 7, 3], [3, 4, 7, 3, 3], [3, 3, 5, 5, 4], [3, 3, 5, 4, 5], [3, 3, 5, 3, 3, 3], [3, 3, 4, 5, 5], [3, 3, 4, 4, 3, 3], [3, 3, 4, 3, 4, 3], [3, 3, 4, 3, 3, 4], [3, 3, 4, 3, 7], [3, 3, 4, 7, 3], [3, 3, 3, 5, 3, 3], [3, 3, 3, 4, 4, 3], [3, 3, 3, 4, 3, 4], [3, 3, 3, 4, 7], [3, 3, 3, 3, 5, 3], [3, 3, 3, 3, 4, 4], [3, 3, 3, 3, 3, 5], [3, 3, 3, 7, 4], [3, 3, 7, 4, 3], [3, 3, 7, 3, 4], [3, 3, 7, 7], [3, 7, 5, 5], [3, 7, 4, 3, 3], [3, 7, 3, 4, 3], [3, 7, 3, 3, 4], [3, 7, 3, 7], [3, 7, 7, 3], [7, 5, 5, 3], [7, 5, 4, 4], [7, 5, 3, 5], [7, 4, 5, 4], [7, 4, 4, 5], [7, 4, 3, 3, 3], [7, 3, 5, 5], [7, 3, 4, 3, 3], [7, 3, 3, 4, 3], [7, 3, 3, 3, 4], [7, 3, 3, 7], [7, 3, 7, 3], [7, 7, 3, 3]] 123
A whopping 123 combinations just for a small target of 20
. If you go through the list of combinations, you will notice that the combinations
include all permutations of a given valid combination resulting in this bloated size. The code also has to do additional work to derive all those combinations.
What can we do to avoid these duplicates?
Sorting comes to the rescue; if we ensure that each valid combination is always sorted then there cannot be a permutation of the same combination in final combinations
. How can this be leveraged in the code?
combinations = []
def bestSum(target, intlist, combination=[]):
if (target == 0):
# found a valid combination
combinations.append(combination)
return
if (target < 0):
return
for i in intlist[::-1]:
# each combination will be in ascending order
# while i is in descending order here, so if last element in my
# combination is greater than i, we can break the loop to stop
# additional work, and make sure that all the combinations are unique
if combination and combination[-1] > i:
break
remainder = target - i
bestSum(remainder, intlist, combination + [i])
target, intlist = 20, [5, 4, 3, 7]
bestSum(target, sorted(intlist)) # sort list of values
print(combinations, len(combinations))
this outputs
[[5, 5, 5, 5], [4, 4, 5, 7], [4, 4, 4, 4, 4], [3, 5, 5, 7], [3, 4, 4, 4, 5], [3, 3, 7, 7], [3, 3, 4, 5, 5], [3, 3, 3, 4, 7], [3, 3, 3, 3, 4, 4], [3, 3, 3, 3, 3, 5]] 10
Now, we don't have any permutations in our combinations! And we can easily get all the shortest/longest combinations as needed from the combinations
.
But as pointed out by @karl in his answer, the interface for this looks quite bad, and we should actually use recursive generators to make the interface cleaner. Which would look something like this -
def bestSum(target, intlist, combination=[]):
if (target == 0):
# found a valid combination
yield combination
if (target < 0):
return
for i in intlist[::-1]:
if combination and combination[-1] > i:
break
remainder = target - i
yield from bestSum(remainder, intlist, combination + [i])
target, intlist = 20, [5, 4, 3, 7]
intlist.sort()
targetCombinations = list(bestSum(target, intlist))
targetCombinations.sort(key=lambda x: len(x))
print("One of the shortest combination - ", targetCombinations[0])
Returning only one of the shortest path using recursion - this is not possible using just the return
and use of a single recursion function since once we return a value, we won't be able to keep on searching through the remaining combinations. So a way to get around this would be to wrap your recursion function in another function and make that return the shortest combination you come across after going through all combinations or use a global variable (I would avoid going the global variable way) or pass a variable by reference to the original function.
def shortestCombinationSum(target, intlist, combination=[]):
shortest = None
def bestSum(target, intlist, combination):
nonlocal shortest
if (target == 0 and (not shortest or len(combination) < len(shortest))):
# found a valid combination which is shorter
shortest = combination
return
if (target < 0):
return
for i in intlist[::-1]:
if combination and combination[-1] > i:
break
remainder = target - i
bestSum(remainder, intlist, combination + [i])
bestSum(target, intlist, combination)
return shortest
target, intlist = 20, [5, 4, 3, 7]
print(shortestCombinationSum(target, sorted(intlist)))
Although this makes the function look weird, the interface for the shortestCombinationSum
is fairly clean. Another alternative could've been making shortest
as a global variable and then keep updating it, we would not have to wrap the function in another function in that case, but we will have to clear the value of the global variable in case we are calling the function over and over again with different sets of inputs.
I would still recommend the iterative solution over the recursive solution above because given an intlist
of size n
, the tree you have in your diagram would be an n-ary tree
, having n branches on each node starting from target
at the top. The iterative solution would be similar to a level order traversal in the n-ary tree and would stop when it reaches the level where it finds the solution. While the recursive solution would be DFS over the tree and it would need to go through each and every combinations(pruning duplicate permutations) of the n-ary tree.