Subset Sum Problem

  • Given objects, where:

    • is it weight of the object (non-negative integers) (arr)
  • is the target weight (target)

  • is the number of copies of the object in the backpack

  • (decision variables) The goal is to find a sequence (where is the number of copies of the object) such that:

  • We define a matrix dp (n+1 x target+1) where dp[i][j] is True if there exists there exists a subset of the first i elements that sums to j, and False otherwise.

01...j - arr[i-1]...j...target
010000000
11
...1
i-1dp[i-1,j-arr[i-1]]dp[i-1,j]
i1dp[i,j]
...1
n1
 
def subset_sum_dp(arr, target):
    """
    Determines if there is a subset of the array `arr` whose sum equals `target`.
    Also returns the list of items that form the subset if it exists.
 
    This function uses dynamic programming to efficiently solve the problem.
    The main idea is to use a 2D table `dp`, where `dp[i][j]` is True if there is a subset
    of the first `i` elements of `arr` that sums to `j`, and False otherwise.
 
    Args:
    arr (list of int): The list of integers to check for subsets.
    target (int): The target sum to check for using subsets of `arr`.
 
    Returns:
    tuple: A tuple containing:
        - list: The list of items that form the subset if it exists, otherwise an empty list.
        - dp: The 2D table used to solve the problem.
    
    Time complexity: O(n * target)
    Space complexity: O(n * target)
    
    """
 
    # Step 1: Initialize the length of the array.
    n = len(arr)
 
    # Step 2: Create a DP table of size (n+1) x (target+1), initialized to False.
    # dp[i][j] will be True if there's a subset of the first `i` elements that sums to `j`.
    dp = [[False] * (target + 1) for _ in range(n + 1)]
 
    # Step 3: Base case: A sum of 0 is always achievable with the empty subset.
    # Set all dp[i][0] to True, since sum 0 is possible with any subset (including empty).
    for i in range(n + 1):
        dp[i][0] = True
 
    # Step 4: Fill the DP table and track the inclusion of elements.
    include = [[False] * (target + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):  # Iterate over the elements of the array
        for j in range(1, target + 1):  # Iterate over all possible target sums
            # If the current element is greater than the current sum `j`, we cannot include it.
            if arr[i - 1] > j:
                dp[i][j] = dp[i - 1][j]  # Exclude the element
            else:
                # Otherwise, we check two cases:
                # 1. Exclude the current element: dp[i - 1][j]
                # 2. Include the current element: dp[i - 1][j - arr[i - 1]]
                if dp[i - 1][j]:
                    dp[i][j] = True
                elif dp[i - 1][j - arr[i - 1]]:
                    dp[i][j] = True
                    include[i][j] = True  # Mark the element as included
 
    # Step 5: If the target sum is achievable, reconstruct the subset.
    if dp[n][target]:
        subset = []
        j = target
        for i in range(n, 0, -1):
            if include[i][j]:
                subset.append(arr[i - 1])
                j -= arr[i - 1]
        return subset[::-1], dp  # Reverse the subset for correct order
    else:
        return [], dp
        
# Example usage
if __name__ == "__main__":
    # Test the function with a sample array and target sum
    result, _ = subset_sum_dp([23, 134, 5, 2,4], 9)
    if result:
      print("The subset that sums to the target is:", result)
    else:
      print("No subset sums to the target.")  

Visualization