Backtracking - some discussion

Table of Contents

Subsets

def subsets(arr: list) -> list:
    n = len(arr)
    def backtrack(idx, comb):
        res.append(comb)
        for i in range(idx, n):
            backtrack(i + 1, comb + [arr[i]])
        
    res = list()
    backtrack(0, [])
    return res

Continuous Subsets

def ctsSubsets(arr: list) -> list:
    n = len(arr)
    def backtrack(idx, comb):
        res.append(comb)
        if idx < n:
            backtrack(idx + 1, comb + [arr[idx]])
        
    res = list()
    for i in range(n):
        backtrack(i+1, [arr[i]])
    return res

How the above two algorithms differ? The “subsets” starts with one single function call, and use a for look inside. While the “contiguous” subsets call the backtrack with for loop outside. The main difference can be outlined by the following graph. In the “subsets” problem, every integer can be combined with a later integer, while the second problem requires that once a start is chosen, all the following numbers has to be added in sequence.

The idx parameter indicates the position of the integer that will be added to the current combination at the next call. So always, when idx==n No new function will be called. Note that this stopping criteria is controlled by the for loop in the “subsets” problem.

image-20220817014608554

Subsets-variation: non-repetitive

A variation of the subsets problem is to narrow the candidates to a sum of a target without repetitious subsets. Though we only need to add two more lines before recursively call the backtrack, the actual considerations can be complex. Consider a list [1,1,6,7], we wants to keep [1,7] for only once, but we want to keep [1,1,6] in the same time, so when the second 1 should be added?

class Solution:
    def combinationSum2(self, candidates, target):
        res = list()
        n = len(candidates)
        def backtrack(idx, comb):
            # meet the target
            if sum(comb) == target:
                res.append(comb)
                return

            # all the numbers have been considered
            if idx == n or sum(comb) > target:
                return
            
            for i in range(idx, n):
                if i > idx and candidates[i] == candidates[i-1]:
                    continue
                backtrack(i + 1, comb + [candidates[i]])

        candidates.sort()
        backtrack(0, [])
        return res

The key is to reduce the same numbers on the same level, which leads to repetitious values, but we do need to include cases where same numbers on different levels. That’s why there is an i>idx in the condition. The below chart shows that we want to get rid of the @2 branch, while keeping the @3.

image-20220828234100687
Yiming Zhang
Yiming Zhang
Quantitative Researcher Associate, JP Morgan