0

I have a list of numbers (source set), and someone took some or all of those numbers and summed them randomly to make a smaller set of numbers (target set). I have the source set and the target set, but I need to figure out what combination was used. Difficulty: I can't guarantee that all numbers in the source set were used to make the target set, and I can't guarantee that all numbers in the target set came from the source set. Here's a simple example:

  • Source Set = 1,2,3,4,5
  • Target Set = 3,8
  • Result:
    • ResultSet1: Sum([1,2])=3 Sum([3,5])=8 NotFound([]) NotUsed([4])
    • ResultSet2: Sum([3])=3 Sum([1,2,5])=8 NotFound([]) NotUsed([4])
    • ResultSet3: Sum([1,3,4])=8 NotFound([3]) NotUsed([2,5])
  • Invalid answers:
    • InvalidSet1: Sum([1,2])=3 Sum([3])=3 Sum([3,5])=8 NotFound([]) NotUsed:[4]
      • Reason: each number from the source set may only be used once to create any target in a given result set

I have found some great examples for deriving the above given a single target value, but I can't find a way to do it with an array of target values rather than a single value (and my coding skills are unfortunately not up to the task). The best start I have is this question, with the code below (note that I removed the s >= target check to accommodate my data):

def subset_sum(numbers, target, partial=[]):
    s = sum(partial)

    # check if the partial sum is equals to target
    if s == target: 
        print "sum(%s)=%s" % (partial, target)
    #if s >= target:
    #    return  # if we reach the number why bother to continue

    for i in range(len(numbers)):
        n = numbers[i]
        remaining = numbers[i+1:]
        subset_sum(remaining, target, partial + [n]) 

if __name__ == "__main__":
    subset_sum([1,2,3,4,5],4)

This will output:

sum([1, 3])=4
sum([4])=4

I've made some valiant attempts at adding a second layer of recursion to support an array for the target, but I won't embarrass myself by putting the failures here.

I understand that the above code has limitations, and I'm open to solutions based on this code, or completely new code, and output in just about any logical format. Python is strongly preferred, but I would entertain just about anything (note: java gives me hives).

2 Answers2

0

You mean something like:

In []:
import itertools as it

source = [1, 2, 3, 4, 5]
targets = [3, 8]

p = [[a for n in range(1, len(source)) for a in it.combinations(source, n) if sum(a) == t] for t in targets]
[dict(zip(targets, a)) for a in it.product(*p) if len(sum(a, tuple())) == len(set(sum(a, tuple())))]

Out[]:
[{3: (3,), 8: (1, 2, 5)}, {3: (1, 2), 8: (3, 5)}]
AChampion
  • 29,683
  • 4
  • 59
  • 75
  • Thanks, but not quite. Where your code seems to output all the possible combinations of each member of the target array individually, it does so mutually exclusive to the other members of the target array. I will try to clarify my OP, but said here: if you use 1 and 2 to make 3, then you can't use 1 or 2 to make 8 in that result set. Using your output format it would include: Set1: {3: [(1, 2)], 8: [(3, 5)]} Set2: {3: [(3)], 8: [(1, 2, 5)]} Set3: {8: [(1, 3, 4)]} etc... – Ben McMahon Apr 19 '18 at 03:35
  • If the number are very low then you can brute force it pretty easily - updated. – AChampion Apr 20 '18 at 02:17
  • your output looks like exactly what I'm after... but for some reason I can't get your code to run (problem is likely between my keyboard and my chair). I was able to get some output by adding print (p) at the end, but it's not the correct result: [[(3,), (1, 2)], [(3, 5), (1, 2, 5), (1, 3, 4)]] – Ben McMahon Apr 22 '18 at 22:29
  • assign a value to the list of dicts, e.g. `result = [dict(zip(...))]; print(result)`. The above working in interactive mode. – AChampion Apr 22 '18 at 22:48
  • ok thanks for clarifying! you're right - i was misreading the output. choosing this as it is slightly more elegant (doesn't require massive recursion for larger sets). thanks all for the help – Ben McMahon Apr 30 '18 at 21:22
0

The only way I found is quite inefficient, and I'm pretty sure there has to be a smarter way, but it works.

The idea is to get all combinations, get the ones for the first number, go through all of them, remove the used numbers from the list, generate all combinations, get those that match the second number and iterate.

Here's the code. Again, quite ugly but it does the job:

from collections import defaultdict
import itertools as it

def get_all_combinations(l):
    return [a for n in range(1, len(l)) for a in it.combinations(l, n)]

def get_combinations_for_target(combinations, target):
    if combinations is None:
        return []
    return [combination for combination in combinations if sum(combination) == target]

def get_list_without_used_numbers(l, combination):
    used_numbers = []
    for item in combination:
        used_numbers.append(item)

    diff_list = list(set(l) - set(used_numbers))
    return diff_list

source = [1, 2, 3, 4, 5]
combinations = get_all_combinations(source)
combinations_first =  get_combinations_for_target(combinations, 3)

combinations_both = defaultdict(dict)

for combination in combinations_first:
    partial_list = get_list_without_used_numbers(source, combination)
    partial_combinations = get_all_combinations(partial_list)
    combinations_both[combination] = get_combinations_for_target(partial_combinations, 8)

print(combinations_both)
ChatterOne
  • 3,381
  • 1
  • 18
  • 24
  • thanks... but the only output I got from this was "defaultdict(, {(1, 2): [(3, 5)], (3,): [(1, 2, 5)]})" - does that indicate a library problem? – Ben McMahon Apr 22 '18 at 22:30
  • What do you mean? That's the output you need, it means that the combinations are either `(1, 2), (3, 5)` or `(3), (1, 2, 5)` for an input of `(3, 8)` – ChatterOne Apr 23 '18 at 08:01