LC 0039 0040 0216 0377 [M] [M] [M] [M] Combination Sum (Subset Sum) I II III IV - ALawliet/algorithms GitHub Wiki

a combination is a subsets problem because order doesn't matter for both

so this problem is similar to generate Subsets but with more conditions

I

I: The same number may be chosen from candidates an unlimited number of times. Two combinations are unique if the frequency of at least one of the chosen numbers is different.

BAD

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        def helper(path):
            if sum(path) == target:
                if sorted(path) not in coll:
                    coll.append(sorted(path))
            elif sum(path) > target:
                return
            else:
                for i in candidates:
                    helper(path + [i])
        
        coll = []
        helper([])
        return coll

but the trick to get uniques without double sorting is to sort just once at the beginning then keep track of the start index we used so we can scan for any frequency of that first then completely move on to the next digit

GOOD

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        def helper(target, start, path):
            if target == 0:
                coll.append(path)
            elif target < 0: # exceed the scope, stop exploration.
                return
            else:
                for i in range(start, len(candidates)):
                    helper(target - candidates[i], i, path + [candidates[i]]) # i not i+1 because we can use many times, give the current number another chance, rather than moving on
        
        coll = []
        helper(target, 0, [])
        return coll

II

II: Each number in candidates may only be used once in the combination.

each number can only be used once so we either exclude or sort and skip dupes

the important difference to Combination Sum I is looping to create many branches for all the numbers vs. just include/exclude and moving to the next idx

BAD (TLE)

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        def helper(target, idx, path):
            if target == 0:
                if path not in coll:
                    coll.append(path)
            elif idx == len(candidates):
                return
            elif target < 0:
                return
            else:
                helper(target, idx + 1, path)
                helper(target - candidates[idx], idx + 1, path + [candidates[idx]])
        
        coll = []
        candidates.sort()
        helper(target, 0, [])
        return coll

or we can skip dupes cause it's sorted (USE THIS METHOD)

GOOD

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        def helper(target, idx, path):
            if target == 0:
                coll.append(path)
            elif target < 0:
                return
            else:
                for i in range(idx, len(candidates)):
                    if i > idx and candidates[i] == candidates[i-1]: continue # skip dupes
                    helper(target - candidates[i], i+1, path + [candidates[i]]) # i+1 because we can only use once
        
        coll = []
        candidates.sort() # sort to skip dupes
        helper(target, 0, [])
        return coll

III

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        def dfs(target, i, path):
            if target == 0:
              if len(path) == k:
                res.append(path)
            elif target < 0:
                return
            elif target > 0:
                for i in range(i, 10):
                    dfs(target - i, i + 1, path + [i])
        
        res = []
        dfs(n, 1, [])
        return res
class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        def helper(target, idx, path):
            if target == 0:
              if len(path) == k:
                coll.append(path)
            elif target < 0:
                return
            else:
                for i in range(idx, 10):
                    helper(target - i, i + 1, path + [i])
        
        coll = []
        helper(n, 1, [])
        return coll
  • it's titled wrong, since order matters it's actually a PERMUTATION sum

just like Coin Change 2 (num ways to make change given unlimited denoms) but since this one is a count PERMUTATION sum and coin change is a count COMBINATION sum, the difference is switching the for loops around

class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        T = [0] * (target + 1)

        T[0] = 1
       
        for i in range(1, target + 1):
             for n in nums:
                if n <= i:
                  T[i] += T[i-n]

        return T[-1]