Partition to Two Equal Sum Subsets - Dynamic Programming / Knapsack

https://algo.monster/problems/partition_equal_subset_sum

This can also be solved using two-pointers
# WRITE YOUR BRILLIANT CODE HERE
n = len(nums)
l,r = 0, n-1
nums.sort()
l_sum, r_sum=nums[l], nums[r]
while l < r:
if l_sum < r_sum:
l+=1
l_sum+=nums[l]
elif l_sum > r_sum:
r-=1
r_sum+=nums[r]
else:
return True
return False

Another way to dedupe in DFS is using search-space deduping. See Backtracking → Subsets for details.
Instead of tracking used indices, we can pick next indices as candidates to visit only unique subsets.

Is Mem working?
We are only checking for current sum has been already explored. But used array may not be same for previously explored current sum. Don’t we need to check current sum same and used array same before looking up mem value?

this will fail the following test: 1 6 6 11

why do we need the used array? would this also work?

def can_partition(nums: List[int]) → bool:

target = sum(nums) // 2
def dfs(i, cursum):
    if cursum == target:
        return True
    for ni, num in enumerate(nums[i::]):
        if cursum + num <= target:
            return dfs(ni +1, cursum + num)
    return False
  
return dfs(0,0)

we can modify the prefix sum to do it
if(ps == target || map.containsKey(ps-target)){
return true;
}

we can modify the prefix sum to do it
if(ps == target || map.containsKey(ps-target)){
return true;
}

My dfs solution works but got TLE on leetcode. Could anyone explain the DP solution more clearly? I’ve found this one is tough to understand.

Check this out: https://www.geeksforgeeks.org/subset-sum-problem-dp-25/

Ok, thanks, found a proper way to do DP after watching this https://www.youtube.com/watch?v=fWX9xDmIzRI&list=PLgUwDviBIf0qUlt5H_kiKYaNSqJ81PMMY&index=15

Sharing all 3 solutions, I’ve found if I start from brute force, DP will make more sense to me

# # S1 - Top-down Recursion, TLE, TC = 2**N, SC = N
# class Solution:
#     def canPartition(self, nums: List[int]) -> bool:
#         total = sum(nums)
#         if total % 2 != 0:
#             return False
#         half = total // 2
        
#         def f(idx, nums, target) -> bool:
#             if idx == 0:
#                 if nums[idx] == target:
#                     return True
#                 return False
#             if target == 0:
#                 return True
#             # Take
#             take = False
#             if target >= nums[idx-1]:
#                 take = f(idx-1, nums, target-nums[idx])
#             # Not take
#             not_take = f(idx-1, nums, target)
#             return take or not_take

#         return f(len(nums)-1, nums, half)

# # S2 - Top-down Recursion with Memoiation, TC = 2**N, SC = N + N*(SUM(nums))
# class Solution:
#     def canPartition(self, nums: List[int]) -> bool:
#         total = sum(nums)
#         if total % 2 != 0:
#             return False
#         half = total // 2
        
#         def f(idx, nums, target, dp) -> bool:
#             if idx == 0:
#                 if nums[idx] == target:
#                     return True
#                 return False
#             if target == 0:
#                 return True
#             if dp[idx][target] != -1:
#                 return dp[idx][target]
#             # Take
#             take = False
#             if target >= nums[idx]:
#                 take = f(idx-1, nums, target-nums[idx], dp)
#             # Not take
#             not_take = f(idx-1, nums, target, dp)
#             dp[idx][target] = take or not_take
#             return dp[idx][target]

#         dp = [[-1 for _ in range(half+1)] for _ in range(len(nums))]
#         return f(len(nums)-1, nums, half, dp)

