Backtracking Concept and Decision Tree Visualization in DSA Typescript - Time & Space Complexity
Backtracking explores many possible choices step-by-step to find solutions. We want to understand how the time needed grows as the problem size increases.
How does the number of steps grow when we try all options in a decision tree?
Analyze the time complexity of the following backtracking code snippet.
function backtrack(choices: number[], path: number[] = []) {
if (path.length === choices.length) {
console.log(path);
return;
}
for (let i = 0; i < choices.length; i++) {
if (!path.includes(choices[i])) {
path.push(choices[i]);
backtrack(choices, path);
path.pop();
}
}
}
backtrack([1, 2, 3]);
This code tries all possible orders of the given numbers by choosing one number at a time and exploring all options recursively.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: The for-loop tries each unused choice at every step.
- How many times: At each level, it tries up to n choices, and this repeats recursively for n levels.
Each step tries many options, and for each option, it tries many more options at the next step, growing very fast.
| Input Size (n) | Approx. Operations |
|---|---|
| 3 | 6 (3 x 2 x 1) |
| 4 | 24 (4 x 3 x 2 x 1) |
| 5 | 120 (5 x 4 x 3 x 2 x 1) |
Pattern observation: The number of operations grows as the factorial of the input size, which means it grows very quickly as n increases.
Time Complexity: O(n!)
This means the time needed grows very fast, multiplying by all smaller numbers down to 1, as we try every possible order.
[X] Wrong: "Backtracking only takes linear time because it just tries each choice once."
[OK] Correct: Backtracking tries many combinations by exploring all possible orders, so it repeats work many times, causing factorial growth, not just linear.
Understanding backtracking time helps you explain why some problems take longer and shows your skill in analyzing complex recursive solutions. This skill is valuable for solving puzzles and optimization problems.
"What if we added a condition to stop exploring early when a partial path is invalid? How would that affect the time complexity?"
Practice
backtracking in algorithms?Solution
Step 1: Understand backtracking approach
Backtracking tries all possible choices step-by-step and goes back if a choice fails.Step 2: Compare options with backtracking definition
Only Try all possible options and undo choices when they lead to dead ends describes trying options and undoing wrong choices, which is backtracking.Final Answer:
Try all possible options and undo choices when they lead to dead ends -> Option AQuick Check:
Backtracking = Try and undo choices [OK]
- Confusing backtracking with sorting
- Thinking backtracking is just looping
- Believing backtracking stores fixed data only
path?Solution
Step 1: Identify expected input and output
Backtracking often builds a path string and does not return a value, so void return is common.Step 2: Match signature with backtracking pattern
function backtrack(path: string): void matches a function taking a string path and returning void, which fits backtracking.Final Answer:
function backtrack(path: string): void -> Option BQuick Check:
Backtracking function signature = string input, void output [OK]
- Using wrong return types like number or boolean
- Missing input parameter for current path
- Confusing return type with output data
function backtrack(path: string, choices: string[]): void {
if (path.length === 2) {
console.log(path);
return;
}
for (const choice of choices) {
backtrack(path + choice, choices);
}
}
backtrack('', ['A', 'B']);What will be printed?
Solution
Step 1: Understand recursion and stopping condition
The function prints when path length is 2, building strings by adding 'A' or 'B'.Step 2: Trace all paths of length 2
Starting empty, choices add 'A' or 'B' twice: 'AA', 'AB', 'BA', 'BB' are printed.Final Answer:
AA, AB, BA, BB -> Option AQuick Check:
All 2-length combos of A and B printed [OK]
- Printing only single letters
- Missing some combinations
- Confusing order of printed strings
[1, 2] but has a bug:function backtrack(start: number, path: number[], nums: number[]) {
console.log(path);
for (let i = start; i < nums.length; i++) {
path.push(nums[i]);
backtrack(i, path, nums);
path.pop();
}
}
backtrack(0, [], [1, 2]);What is the bug?
Solution
Step 1: Analyze recursive call parameter
The call uses backtrack(i, ...) which reuses the same index, causing repeated subsets.Step 2: Fix recursion to avoid duplicates
Changing to backtrack(i + 1, ...) moves forward, preventing duplicates and infinite loops.Final Answer:
Recursive call should use i + 1 instead of i to avoid duplicates -> Option DQuick Check:
Advance index in recursion = i + 1 [OK]
- Not advancing index causing infinite recursion
- Removing path.pop() causing wrong subsets
- Misplacing console.log outside recursion
['X', 'Y', 'Z'] but without repeating the same letter consecutively. Which change to this code snippet enforces that?function backtrack(path: string, letters: string[]) {
if (path.length === 3) {
console.log(path);
return;
}
for (const letter of letters) {
// What condition to add here?
backtrack(path + letter, letters);
}
}
backtrack('', ['X', 'Y', 'Z']);Solution
Step 1: Understand the no consecutive repeat rule
We must skip adding a letter if it matches the last letter in path.Step 2: Write condition to check last letter
Condition if path is empty or last letter differs from current letter ensures no repeats.Final Answer:
Add: if (path.length === 0 || path[path.length - 1] !== letter) before recursive call -> Option CQuick Check:
Check last letter before adding new letter [OK]
- Filtering letters incorrectly by fixed letter
- Checking only path length without letter comparison
- Using includes() which disallows all repeats
Ready to prove it?
Pass the test and this pattern moves unknown
