Post

Backtracking - Combination Sum II

All diagrams presented herein are original creations, meticulously designed to enhance comprehension and recall. Crafting these aids required considerable effort, and I kindly request attribution if this content is reused elsewhere.

Difficulty : Easy

DFS, Backtracking

Problem

Given a collection of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sum to target.

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

Note: The solution set must not contain duplicate combinations.

Example 1:

1
2
3
4
5
6
7
8
Input: candidates = [10,1,2,7,6,1,5], target = 8
Output: 
[
  [1,1,6],
  [1,2,5],
  [1,7],
  [2,6]
]

Example 2:

1
2
3
4
5
6
Input: candidates = [2,5,2,1,2], target = 5
Output: 
[
  [1,2,2],
  [5]
]

Solution

Here we need to make sure that [1, 7] and [7, 1] both are not selected using Example 1. In order to achieve this, first sort the array and skip over to the next different element than current one. This way 1 will not be selected after choosing 7 as duplicates are not allowed.

It is a very similar problem as the Combination Sum. Here also we will implement using both the templates we have already discussed.

image-20240514221758079

Basic Structure

Before implementing each solutions, let’s build the basic structure of the code, which is not going to change. We would need one variables for the output to return. The dfs() function takes 3 parameters :

  • The index location for traversing
  • The path to return to.
  • The curr_sum whenever need to determine base case.

We will sort the candidates just to make sure we can skip the duplicates. (More on this below)

1
2
3
4
5
output = []

candidates.sort() 

def dfs(index, path, curr_sum):

Lets also define the terminating condition when the path can be added to the output.

1
2
3
4
def dfs(index, path, curr_sum):
  if curr_sum == target:
    output.append(path.copy())
    return

We will skip the internal logic and finally call the dfs() and return the output.

1
2
3
4
5
6
7
8
9
10
11
output = []

def dfs(index, path, curr_sum):
  if curr_sum == target:
    output.append(path.copy())
    return
  
  # Traversal logic goes here
  
def(0,[],0)
return output

Implement using Template 1

When using template 1, we can’t keep incrementing the index so we need to check for boundary condition. We shouldn’t traverse further if any one of the following is True.

1
2
if index == len(candidates) or curr_sum > target:
  return

Now it’s time to follow the building blocks of template 1. Here is the dfs(i) call where we first update path and add pass the same index to dfs(). Once done we will pop() back the latest so that we now have the original path to try the next index.

1
2
3
path.append(candidates[index])
dfs(index,path,curr_sum+candidates[index])
path.pop()

The only change in this solution from the previous one is this one and it’s very important to understand the difference.

What we don’t want is the duplication of [1,7]. Once we initiate the dfs() from the 1st 1 it will always try to find a path to target using the 2nd 1. However initiating another dfs() from the 2nd 1 will create duplicate entry (red arrow).

image-20240515000552542

Hence we will skip all the duplicate entries as they are already part of the recursive call. We need one [1,7], it does not matter the 1 comes from index 0 or index 1. As see below, we will skip over the 2nd 1 and call dfs() using 2.

image-20240515001442813

1
2
while index+1 < len(candidates) and candidates[index]==candidates[index+1]:
  index+=1

Increase the index and call the dfs() using existing path and curr_sum

1
dfs(index+1,path,curr_sum)

Thats all needed for implementing using template 1.

Implement using Template 2

I feel this implementation is very easy to implement using the Template 2. We need to traverser using a loop till end of the candidates array starting from current index and call dfs() only if the curr_sum+candidates[j]<=target.

Append candidates[j] to current path, call the dfs() and then backtrack path. The only extra code here is to keep track of the previous value and skip over if candidates[i] == prev.

1
2
3
4
5
6
7
prev = -1
for j in range(index,len(candidates)):
  if curr_sum+candidates[j]<=target and candidates[i] != prev:
    path.append(candidates[j])
    dfs(j,path,curr_sum+candidates[j])
    path.pop()
  prev= candidates[i]

Final Code

Here is the full code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def combinationSum(candidates, target):

    output = []

    def dfs(index, path, curr_sum):
        if curr_sum == target:
            output.append(path.copy())
            return

        '''
        # Using Template 2
				prev = -1
        for j in range(index,len(candidates)):
            if curr_sum+candidates[j]<=target and candidates[i] != prev:
                path.append(candidates[j])
                dfs(j,path,curr_sum+candidates[j])
                path.pop()
            prev= candidates[i]
        '''
				
        # Using Template 1
        if index == len(candidates) or curr_sum > target:
            return

        path.append(candidates[index])
        dfs(index, path, curr_sum+candidates[index])
        path.pop()
					
       	while index+1 < len(candidates) and candidates[index]==candidates[index+1]:
            index+=1
        
        dfs(index+1, path, curr_sum)

    dfs(0, [], 0)

    return output
This post is licensed under CC BY 4.0 by the author.