# S3 - Bottom-up Tabulation, TC = N*SUM(nums), SC = N*(SUM(nums))
class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        total = sum(nums)
        if total % 2 != 0:
            return False
        half = total // 2
        
        if len(nums) == 1:
            return False
        
        # Build base cases
        dp = [[False for _ in range(half+1)] for _ in range(len(nums))]
        # For any idx, if target is 0 then the value of dp is True
        for i in range(len(dp)):
            dp[i][0] = True
        # If idx is 0, only when the secondary idx is nums[idx], the dp
        # value is true
        if nums[0] <= half:
            dp[0][nums[0]] = True
        # Now form the nested loops
        for idx in range(1, len(dp)):
            for target in range(1, half+1):
                # Take
                take = False
                if target >= nums[idx]:
                    diff = target-nums[idx]
                    # print(f"target: {target}, idx: {idx}, diff")
                    take = dp[idx-1][target-nums[idx]]
                # Not take
                not_take = dp[idx-1][target]
                dp[idx][target] = take or not_take
        
        return dp[-1][-1]

To me, using HashMap with custom class to record visited node seems much easier to understand.
This is what I mean:

private static class PartitionCoordinate{
int x;
int y;

    public PartitionCoordinate(int xValue, int yValue) {
        this.x = xValue;
        this.y = yValue;
    }
    
    @Override
    public int hashCode() {
        return x + y;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (obj == null) return false;
        PartitionCoordinate coordinate = (PartitionCoordinate)obj;
        return (coordinate.x == x && coordinate.y == y);
    }
}

public static boolean canPartition(List<Integer> nums) {
    int total = 0;
    int target = 0;
    for(int num: nums) {
        total += num;
    }
    if (total%2 == 1) return false;
    target = total/2;
    Map<PartitionCoordinate, Boolean>set = new HashMap<>();
    
    return partitionDfs(0, target, set, nums, 0);
}

private static boolean partitionDfs(int sum, int target, Map<PartitionCoordinate, Boolean>set,
                          List<Integer> nums, int index) {
    PartitionCoordinate coordinate = new PartitionCoordinate(index, sum);
    if(set.containsKey(coordinate)) return set.get(coordinate);
    if (sum == target) return true;
    if (sum > target || index == nums.size()) return false;
    
    boolean result1 = false;
    boolean result2 = false;
    int next = sum + nums.get(index);
    result1 = partitionDfs(next, target, set, nums, index + 1);
    next = sum;
    result2 = partitionDfs(next, target, set, nums, index + 1);
    set.put(coordinate, (result1 || result2));
    return (result1 || result2);   
}

Much cleaner version of the bottom up approach

using namespace std;

bool can_partition(std::vector<int> nums) {
    int total = accumulate(nums.begin(), nums.end(), 0);
    if (total % 2 != 0) return false;
    int size = nums.size();
    int target = total / 2;
    vector<vector<bool>> dp(2, vector<bool>(target + 1, false));
    dp[0][0] = dp[1][0] = true;
    for (int i = 0; i < size; i++) {
        for (int s = 1; s <= target; s++) {
            if (!dp[1][s] && s - nums[i] >= 0) {
                dp[1][s] = dp[0][s - nums[i]];
            }
        }
        dp[0] = dp[1];
    }
    return dp[1][target];
}

I think the time complexity is wrong, it should be 2^n -1, so it is O(log2^n) since there will be 2^n -1 nodes.

2^n = 2*2^n-1, 2 is constant

The O(n * target) complexity is not great if “target” was large. It’s possible to solve this in O(n*n) time and O(n) space:

def can_partition(nums: List[int]) → bool:
total = sum(nums)
if total % 2 == 1:
return False

achievable_weights = set([0])
for new_weight in nums:
    new_set = achievable_weights.copy()
    for previous_sum in achievable_weights:
        new_set.add(previous_sum + new_weight)
    achievable_weights = new_set

return total // 2 in achievable_weights

If one of the elements in the input array is 2,000,000,000 (still a valid int in Java), I get an OutOfMemoryError with the proposed top-down solution. An alternative to avoid this problem is, instead of having the 2D array memo, to create a class Node that contains two int fields, sum and n, and make the memo be a HashSet of Node instances. Although it takes more millis to run, it doesn’t run out of memory for big elements.