6

I am trying to write a function that will not only determine whether the sum of a subset of a set adds to a desired target number, but also to print the subset that is the solution.

Here is my code for finding whether a subset exists:

def subsetsum(array,num):

    if num == 0 or num < 1:
        return False
    elif len(array) == 0:
        return False
    else:
        if array[0] == num:
            return True
        else:
            return subsetsum(array[1:],(num - array[0])) or subsetsum(array[1:],num)

How can I modify this to record the subset itself so that I can print it? Thanks in advance!

Chase McCoy
  • 1,550
  • 2
  • 13
  • 16
  • Return a tuple of the subset and whether or not the sum adds to your desired target number. Like `return subset, True`. – 2rs2ts Apr 15 '14 at 15:16
  • Not sure how to do that. How do I get the tuple? – Chase McCoy Apr 15 '14 at 15:18
  • Did you try to search the web for how to do that? [Here's one of the top results on google](http://forums.udacity.com/questions/2017072/python-101-unit-2-multiple-return-values-and-assignments). – 2rs2ts Apr 15 '14 at 15:20
  • Sorry, I do know how to return multiple values. That is not the problem. I am wondering how I can add the values that add up to the desired number to a list or tuple. Right now the function only determines True or False. I need to populate the tuple with values. – Chase McCoy Apr 15 '14 at 15:22
  • You need to make some minor effort on your own first. [Take a look at the `append()` method of lists](http://www.tutorialspoint.com/python/list_append.htm). – 2rs2ts Apr 15 '14 at 15:28

7 Answers7

11

Based on your solution:

def subsetsum(array,num):

    if num == 0 or num < 1:
        return None
    elif len(array) == 0:
        return None
    else:
        if array[0] == num:
            return [array[0]]
        else:
            with_v = subsetsum(array[1:],(num - array[0])) 
            if with_v:
                return [array[0]] + with_v
            else:
                return subsetsum(array[1:],num)
Samy Arous
  • 6,794
  • 13
  • 20
7

Modification to also detect duplicates and further solutions when a match happened

def subset(array, num):
    result = []
    def find(arr, num, path=()):
        if not arr:
            return
        if arr[0] == num:
            result.append(path + (arr[0],))
        else:
            find(arr[1:], num - arr[0], path + (arr[0],))
        find(arr[1:], num, path)
    find(array, num)
    return result
Community
  • 1
  • 1
harry
  • 1,061
  • 1
  • 13
  • 13
6

You could change your approach to do that more easily, something like:

def subsetsum(array, num):
    if sum(array) == num:
        return array
    if len(array) > 1:
        for subset in (array[:-1], array[1:]):
            result = subsetsum(subset, num)
            if result is not None:
                return result

This will return either a valid subset or None.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • @user2872761 oops, typo; fixed – jonrsharpe Apr 15 '14 at 15:28
  • This works perfect. However, I can't wrap my head around the for loop. More or less what is the explanation of that specific python syntax? In a particular iteration, what are it's values (subset, and the two slices)? Is it iterating through each subset value AND two arrays (the slices) on each iteration? I ask because I'm trying to convert the solution into PHP. – Alpinestar22 Oct 03 '14 at 00:02
  • 1
    Why don't you put in some `print`s and find out? – jonrsharpe Oct 03 '14 at 07:17
  • On my interpreter this fails for the following input: `subsetsum([1,2,3,-1], 0) when it should return `[1,-1]` – Arnab Datta Oct 11 '14 at 20:17
  • 2
    @ArnabDatta this code only does contiguous subsets, you'd have to modify it slightly to handle non-contiguous subsets too. – jonrsharpe Oct 11 '14 at 20:25
  • I understand, but as far as I understood the subset sum problem does not make any claims as to the nature of the input (and neither does the OP). – Arnab Datta Oct 12 '14 at 01:52
  • @ArnabDatta I based it on the OP's approach so far, and they say it *"works perfect"*, but I'm happy to revise given new information. – jonrsharpe Oct 12 '14 at 07:05
  • 2
    I tried your code and it did not work well with a array like subsetsum([2,5,3,4,6], 7), it returned [5,2], but it should also return[3,4] as well. – Kun Feb 16 '16 at 02:33
  • @Fox you could modify it into a generator function to yield all subsets, not that wasn't part of this question's requirements. – jonrsharpe Feb 16 '16 at 07:17
5

Thought I'll throw another solution into the mix.

We can map each selection of a subset of the list to a (0-padded) binary number, where a 0 means not taking the member in the corresponsing position in the list, and 1 means taking it.

So masking [1, 2, 3, 4] with 0101 creates the sub-list [2, 4].

So, by generating all 0-padded binary numbers in the range between 0 and 2^LENGTH_OF_LIST, we can iterate all selections. If we use these sub-list selections as masks and sum the selection - we can know the answer.

This is how it's done:

#!/usr/bin/env python

# use a binary number (represented as string) as a mask
def mask(lst, m):
    # pad number to create a valid selection mask 
    # according to definition in the solution laid out 
    m = m.zfill(len(lst))
    return map(lambda x: x[0], filter(lambda x: x[1] != '0', zip(lst, m)))

def subset_sum(lst, target):
    # there are 2^n binary numbers with length of the original list
    for i in xrange(2**len(lst)):
        # create the pick corresponsing to current number
        pick = mask(lst, bin(i)[2:])
        if sum(pick) == target:
            return pick
    return False


print subset_sum([1,2,3,4,5], 7)

Output:

[3, 4]

To return all possibilities we can use a generator instead (the only changes are in subset_sum, using yield instead of return and removing return False guard):

#!/usr/bin/env python

# use a binary number (represented as string) as a mask
def mask(lst, m):
    # pad number to create a valid selection mask 
    # according to definition in the solution laid out 
    m = m.zfill(len(lst))
    return map(lambda x: x[0], filter(lambda x: x[1] != '0', zip(lst, m)))

def subset_sum(lst, target):
    # there are 2^n binary numbers with length of the original list
    for i in xrange(2**len(lst)):
        # create the pick corresponsing to current number
        pick = mask(lst, bin(i)[2:])
        if sum(pick) == target:
            yield pick

# use 'list' to unpack the generator
print list(subset_sum([1,2,3,4,5], 7))

Output:

[[3, 4], [2, 5], [1, 2, 4]]

Note: While not padding the mask with zeros may work as well, as it will simply select members of the original list in a reverse order - I haven't checked it and didn't use it.

I didn't use it since it's less obvious (to me) what's going on with such trenary-like mask (1, 0 or nothing) and I rather have everything well defined.

Reut Sharabani
  • 30,449
  • 6
  • 70
  • 88
  • This remains an intriguing solution. If you are trying to run this in python3, two minor adaptations are required: - instead of `xrange`, use `range`. In Python 3, xrange is removed and range behaves almost like xrange in python 2 - instead of `pick = mask(lst, bin(i)[2:])` write `pick = list(mask(lst, bin(i)[2:]))` to unpack the generator. – Olaf May 16 '23 at 12:51
1

Slightly updated the below code to return all possible combinations for this problem. Snippet in the thread above will not print all possible combinations when the input is given as subset([4,3,1],4)

def subset(array, num):
    result = []
    def find(arr, num, path=()):
        if not arr:
            return
        if arr[0] == num:
            result.append(path + (arr[0],))
        else:
            find(arr[1:], num - arr[0], path + (arr[0],))
        find(arr[1:], num, path)
    find(array, num)
    return result
aaossa
  • 3,763
  • 2
  • 21
  • 34
Tony
  • 33
  • 1
  • 8
0

A bit different approach to print all subset through Recursion.

def subsetSumToK(arr,k):
    if len(arr)==0:
        if k == 0:
            return [[]]
        else:
            return []
    
    output=[]
    if arr[0]<=k: 
        temp2=subsetSumToK(arr[1:],k-arr[0])  #Including the current element 
        if len(temp2)>0:
            for i in range(len(temp2)):
                temp2[i].insert(0,arr[0])
                output.append(temp2[i])
    
    temp1=subsetSumToK(arr[1:],k)            #Excluding the current element
    if len(temp1)>0:
        for i in range(len(temp1)):
            output.append(temp1[i])
    return output

arr=[int(i) for i in input().split()]
k=int(input())
sub=subsetSumToK(arr,k)
for i in sub:
    for j in range(len(i)):
        if j==len(i)-1:
            print(i[j])
        else:
            print(i[j],end=" ")
0

Rather than using recursion, you could use the iterative approach.

def desiredSum(array, sum):

  numberOfItems = len(array)
  storage = [[0 for x in range(sum + 1)] for x in range(numberOfItems + 1)]

  for i in range(numberOfItems + 1):
    for j in range(sum + 1):

        value = array[i - 1]

        if i is 0: storage[i][j] = 0
        if j is 0: storage[i][j] = 1

        if value <= j:

            noTake = storage[i - 1][j]
            take = storage[i - 1][j - value]
            storage[i][j] = noTake + take

  return storage[numberOfItems][sum]
Adam Cooper
  • 867
  • 1
  • 6
  • 23