Find which sum of any numbers in an array equals amount

Viewed 83

I have a customer who sends electronic payments but doesn't bother to specify which invoices. I'm left guessing which ones and I would rather not try every single combination manually. I need some sort of pseudo-code to do it and then I can adapt it but I'm not sure I can come up with a good algorithm myself. . I'm familiar with php, bash, and python but I can adapt.

I would need an array with the following numbers: [357.15, 223.73, 106.99, 89.96, 312.39, 120.00]. Those are the amounts of the invoices. Then I would need to find a sum of any combination of two or more of those numbers that adds up to 596.57. Once found the program would need to tell me exactly which numbers it used to reach the sum so I can then know which invoices got paid.

2 Answers

You can use itertools.combinations(t,r) to list all combinations of r elements in array t.

So we loop on the possible values of r, then on the results of itertools.combinations:

import itertools

def find_sum(t, obj):
  t = [x for x in t if x < obj] # filter out elements which are too big
  for r in range(1, len(t)+1):    # loop on number of elements
    for subt in itertools.combinations(t, r):  # loop on combinations of r elements
      if sum(subt) == obj:
        return subt
  return None

find_sum([1,2,3,4], 6)
# (2, 4)
find_sum([1,2,3,4], 10)
# (1, 2, 3, 4)
find_sum([1,2,3,4], 11)
# none
find_sum([35715, 22373, 10699, 8996, 31239, 12000], 59657)
# none

Rounding errors:

  • The code above is meant to be used with integers, rather than floats.
  • To use with floats, replace the test sum(subt) == obj with the more forgiving test sum(subt) - obj < 0.01.

Relevant documentation:

This is very similar to the Subset Sum problem and can be solved using a similar approach to the typical brute-force method used for that problem. I have to do this often enough that I keep a simple template of this algorithm handy for when I need it. What is posted below is a slightly modified version1.

This has no restrictions on whether the values are integer or float. The basic idea is to iterate over the list of input values and keep a running list of every subset that sums to less than the target value (since there might be a later value in the inputs that will yield the target). It could be modified to handle negative values as well by removing the rule that only keeps candidate subsets if they sum to less than the target. In that case, you'd keep all subsets, and then search through them at the end.

import copy

def find_subsets(base_values, taget):
    possible_matches = [[0, []]] # [[known_attainable_value, [list, of, components]], [...], ...]
    matches = [] # we'll return ALL subsets that sum to `target`
    for base_value in base_values:
        temp = copy.deepcopy(possible_matches) # Can't modify in loop, so use a copy
        for possible_match in possible_matches:
            new_val = possible_match[0] + base_value
            if new_val <= target:
                new_possible_match = [new_val, possible_match[1]]
                new_possible_match[1].append(base_value)
                temp.append(new_possible_match)
                if new_val == target:
                    matches.append(new_possible_match[1])
        possible_matches = temp
    return matches


find_subsets([list, of input, values], target_sum)

This is a very inefficient algorithm and it will blow up quickly as the size of the input grows. The Subset Sum problem is NP-Complete, so you are not likely to find a generalized solution that will work in all cases and is efficient.


1: The way lists are being used here is kludgy. If the goal was to simply find any match, the nested lists could be replaced with a dictionary, and we could exit right away once a match is found. But doing that will cause intermediate subsets that sum to the same value to also map to the same dictionary slot, so only one subset with that sum is kept. Since we need to report all matching subsets (because the values represent checks and are presumably not fungible even if the dollar amounts are equal), a dictionary won't work.

Related