I would start writing a generic combinations
function which generates combinations of a specified size -
def combinations(items, size):
if size == 0:
yield ()
elif not items:
return
else:
head, *tail = items
for p in combinations(tail, size - 1):
yield (head, *p)
yield from combinations(tail, size)
We're relying on Python's generators which lazily produce the output. We can use list
to get all of the results -
data = [ 1, 2, 3, 4, 5, 6, 7, 8 ]
list(combinations(data, 2))
# [ (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (2, 3),
# (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (3, 4), (3, 5), (3, 6),
# (3, 7), (3, 8), (4, 5), (4, 6), (4, 7), (4, 8), (5, 6), (5, 7),
# (5, 8), (6, 7), (6, 8), (7, 8) ]
It can easily generate combinations of other sizes, and the input iterable can contain elements of any type -
list(combinations("abcd", 3)))
# [ ('a', 'b', 'c'), ('a', 'b', 'd'),
# ('a', 'c', 'd'), ('b', 'c', 'd') ]
By writing combinations
as a standalone function we detangle the concerns of our program. As a result sum_pairs
is easy to write and test. We pass 2
as the size
argument because we want to generate pairs (of two numbers) -
def sum_pairs(ints, goal):
for p in combinations(ints, 2): # <-- use combinations
if sum(p) == goal:
return p
return None # <-- don't forget unsolvable case
print(sum_pairs(data, 13))
# (5, 8)
Because we are using Python's generators, sum_pairs
will return immediately after finding an answer. This short-circuit behaviour means no unnecessary combinations or iterations are wasted.
When given an unreachable goal, sum_pairs
will return None
-
print(sum_pairs(data, 99))
# None
A small modification demonstrates the effectiveness of our combinations
generic. In this program, any_sum
will find a combination of any size that sums to our expected goal -
def any_sum(ints, goal):
for size in range(len(ints)):
for p in combinations(ints, size + 1):
if sum(p) == goal:
return p
return None
While this is not the most efficient implementation of any_sum
, it does show how combinations
can be reused without introducing new complexity -
print(any_sum(data, 17))
# (2, 7, 8)
print(any_sum(data, 25))
# (4, 6, 7, 8)
print(any_sum(data, 33))
# (3, 4, 5, 6, 7, 8)
print(any_sum(data, 99))
# None