LeetCode ยท #40

Combination Sum II Solution

Find all unique combinations where each number may be used at most once and the sum equals target.

01
Section One ยท Problem

Problem Statement

๐Ÿท๏ธ

Difficulty

Medium

๐Ÿ”—

LeetCode

Problem #40

๐Ÿ—๏ธ

Pattern

Backtracking โ€” sort + no reuse + dedup

Compared with LC 39, there are two key differences: the input may contain duplicates, and each element can be used only once. That changes the recursive step from i to i + 1 and requires duplicate skipping at the same recursion level.

Example
Input: candidates = [10,1,2,7,6,1,5], target = 8 Output: [[1,1,6],[1,2,5],[1,7],[2,6]]
Constraints
โ€ข 1 โ‰ค candidates.length โ‰ค 100 โ€ข 1 โ‰ค candidates[i] โ‰ค 50 โ€ข 1 โ‰ค target โ‰ค 30
02
Section Two ยท Approach 1

Generate Candidates + Deduplicate

Try include/exclude recursion across indices, collect every combination that reaches the target, then store normalized combinations inside a set so duplicates collapse.

Problem: This still generates duplicate branches first, especially when repeated values appear in the input. We can prevent duplicates during recursion by sorting and skipping equal values at the same level.
Java โ€” Use a set to deduplicate
import java.util.*; class Solution { public List<List<Integer>> combinationSum2(int[] candidates, int target) { Arrays.sort(candidates); Set<List<Integer>> set = new LinkedHashSet<>(); dfs(candidates, 0, target, new ArrayList<>(), set); return new ArrayList<>(set); } private void dfs(int[] c, int i, int remain, List<Integer> path, Set<List<Integer>> set) { if (remain == 0) { set.add(new ArrayList<>(path)); return; } if (i == c.length || remain < 0) return; path.add(c[i]); dfs(c, i + 1, remain - c[i], path, set); path.remove(path.size() - 1); dfs(c, i + 1, remain, path, set); } }
Python โ€” Use a set to deduplicate
class Solution: def combinationSum2(self, candidates: list[int], target: int) -> list[list[int]]: candidates.sort() seen = set() def dfs(i: int, remain: int, path: list[int]) -> None: if remain == 0: seen.add(tuple(path)) return if i == len(candidates) or remain < 0: return path.append(candidates[i]) dfs(i + 1, remain - candidates[i], path) path.pop() dfs(i + 1, remain, path) dfs(0, target, []) return [list(t) for t in seen]
MetricValue
TimeExponential
SpaceExponential output + set
03
Section Three ยท Approach 2

Sorted Backtracking with No Reuse

Sort first. Loop from start. Skip duplicates using i > start && c[i] == c[i-1]. When choosing a number, recurse with i + 1 so the same element index cannot be reused.

๐Ÿ’ก Mental model: You are picking coupons from a sorted pile. At one decision level, if two coupons have the same value, starting with the second copy would recreate the same combinations as starting with the first copy. So you skip the later duplicate at that same level. But after taking the first one, taking the second deeper in recursion is fine.
Algorithm
  • Sort the candidates.
  • If remain == 0, record the path.
  • If candidates[i] > remain, break โ€” sorted order lets us prune early.
  • If i > start && candidates[i] == candidates[i-1], continue โ€” duplicate at same level.
  • Recurse with i + 1 because each index is used at most once.
๐ŸŽฏ LC 39 vs LC 40: LC 39 reuses the current candidate so it recurses with i. LC 40 forbids reuse, so it recurses with i + 1. That single parameter change is the heart of the problem.
04
Section Four ยท Trace

Visual Walkthrough

Trace for sorted [1,1,2,5,6,7,10], target = 8:

Backtracking with same-level duplicate skipping
Pick first 1 at root. Later pick second 1 deeper โ†’ [1,1,6] is valid. Skip second 1 at root level, or we'd recreate the same families. Valid outputs found: [1,1,6], [1,2,5], [1,7], [2,6] When candidate > remain, break immediately because later values are even larger. Answer set [1,1,6], [1,2,5], [1,7], [2,6]
05
Section Five ยท Implementation

Code โ€” Java & Python

Java โ€” Sorted backtracking
import java.util.*; class Solution { public List<List<Integer>> combinationSum2(int[] candidates, int target) { Arrays.sort(candidates); List<List<Integer>> res = new ArrayList<>(); backtrack(candidates, target, 0, new ArrayList<>(), res); return res; } private void backtrack(int[] c, int remain, int start, List<Integer> path, List<List<Integer>> res) { if (remain == 0) { res.add(new ArrayList<>(path)); return; } for (int i = start; i < c.length; i++) { if (i > start && c[i] == c[i - 1]) continue; if (c[i] > remain) break; path.add(c[i]); backtrack(c, remain - c[i], i + 1, path, res); path.remove(path.size() - 1); } } }
Python โ€” Sorted backtracking
class Solution: def combinationSum2(self, candidates: list[int], target: int) -> list[list[int]]: candidates.sort() res = [] def backtrack(start: int, remain: int, path: list[int]) -> None: if remain == 0: res.append(path[:]) return for i in range(start, len(candidates)): if i > start and candidates[i] == candidates[i - 1]: continue if candidates[i] > remain: break path.append(candidates[i]) backtrack(i + 1, remain - candidates[i], path) path.pop() backtrack(0, target, []) return res
06
Section Six ยท Analysis

Complexity Analysis

ApproachTimeSpaceTrade-off
Generate + setExponentialHighCreates duplicates first
Sorted backtracking โ† optimalExponential worst-caseO(target) recursion + outputPrunes and avoids duplicate branches
07
Section Seven ยท Edge Cases

Edge Cases & Pitfalls

CaseInputWhy It Matters
Duplicate values[1,1,1,2], target=3Should not duplicate [1,2].
No solution[4,5], target=3Return empty list.
Single exact match[8], target=8Return [[8]].
No reuse allowed[2], target=4Cannot use the same index twice.
โš  Common Mistake: Reusing the LC 39 recursion and calling with i instead of i + 1. That accidentally allows using the same element multiple times and produces invalid answers.

โ† Back to Backtracking problems