Recognizing Backtracking and DP Signals
1. Recognizing Generate All Solutions Signal
| Signal | Description | Common Patterns |
|---|---|---|
| "all possible" | Problem asks for all valid combinations or arrangements | Permutations, combinations, subsets |
| "find all" | Explicitly asks for enumeration of solutions | Sudoku solvers, N-Queens, pathfinding |
| "generate" | Task involves generating every valid configuration | Valid parentheses, word break, expression parsing |
| "enumerate" | Requires listing all solutions without constraints | Power set, binary trees, graph traversals |
Example: Generate All Valid Parentheses
function generateParenthesis(n) {
const result = [];
function backtrack(current, open, close) {
if (current.length === 2 * n) {
result.push(current);
return;
}
if (open < n) {
backtrack(current + "(", open + 1, close);
}
if (close < open) {
backtrack(current + ")", open, close + 1);
}
}
backtrack("", 0, 0);
return result;
}
// Example usage:
console.log(generateParenthesis(2));
// Output: ["(())", "()()"]
Note: When a problem asks for "all valid solutions" or "generate all combinations", it's often a backtracking problem. These problems typically involve constraints that must be satisfied for each solution.
2. Recognizing Count Solutions or Optimize Signal
| Signal | Description | Common Patterns | Examples |
|---|---|---|---|
| Count Solutions | Problem asks for total number of valid solutions | Combinatorial problems, path counting, subset generation | How many ways to reach the destination? |
| Optimize Signal | Problem seeks optimal solution (min/max) | Shortest path, maximum profit, minimum cost | Find the shortest path between two nodes |
Example: Counting Valid Parentheses
function countValidParentheses(n) {
if (n === 0) return 1;
let dp = new Array(n + 1).fill(0);
dp[0] = 1;
for (let i = 1; i <= n; i++) {
for (let j = 0; j < i; j++) {
dp[i] += dp[j] * dp[i - 1 - j];
}
}
return dp[n];
}
Example: Minimum Path Sum
function minPathSum(grid) {
const m = grid.length, n = grid[0].length;
const dp = Array(m).fill().map(() => Array(n).fill(0));
dp[0][0] = grid[0][0];
for (let i = 1; i < m; i++) dp[i][0] = dp[i-1][0] + grid[i][0];
for (let j = 1; j < n; j++) dp[0][j] = dp[0][j-1] + grid[0][j];
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
return dp[m-1][n-1];
}
Note: Counting problems often use dynamic programming with recurrence relations, while optimization problems typically involve minimizing or maximizing a value.
3. Recognizing Optimal Substructure Signal
| Signal | Description | Implication |
|---|---|---|
| Overlapping Subproblems | Same subproblem solved multiple times in recursion | DP or memoization can optimize |
| Optimal Substructure | Optimal solution contains optimal solutions to subproblems | DP or recursion with memoization can be applied |
| Recursive Structure | Problem can be broken into smaller recursive subproblems | May lead to exponential time complexity without optimization |
Example: Fibonacci Sequence
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
Note: The Fibonacci sequence exhibits optimal substructure because the nth Fibonacci number is the sum of the (n-1)th and (n-2)th numbers, which are optimal solutions to their respective subproblems.
4. Recognizing Overlapping Subproblems Signal
| Signal | Description | Example Problem | Indication |
|---|---|---|---|
| Optimal Substructure | Optimal solution contains optimal solutions to subproblems | Shortest path in a graph | Can break down into smaller subproblems with optimal solutions |
| Overlapping Subproblems | Same subproblems solved multiple times in recursive solution | Fibonacci sequence | Recursive calls to same inputs yield same results |
| Memoization Potential | Results of subproblems can be stored and reused | Dynamic Programming problems like Coin Change | Recomputing same subproblems indicates need for caching |
Example: Fibonacci Sequence Analysis
function fib(n, memo = {}) {
if (n in memo) return memo[n];
if (n <= 1) return n;
memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
return memo[n];
}
Note: The recursive Fibonacci has overlapping subproblems because
fib(3) is computed multiple times. This pattern signals potential for memoization or DP.
Key Indicators of Overlapping Subproblems
- Same subproblem solved repeatedly in recursion
- Recursive tree with repeated nodes
- Optimal substructure property holds
5. Recognizing Greedy Choice Property Signal
| Signal | Description | Examples |
|---|---|---|
| Local Optimization | Choosing the best local option at each step without considering future consequences | Activity Selection Problem, Huffman Coding |
| No Afterthoughts | Once a choice is made, it's never reconsidered or reverted | Kruskal's MST, Prim's MST |
| Optimal Substructure | Optimal solution contains optimal solutions to subproblems | Shortest Path (Dijkstra), Minimum Spanning Tree |
Example: Activity Selection
const activities = [
{ start: 1, end: 4 },
{ start: 3, end: 5 },
{ start: 0, end: 6 },
{ start: 5, end: 7 },
{ start: 3, end: 9 },
{ start: 5, end: 9 },
{ start: 6, end: 10 },
{ start: 8, end: 11 },
{ start: 8, end: 12 },
{ start: 2, end: 14 },
{ start: 12, end: 16 }
];
function selectActivities(activities) {
const selected = [activities[0]];
let lastEnd = activities[0].end;
for (let i = 1; i < activities.length; i++) {
const current = activities[i];
if (current.start >= lastEnd) {
selected.push(current);
lastEnd = current.end;
}
}
return selected;
}
console.log(selectActivities(activities));
Note: Greedy algorithms work when the problem has
optimal substructure and greedy choice property. Not all problems can be solved greedily.
Key Indicators of Greedy Approach
- Local optimization at each step
- No need to backtrack or reconsider choices
- Optimal substructure exists
- Global optimum can be achieved by making locally optimal choices
6. Recognizing Subset Sum/Partition Signal
| Signal | Description | Typical Use Case |
|---|---|---|
| Sum of elements equals target | Given a set of numbers, determine if any subset sums to a specific value. | Subset sum problem, partition problem |
| Equal partitioning required | Partition a set into two subsets with equal sums. | Balance load distribution, resource allocation |
| Multiple subset constraints | Find if elements can be grouped into subsets with specific sum criteria. | Task scheduling, bin packing |
Example: Subset Sum Problem
function canPartition(nums) {
const total = nums.reduce((a, b) => a + b, 0);
if (total % 2 !== 0) return false;
const target = total / 2;
const dp = new Array(target + 1).fill(false);
dp[0] = true;
for (let num of nums) {
for (let j = target; j >= num; j--) {
dp[j] = dp[j] || dp[j - num];
}
}
return dp[target];
}
console.log(canPartition([1, 5, 11, 5])); // true
Note: The subset sum problem is a classic dynamic programming problem where we check if a subset of the array sums up to half of the total sum.
Key Indicators for Subset Sum/Partition
- Array of integers with a target sum
- Need to find if any subset sums to a specific value
- Partitioning into equal subsets
- Optimization or decision problem involving sums
7. Recognizing 0/1 Knapsack Signal
| Signal | Description | Example |
|---|---|---|
| 0/1 Knapsack | Each item can be taken at most once | Maximize value with weight constraint |
| Unbounded Knapsack | Each item can be taken multiple times | Maximize value with weight constraint (repeated items) |
| Subset Sum | Check if a subset sums to target | Given array, check if any subset sums to K |
| Partition Problem | Split array into two equal subsets | Check if array can be partitioned into two equal sums |
| Coin Change | Minimize coins to make amount | Find minimum coins to sum to target amount |
Example: 0/1 Knapsack
const knapsack = (weights, values, capacity) => {
const dp = Array(capacity + 1).fill(0);
for (let i = 0; i < weights.length; i++) {
for (let w = capacity; w >= weights[i]; w--) {
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}
return dp[capacity];
};
// Example usage:
const weights = [2, 1, 3, 2];
const values = [12, 10, 20, 15];
const capacity = 5;
console.log(knapsack(weights, values, capacity)); // Output: 37
Note: The 0/1 Knapsack pattern involves selecting items with weights and values to maximize value under a weight constraint, where each item can only be used once.
8. Recognizing Matrix Chain Multiplication Signal
| Signal | Description | Indicates |
|---|---|---|
| Sequence of matrices | Input consists of a sequence of matrices with dimensions | Dynamic Programming or Backtracking approach |
| Matrix dimensions | Each matrix has specific width and height (e.g., 2x3, 4x5) | Need to compute optimal multiplication order |
| Optimization goal | Minimize total scalar multiplications | Matrix Chain Multiplication (MCM) problem |
| Parenthesization | Order of multiplication matters due to matrix dimensions | Need to find optimal parenthesization |
Example: Matrix Chain Multiplication Signal
const matrices = [
{ rows: 10, cols: 20 },
{ rows: 20, cols: 30 },
{ rows: 30, cols: 40 },
{ rows: 40, cols: 50 }
];
// Signal: sequence of matrices with dimensions
// Indication: MCM problem - find optimal parenthesization
Note: Matrix Chain Multiplication is a classic DP problem where the goal is to minimize scalar multiplications by choosing optimal parenthesization order.