Subset Sum using Sets (Python)

Question:

I’m trying to solve the Subset Sum problem using Set and recursion in python.
I’ve found a lot of solutions with an array, but none with a set.

Here is my code:

def checkForSubSet(set, sum):

  if len(set) == 0 or sum < 0:
      return False

  e = set.pop() 

  if sum == e:
     return True

  return checkForSubSet(set, sum - e) or checkForSubSet(set, sum)

if __name__ == '__main__':  

  set = {3, 34, 4, 12, 5, 2}
  sum = 17
  if (checkForSubSet(set, sum) == True) :
      print("Found a subset with given sum")
  else :
      print("No subset with given sum")

sometimes its works and sometimes not… any idea?
ty for any help!

Asked By: David Shandor

||

Answers:

Your basic error is that you pop() a value from the input set. This means that in all subsequent calls you will not have that value available. This is easily seen with a print statement, where I’ve replaced sum and set with names that don’t clash with builtins:

def checkForSubSet(components, target):
  print(f"{components=} {target=}")
  if len(components) == 0 or target< 0:
      return False
  e = components.pop() 
  if target == e:
     return True
  return checkForSubSet(components, target - e) or checkForSubSet(components, target)

>>> checkForSubSet({3,2,1}, 4)
components={1, 2, 3} target=4
components={2, 3} target=3
components={3} target=1
components=set() target=-2
components=set() target=1
components=set() target=3
components=set() target=4
False

We see that the set gets ordered. 1 is tested first, and then popped off. And so on until 3 is popped off and then, hitting the base case, the function returns False. It tries the alternative cases – but the set (which persists throughout) no longer has items in it. Thus, as it goes back up the tree it is testing against an empty set.

Recursion is always about a base case and an iterative case:

def has_subset(components: Set[int], target: int) -> bool:
    print(f"{components=} {target=}")
    # Base Case: check for truth once the set is empty
    if 0 == target:
        return True
    # Iterative Case: iterate through each option you have for reducing the size of the set by one
    else:
        return any(has_subset(components - {item}, target - item) for item in components)

Here, we check the base case of an empty set. If we have an empty set and the target is 0, we return true. Otherwise, we return false.

The iterative case is (almost always) more interesting. Here, we simply iterate through all elements of the set, and check what has_subset() returns if we remove one item and reduce our target by that item. any() lets us return the first result we get that is True, or False if we exhaust all options. Note, however, that we are using the construct components - {item} which takes your initial set and returns a new set that includes all the elements except those in the set {item}. Since this is a new set, it does not fall prey to the previous problem when we recurse with it.

There are optimizations that can happen (what happens if your choice makes target a negative number?), but this should still work.

Answered By: Nathaniel Ford
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.