Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Backtracking & State Search

Builds on Subsets, which you haven’t finished yet.

P1MediumBuilt on Subsets~5 min speedrun

Combinations / Combination Sum

Built on
SubsetsThe loop starts at start and the search tracks a remaining target, so it stops a branch when the remainder goes below zero.
Grows into
String Partitioning
The problem

Combinations (LC 77)(opens leetcode.com)

What a combination is (assume nothing): a combination is a group of numbers where only which numbers are in the group matters, not their order. [1, 3] and [3, 1] are the same combination.

You get two integers n and k. Using the numbers 1 to n, list every group of exactly k different numbers. Each group appears once, and the groups can be returned in any order.

Example 1
Input:n = 4, k = 2
Output:[[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
There are 6 ways to choose 2 of the 4 numbers. [2,1] is not listed separately because it is the same group as [1,2].

Combinations

target remainder ➔ candidates[start:] ➔ prune when candidate > remain
Spot it when: You build combinations that must hit a target (a sum, a length, a pattern), possibly reusing items, and the order inside a combination doesn't matter.
Combinations
Python
1from typing import Callable, List, TypeVar
2 
3Item = TypeVar("Item")
4 
5 
6def combinations(options: List[Item], is_complete: Callable[[List[Item]], bool], can_add: Callable[[List[Item], Item], bool], next_start: Callable[[int], int]) -> List[List[Item]]:
7 """Combinations: every group of options, each built once in index order.
8 is_complete(path) says when to save path. can_add(path, option) prunes an option.
9 next_start(i) is i + 1 when an option may be used once, and i when it may be reused.
10 Precondition: every branch ends, because can_add fails or is_complete holds.
11 B = the calls to backtrack. I = the loop steps over all calls. G = the groups saved. L = the longest group.
12 T = the cost of one can_add, is_complete or next_start call.
13 Time: O((B + I) * T + G * L) | Space: O(L) path + recursion stack, output not counted
14 """
15 results: List[List[Item]] = []
16 path: List[Item] = []
17 
18 def backtrack(start: int) -> None:
19 # 1. Save a Complete Group:
20 # is_complete decides when path is a finished group (a target size or sum
21 # reached); stop there, since a shorter path is not yet an answer.
22 if is_complete(path):
23 results.append(list(path))
24 return
25 
26 for i in range(start, len(options)):
27 # 2. Choose, Explore, Un-choose, Forward Only:
28 # can_add prunes an option that cannot help; next_start(i) is i + 1 when an
29 # option is used once and i when it may repeat, so each group is still built
30 # once, in index order, never both [1, 2] and [2, 1].
31 if can_add(path, options[i]):
32 path.append(options[i])
33 backtrack(next_start(i))
34 path.pop()
35 
36 backtrack(0)
37 return results
Invariant: path is in increasing order and every number still allowed is at least start, so each group is built in exactly one order.

Pick numbers only from start onward and recurse with backtrack(i + 1), so every group is built in increasing order. A group of numbers has exactly one increasing order, so the forward-only loop builds each combination once and never both [1, 2] and [2, 1]; save it when len(path) == k. The trap is the recursive call: pass i + 1, not start + 1, or the next pick can be smaller than the one just made.

See it run

Combinations, step by step

start 1 · path [] · saved 0

start 1 · path [1] · saved 0

start 2 · path [1, 2] · saved 0

start 3 · path [1, 2] · saved 1

start 2 · path [1] · saved 1

start 2 · path [1, 3] · saved 1

start 4 · path [1, 3] · saved 2

start 2 · path [1] · saved 2

start 2 · path [1, 4] · saved 2

start 5 · path [1, 4] · saved 3

start 2 · path [1] · saved 3

start 1 · path [] · saved 3

start 1 · path [2] · saved 3

start 3 · path [2, 3] · saved 3

start 4 · path [2, 3] · saved 4

start 3 · path [2] · saved 4

start 3 · path [2, 4] · saved 4

start 5 · path [2, 4] · saved 5

start 3 · path [2] · saved 5

start 1 · path [] · saved 5

start 1 · path [3] · saved 5

start 4 · path [3, 4] · saved 5

start 5 · path [3, 4] · saved 6

start 4 · path [3] · saved 6

start 1 · path [] · saved 6

start 1 · path [] · saved 6 · answer [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]

The top row holds the numbers 1 to n. The middle row is 1 when the number is in path. The bottom row is path, filled from the left.

  • i the number the loop is looking at
  • start the smallest number this call may use
  • i = start the loop number is the smallest allowed number
  • path the slot just added to path
  • undo the slot just removed from path

n = 4, k = 2, and path is empty, so need = 2. The loop in backtrack(1) stops at i = 3, because a later i leaves too few numbers.

Choose 1: path becomes [1]. Then backtrack(2) runs.

Choose 2: path becomes [1, 2]. Then backtrack(3) runs.

path = [1, 2] holds k = 2 numbers, so a copy is saved. results now holds 1 group.

Undo: path.pop() removes 2, so path is [1] again. The loop moves to the next i.

Choose 3: path becomes [1, 3]. The call is backtrack(4); backtrack(start + 1) would be backtrack(3) and could pick 3 again.

path = [1, 3] holds k = 2 numbers, so a copy is saved. results now holds 2 groups.

Undo: path.pop() removes 3, so path is [1] again. The loop moves to the next i.

Choose 4: path becomes [1, 4]. Then backtrack(5) runs.

path = [1, 4] holds k = 2 numbers, so a copy is saved. results now holds 3 groups.

Undo: path.pop() removes 4, so path is [1] again. The loop moves to the next i.

Undo: path.pop() removes 1, so path is [] again. The loop moves to the next i.

Choose 2: path becomes [2]. Then backtrack(3) runs.

Choose 3: path becomes [2, 3]. Then backtrack(4) runs.

path = [2, 3] holds k = 2 numbers, so a copy is saved. results now holds 4 groups.

Undo: path.pop() removes 3, so path is [2] again. The loop moves to the next i.

Choose 4: path becomes [2, 4]. Then backtrack(5) runs.

path = [2, 4] holds k = 2 numbers, so a copy is saved. results now holds 5 groups.

Undo: path.pop() removes 4, so path is [2] again. The loop moves to the next i.

Undo: path.pop() removes 2, so path is [] again. The loop moves to the next i.

Choose 3: path becomes [3]. Then backtrack(4) runs.

Choose 4: path becomes [3, 4]. Then backtrack(5) runs.

path = [3, 4] holds k = 2 numbers, so a copy is saved. results now holds 6 groups.

Undo: path.pop() removes 4, so path is [3] again. The loop moves to the next i.

Undo: path.pop() removes 3, so path is [] again. The loop moves to the next i.

backtrack(1) returns. results holds 6 groups: [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]].

numbers
11start
22
33
44
in path
0
0
0
0
path
1 / 26
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Recursing with backtrack(start + 1) instead of backtrack(i + 1)

    Mistake
    backtrack(start + 1)
    Fix
    backtrack(i + 1)

    What happens: The next number must come after i, the number just added. backtrack(start + 1) starts the next level after start instead. When i > start, the next pick can be i again or a smaller number. So groups such as [2, 2] appear.

    Example: n = 3, k = 2
    Mistake: returns [[1, 2], [1, 3], [2, 2], [2, 3]]Fix: returns [[1, 2], [1, 3], [2, 3]]
  2. 2

    Gotcha 2: Restarting the loop at 0 to reuse a number

    Mistake
    for i in range(0, len(candidates)):
    Fix
    for i in range(start, len(candidates)):

    What happens: Combination Sum (LC 39) lets you use a number more than once. To allow that, recurse with backtrack(i, ...) instead of backtrack(i + 1, ...). The loop must still start at start. A loop from 0 builds the same group in several orders, such as [2, 3] and [3, 2].

    Example: Combination Sum (LC 39): candidates = [2, 3], target = 5
    Mistake: returns [[2, 3], [3, 2]]Fix: returns [[2, 3]]
  3. 3

    Gotcha 3: Cutting the loop bound one lower than n - need + 2

    Mistake
    for i in range(start, n - need + 1):
    Fix
    for i in range(start, n - need + 2):

    What happens: need is the count of numbers still to pick. The last useful value of i is n - need + 1, because then exactly need numbers remain. range stops before its end, so the end must be n - need + 2. One lower loses every group that ends in n.

    Example: n = 3, k = 2
    Mistake: returns [[1, 2]]Fix: returns [[1, 2], [1, 3], [2, 3]]
  4. 4

    Gotcha 4: No sibling skip when the numbers repeat

    Mistake
    for i in range(start, len(candidates)):
        if candidates[i] > remaining:
    Fix
    for i in range(start, len(candidates)):
        if i > start and candidates[i] == candidates[i - 1]:
            continue
        if candidates[i] > remaining:

    What happens: Combination Sum II (LC 40) can hold the same number twice. Sort candidates first. Then skip candidates[i] when i > start and it equals candidates[i - 1]. Without the skip, each copy starts the same group again, and the group is saved once per copy.

    Example: Combination Sum II (LC 40): candidates = [1, 1, 2], target = 3
    Mistake: returns [[1, 2], [1, 2]]Fix: returns [[1, 2]]
Not this one if…
  • Order matters, as in Permutations (LC 46): [1, 2] and [2, 1] are two different answers. See Permutations
  • Only the number of ways is asked, as in Coin Change II (LC 518), not the combinations themselves, and there can be far too many to list. See Unbounded Knapsack

Deep Dive

What this card adds to Subsets: a target and a bound

The Subsets card already shows why a forward-only loop builds each group once. This card uses the same loop for groups that must reach a target, such as a length k or a sum.

It adds two things. First, is_complete decides when to save, so path is saved only when it is finished and the branch stops there. Second, can_add and the loop bound cut a branch that can never finish.

The orders [1, 2] and [2, 1] are one group. Only the increasing order is built, because the recursion goes to i + 1.

Why the recursion goes to i + 1, not start + 1

Take combine(4, 2). At the first call, the loop picks i = 1, then i = 2, then i = 3. After a pick of i, every later pick must be larger than i.

backtrack(i + 1) does this. At the pick i = 2, the call is backtrack(3). The next level then sees only 3 and 4.

backtrack(start + 1) is wrong. At the pick i = 2 with start = 1, it calls backtrack(2), and the next level can pick 2 again. For combine(3, 2), the result then includes [2, 2].

The result for combine(4, 2) is [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]. This is the example of Combinations (LC 77).

Why the loop ends at n - need + 1

need = k - len(path) is how many numbers are still missing. If you pick i, then n - i larger numbers are left. You need need - 1 more after i, so the pick is useful only when n - i >= need - 1. That gives i <= n - need + 1.

range stops before its end value, so the code writes range(start, n - need + 2). For combine(4, 2) at the first call, need = 2 and the loop is range(1, 4). The pick i = 4 is never tried, because no number would remain for the second position.

This bound is a saving, not a change of the answer. For combine(4, 2), backtrack runs 10 times with the bound and 11 times without it. For combine(10, 5), it runs 462 times with the bound and 638 times without it.

Variants: reuse, repeats and a sum target

The card's template changes in one place for each variant. next_start(i) is i + 1 when each number is used once. It is i when a number may be reused, as in Combination Sum (LC 39). The loop still starts at start, so [2, 3] and [3, 2] are not both built.

If the same value appears twice in the input, as in Combination Sum II (LC 40), sort first and skip candidates[i] when i > start and it equals candidates[i - 1]. This is the skip test from the Subsets card.

For a sum target, as in Combination Sum III (LC 216), can_add rejects a number larger than the remaining sum. If the candidates are sorted, the loop can break at the first rejected number, because every later number is larger.

The cost is O((B + I) * T + G * L). B is the number of backtrack calls and I is the number of loop steps. G is the number of saved groups and L is the longest group. For combine(n, k), the groups are C(n, k), and each copy costs O(k).

60-Second Pitch

"The order inside a group does not matter, so I build each group in increasing order only. The loop picks from start, and the recursion goes to i + 1. I save a copy of path when it has k numbers. Before that, I bound the loop with need = k - len(path), so a pick that leaves too few numbers is never tried. The time is proportional to the number of calls plus k times C(n, k) for the copies. The space is O(k). If a number may be reused, I recurse with i and keep the same loop start."

Common Questions

Why does a combination use i + 1 and not restart the loop at 0?

A group has one increasing order. With i + 1, the next pick is always larger, so [1, 2] is built and [2, 1] is not. A loop from 0 builds both, and the same group is saved twice.

What is the time complexity of combine(n, k)?

The saved groups number C(n, k), and each copy costs O(k), so the output takes O(k * C(n, k)). The loop bound keeps the extra calls small. The space is O(k) for path and the recursion stack.

How do you allow a number to be used many times?

Recurse with backtrack(i, ...) and not backtrack(i + 1, ...). The loop still starts at start. This is Combination Sum (LC 39). You also need a way to stop, such as a remaining sum that reaches 0 or becomes negative.

How do you handle repeated values in the input?

Sort the input. In each call, skip candidates[i] when i > start and it equals candidates[i - 1]. This is Combination Sum II (LC 40). The test uses start so that the first pick of each call is never skipped.

Why is the loop bound n - need + 2 and not n + 1?

A pick i is useful only if enough larger numbers remain to finish the group. The last useful i is n - need + 1, and range stops before its end, so the end is n - need + 2. The answer is the same with n + 1, but more calls run: 638 and not 462 for combine(10, 5).

SWE Reasoning

Spot it

  • The answer is a list of groups, and the order inside a group does not matter.
  • Every group must reach a target: a length k, a sum, or a count of items.
  • A number may be used once, or many times. The statement says which.
  • A partial group that has passed the target can be cut at once, for example when the sum is already too large.
  • The input has repeated values and the answer must not list a group twice. Sort and skip the repeat.

The invariant, step by step

  1. backtrack(start) runs. If is_complete(path) is True, such as len(path) == k, it saves list(path) and returns.
  2. The loop runs i from start. For combine, it stops at n - need + 1, because a later i leaves too few numbers.
  3. path.append(i) makes the pick. backtrack(i + 1) explores every group that continues with larger numbers.
  4. path.pop() removes the pick, so the next i starts from the same path.
  5. path is always in increasing order, and every number still allowed is at least start. A group has one increasing order, so each group is built once.

Traps

  • backtrack(start + 1) instead of backtrack(i + 1). The next pick can be a number that is already in path. combine(3, 2) returns [[1, 2], [1, 3], [2, 2], [2, 3]].
  • A loop that restarts at 0 for reuse. For reuse, change the recursion to backtrack(i, ...). The loop must still start at start. A loop from 0 builds [2, 3] and [3, 2] for target 5.
  • The loop bound one too low. range(start, n - need + 1) loses every group that ends in n. The end must be n - need + 2.
  • No skip for repeated values. In Combination Sum II (LC 40), the same number twice starts the same group twice. Sort, then skip when i > start and the value equals the left neighbour.
  • Saving path and not list(path). The saved lists change when path changes.

Practice

Canonical LeetCode:Combinations (LC 77)Combination Sum (LC 39)Combination Sum II (LC 40)Combination Sum III (LC 216)Factor Combinations (LC 254)Maximum Rows Covered by Columns (LC 2397)
Theory Context•Backtracking & State Search

Builds on Subsets, which you haven’t finished yet.

P1MediumBuilt on Subsets~5 min speedrun

Combinations / Combination Sum

Built on
SubsetsThe loop starts at start and the search tracks a remaining target, so it stops a branch when the remainder goes below zero.
Grows into
String Partitioning
The problem

Combinations (LC 77)(opens leetcode.com)

What a combination is (assume nothing): a combination is a group of numbers where only which numbers are in the group matters, not their order. [1, 3] and [3, 1] are the same combination.

You get two integers n and k. Using the numbers 1 to n, list every group of exactly k different numbers. Each group appears once, and the groups can be returned in any order.

Example 1
Input:n = 4, k = 2
Output:[[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
There are 6 ways to choose 2 of the 4 numbers. [2,1] is not listed separately because it is the same group as [1,2].

Combinations

target remainder ➔ candidates[start:] ➔ prune when candidate > remain
Spot it when: You build combinations that must hit a target (a sum, a length, a pattern), possibly reusing items, and the order inside a combination doesn't matter.
Combinations
Python
1from typing import Callable, List, TypeVar
2 
3Item = TypeVar("Item")
4 
5 
6def combinations(options: List[Item], is_complete: Callable[[List[Item]], bool], can_add: Callable[[List[Item], Item], bool], next_start: Callable[[int], int]) -> List[List[Item]]:
7 """Combinations: every group of options, each built once in index order.
8 is_complete(path) says when to save path. can_add(path, option) prunes an option.
9 next_start(i) is i + 1 when an option may be used once, and i when it may be reused.
10 Precondition: every branch ends, because can_add fails or is_complete holds.
11 B = the calls to backtrack. I = the loop steps over all calls. G = the groups saved. L = the longest group.
12 T = the cost of one can_add, is_complete or next_start call.
13 Time: O((B + I) * T + G * L) | Space: O(L) path + recursion stack, output not counted
14 """
15 results: List[List[Item]] = []
16 path: List[Item] = []
17 
18 def backtrack(start: int) -> None:
19 # 1. Save a Complete Group:
20 # is_complete decides when path is a finished group (a target size or sum
21 # reached); stop there, since a shorter path is not yet an answer.
22 if is_complete(path):
23 results.append(list(path))
24 return
25 
26 for i in range(start, len(options)):
27 # 2. Choose, Explore, Un-choose, Forward Only:
28 # can_add prunes an option that cannot help; next_start(i) is i + 1 when an
29 # option is used once and i when it may repeat, so each group is still built
30 # once, in index order, never both [1, 2] and [2, 1].
31 if can_add(path, options[i]):
32 path.append(options[i])
33 backtrack(next_start(i))
34 path.pop()
35 
36 backtrack(0)
37 return results
Invariant: path is in increasing order and every number still allowed is at least start, so each group is built in exactly one order.

Pick numbers only from start onward and recurse with backtrack(i + 1), so every group is built in increasing order. A group of numbers has exactly one increasing order, so the forward-only loop builds each combination once and never both [1, 2] and [2, 1]; save it when len(path) == k. The trap is the recursive call: pass i + 1, not start + 1, or the next pick can be smaller than the one just made.

See it run

Combinations, step by step

start 1 · path [] · saved 0

start 1 · path [1] · saved 0

start 2 · path [1, 2] · saved 0

start 3 · path [1, 2] · saved 1

start 2 · path [1] · saved 1

start 2 · path [1, 3] · saved 1

start 4 · path [1, 3] · saved 2

start 2 · path [1] · saved 2

start 2 · path [1, 4] · saved 2

start 5 · path [1, 4] · saved 3

start 2 · path [1] · saved 3

start 1 · path [] · saved 3

start 1 · path [2] · saved 3

start 3 · path [2, 3] · saved 3

start 4 · path [2, 3] · saved 4

start 3 · path [2] · saved 4

start 3 · path [2, 4] · saved 4

start 5 · path [2, 4] · saved 5

start 3 · path [2] · saved 5

start 1 · path [] · saved 5

start 1 · path [3] · saved 5

start 4 · path [3, 4] · saved 5

start 5 · path [3, 4] · saved 6

start 4 · path [3] · saved 6

start 1 · path [] · saved 6

start 1 · path [] · saved 6 · answer [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]

The top row holds the numbers 1 to n. The middle row is 1 when the number is in path. The bottom row is path, filled from the left.

  • i the number the loop is looking at
  • start the smallest number this call may use
  • i = start the loop number is the smallest allowed number
  • path the slot just added to path
  • undo the slot just removed from path

n = 4, k = 2, and path is empty, so need = 2. The loop in backtrack(1) stops at i = 3, because a later i leaves too few numbers.

Choose 1: path becomes [1]. Then backtrack(2) runs.

Choose 2: path becomes [1, 2]. Then backtrack(3) runs.

path = [1, 2] holds k = 2 numbers, so a copy is saved. results now holds 1 group.

Undo: path.pop() removes 2, so path is [1] again. The loop moves to the next i.

Choose 3: path becomes [1, 3]. The call is backtrack(4); backtrack(start + 1) would be backtrack(3) and could pick 3 again.

path = [1, 3] holds k = 2 numbers, so a copy is saved. results now holds 2 groups.

Undo: path.pop() removes 3, so path is [1] again. The loop moves to the next i.

Choose 4: path becomes [1, 4]. Then backtrack(5) runs.

path = [1, 4] holds k = 2 numbers, so a copy is saved. results now holds 3 groups.

Undo: path.pop() removes 4, so path is [1] again. The loop moves to the next i.

Undo: path.pop() removes 1, so path is [] again. The loop moves to the next i.

Choose 2: path becomes [2]. Then backtrack(3) runs.

Choose 3: path becomes [2, 3]. Then backtrack(4) runs.

path = [2, 3] holds k = 2 numbers, so a copy is saved. results now holds 4 groups.

Undo: path.pop() removes 3, so path is [2] again. The loop moves to the next i.

Choose 4: path becomes [2, 4]. Then backtrack(5) runs.

path = [2, 4] holds k = 2 numbers, so a copy is saved. results now holds 5 groups.

Undo: path.pop() removes 4, so path is [2] again. The loop moves to the next i.

Undo: path.pop() removes 2, so path is [] again. The loop moves to the next i.

Choose 3: path becomes [3]. Then backtrack(4) runs.

Choose 4: path becomes [3, 4]. Then backtrack(5) runs.

path = [3, 4] holds k = 2 numbers, so a copy is saved. results now holds 6 groups.

Undo: path.pop() removes 4, so path is [3] again. The loop moves to the next i.

Undo: path.pop() removes 3, so path is [] again. The loop moves to the next i.

backtrack(1) returns. results holds 6 groups: [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]].

numbers
11start
22
33
44
in path
0
0
0
0
path
1 / 26
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Recursing with backtrack(start + 1) instead of backtrack(i + 1)

    Mistake
    backtrack(start + 1)
    Fix
    backtrack(i + 1)

    What happens: The next number must come after i, the number just added. backtrack(start + 1) starts the next level after start instead. When i > start, the next pick can be i again or a smaller number. So groups such as [2, 2] appear.

    Example: n = 3, k = 2
    Mistake: returns [[1, 2], [1, 3], [2, 2], [2, 3]]Fix: returns [[1, 2], [1, 3], [2, 3]]
  2. 2

    Gotcha 2: Restarting the loop at 0 to reuse a number

    Mistake
    for i in range(0, len(candidates)):
    Fix
    for i in range(start, len(candidates)):

    What happens: Combination Sum (LC 39) lets you use a number more than once. To allow that, recurse with backtrack(i, ...) instead of backtrack(i + 1, ...). The loop must still start at start. A loop from 0 builds the same group in several orders, such as [2, 3] and [3, 2].

    Example: Combination Sum (LC 39): candidates = [2, 3], target = 5
    Mistake: returns [[2, 3], [3, 2]]Fix: returns [[2, 3]]
  3. 3

    Gotcha 3: Cutting the loop bound one lower than n - need + 2

    Mistake
    for i in range(start, n - need + 1):
    Fix
    for i in range(start, n - need + 2):

    What happens: need is the count of numbers still to pick. The last useful value of i is n - need + 1, because then exactly need numbers remain. range stops before its end, so the end must be n - need + 2. One lower loses every group that ends in n.

    Example: n = 3, k = 2
    Mistake: returns [[1, 2]]Fix: returns [[1, 2], [1, 3], [2, 3]]
  4. 4

    Gotcha 4: No sibling skip when the numbers repeat

    Mistake
    for i in range(start, len(candidates)):
        if candidates[i] > remaining:
    Fix
    for i in range(start, len(candidates)):
        if i > start and candidates[i] == candidates[i - 1]:
            continue
        if candidates[i] > remaining:

    What happens: Combination Sum II (LC 40) can hold the same number twice. Sort candidates first. Then skip candidates[i] when i > start and it equals candidates[i - 1]. Without the skip, each copy starts the same group again, and the group is saved once per copy.

    Example: Combination Sum II (LC 40): candidates = [1, 1, 2], target = 3
    Mistake: returns [[1, 2], [1, 2]]Fix: returns [[1, 2]]
Not this one if…
  • Order matters, as in Permutations (LC 46): [1, 2] and [2, 1] are two different answers. See Permutations
  • Only the number of ways is asked, as in Coin Change II (LC 518), not the combinations themselves, and there can be far too many to list. See Unbounded Knapsack

Deep Dive

What this card adds to Subsets: a target and a bound

The Subsets card already shows why a forward-only loop builds each group once. This card uses the same loop for groups that must reach a target, such as a length k or a sum.

It adds two things. First, is_complete decides when to save, so path is saved only when it is finished and the branch stops there. Second, can_add and the loop bound cut a branch that can never finish.

The orders [1, 2] and [2, 1] are one group. Only the increasing order is built, because the recursion goes to i + 1.

Why the recursion goes to i + 1, not start + 1

Take combine(4, 2). At the first call, the loop picks i = 1, then i = 2, then i = 3. After a pick of i, every later pick must be larger than i.

backtrack(i + 1) does this. At the pick i = 2, the call is backtrack(3). The next level then sees only 3 and 4.

backtrack(start + 1) is wrong. At the pick i = 2 with start = 1, it calls backtrack(2), and the next level can pick 2 again. For combine(3, 2), the result then includes [2, 2].

The result for combine(4, 2) is [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]. This is the example of Combinations (LC 77).

Why the loop ends at n - need + 1

need = k - len(path) is how many numbers are still missing. If you pick i, then n - i larger numbers are left. You need need - 1 more after i, so the pick is useful only when n - i >= need - 1. That gives i <= n - need + 1.

range stops before its end value, so the code writes range(start, n - need + 2). For combine(4, 2) at the first call, need = 2 and the loop is range(1, 4). The pick i = 4 is never tried, because no number would remain for the second position.

This bound is a saving, not a change of the answer. For combine(4, 2), backtrack runs 10 times with the bound and 11 times without it. For combine(10, 5), it runs 462 times with the bound and 638 times without it.

Variants: reuse, repeats and a sum target

The card's template changes in one place for each variant. next_start(i) is i + 1 when each number is used once. It is i when a number may be reused, as in Combination Sum (LC 39). The loop still starts at start, so [2, 3] and [3, 2] are not both built.

If the same value appears twice in the input, as in Combination Sum II (LC 40), sort first and skip candidates[i] when i > start and it equals candidates[i - 1]. This is the skip test from the Subsets card.

For a sum target, as in Combination Sum III (LC 216), can_add rejects a number larger than the remaining sum. If the candidates are sorted, the loop can break at the first rejected number, because every later number is larger.

The cost is O((B + I) * T + G * L). B is the number of backtrack calls and I is the number of loop steps. G is the number of saved groups and L is the longest group. For combine(n, k), the groups are C(n, k), and each copy costs O(k).

60-Second Pitch

"The order inside a group does not matter, so I build each group in increasing order only. The loop picks from start, and the recursion goes to i + 1. I save a copy of path when it has k numbers. Before that, I bound the loop with need = k - len(path), so a pick that leaves too few numbers is never tried. The time is proportional to the number of calls plus k times C(n, k) for the copies. The space is O(k). If a number may be reused, I recurse with i and keep the same loop start."

Common Questions

Why does a combination use i + 1 and not restart the loop at 0?

A group has one increasing order. With i + 1, the next pick is always larger, so [1, 2] is built and [2, 1] is not. A loop from 0 builds both, and the same group is saved twice.

What is the time complexity of combine(n, k)?

The saved groups number C(n, k), and each copy costs O(k), so the output takes O(k * C(n, k)). The loop bound keeps the extra calls small. The space is O(k) for path and the recursion stack.

How do you allow a number to be used many times?

Recurse with backtrack(i, ...) and not backtrack(i + 1, ...). The loop still starts at start. This is Combination Sum (LC 39). You also need a way to stop, such as a remaining sum that reaches 0 or becomes negative.

How do you handle repeated values in the input?

Sort the input. In each call, skip candidates[i] when i > start and it equals candidates[i - 1]. This is Combination Sum II (LC 40). The test uses start so that the first pick of each call is never skipped.

Why is the loop bound n - need + 2 and not n + 1?

A pick i is useful only if enough larger numbers remain to finish the group. The last useful i is n - need + 1, and range stops before its end, so the end is n - need + 2. The answer is the same with n + 1, but more calls run: 638 and not 462 for combine(10, 5).

SWE Reasoning

Spot it

  • The answer is a list of groups, and the order inside a group does not matter.
  • Every group must reach a target: a length k, a sum, or a count of items.
  • A number may be used once, or many times. The statement says which.
  • A partial group that has passed the target can be cut at once, for example when the sum is already too large.
  • The input has repeated values and the answer must not list a group twice. Sort and skip the repeat.

The invariant, step by step

  1. backtrack(start) runs. If is_complete(path) is True, such as len(path) == k, it saves list(path) and returns.
  2. The loop runs i from start. For combine, it stops at n - need + 1, because a later i leaves too few numbers.
  3. path.append(i) makes the pick. backtrack(i + 1) explores every group that continues with larger numbers.
  4. path.pop() removes the pick, so the next i starts from the same path.
  5. path is always in increasing order, and every number still allowed is at least start. A group has one increasing order, so each group is built once.

Traps

  • backtrack(start + 1) instead of backtrack(i + 1). The next pick can be a number that is already in path. combine(3, 2) returns [[1, 2], [1, 3], [2, 2], [2, 3]].
  • A loop that restarts at 0 for reuse. For reuse, change the recursion to backtrack(i, ...). The loop must still start at start. A loop from 0 builds [2, 3] and [3, 2] for target 5.
  • The loop bound one too low. range(start, n - need + 1) loses every group that ends in n. The end must be n - need + 2.
  • No skip for repeated values. In Combination Sum II (LC 40), the same number twice starts the same group twice. Sort, then skip when i > start and the value equals the left neighbour.
  • Saving path and not list(path). The saved lists change when path changes.

Practice

Canonical LeetCode:Combinations (LC 77)Combination Sum (LC 39)Combination Sum II (LC 40)Combination Sum III (LC 216)Factor Combinations (LC 254)Maximum Rows Covered by Columns (LC 2397)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•