2.2 Algorithm Selection: Greedy vs Dynamic Programming vs Backtracking

Choosing between Greedy, Dynamic Programming (DP), and Backtracking is one of the most critical decisions in problem-solving. Each approach has distinct characteristics, use cases, and trade-offs. Understanding when to use each is essential for efficient algorithm design.

2.2.1 Core Differences

Aspect Greedy Dynamic Programming Backtracking
Goal Find one optimal solution via local choices Find optimal value (min/max/count) Find all solutions or one valid solution
Question Type “What is the optimal strategy?” “What is the minimum/maximum?” “What are all possible combinations?”
Example Question “Capture every price increase?” “Minimum coins to make amount?” “All ways to make amount?”
Decision Making Local optimal choice → Global optimum Optimal substructure with overlapping subproblems Exhaustive search with pruning
Reconsiders Choices Never - choices are irrevocable Implicitly via memoization/tabulation Always - tries all possibilities
Overlapping Subproblems Not applicable (no subproblems) Required - reuses computed results May exist but not exploited
State Storage None (stateless decisions) Stores results of subproblems Stores current path/state
Time Complexity Linear to \(O(n \log n)\) Polynomial (typically \(O(n \times m)\)) Exponential (typically \(O(b^d)\) or \(O(n!)\))
Space Complexity \(O(1)\) to \(O(n)\) \(O(n)\) to \(O(n \times m)\) \(O(d)\) recursion stack
Optimization Technique Greedy choice property Optimal substructure + memoization Pruning invalid paths
Returns One optimal solution Single value (number, boolean) List of all solutions or single path
Proof Required Yes - must prove greedy works No (correctness follows from recurrence) No (exhaustive search guarantees correctness)

2.2.2 Key Insight: Local vs Global vs Exhaustive

The fundamental differences:

  • Greedy: “Make the best local choice now” (assumes local optimum → global optimum)
  • DP: “What is the best global value?” (builds optimal solution from subproblems)
  • Backtracking: “What are all possible ways?” (explores entire solution space)

2.2.3 When to Use Greedy

Use Greedy when the problem has the greedy choice property: making locally optimal choices leads to a globally optimal solution.

Critical Requirement: You must prove that the greedy strategy works (via exchange argument, contradiction, or induction). If greedy fails, use DP instead.

Greedy Question Patterns: - “Maximize/minimize by making optimal choices at each step” - Problems where choices don’t conflict with each other - No dependencies between choices (future choices don’t depend on past ones)

Examples:

Problem Greedy Strategy Why Greedy Works?
Stock II Capture every price increase No conflicts - all increases are independently profitable
Activity Selection Pick activity with earliest finish time Leaves maximum room for future activities
Huffman Coding Merge two smallest frequency nodes Proven optimal by exchange argument
Fractional Knapsack Take items by value/weight ratio Can take fractions - greedy maximizes value

When Greedy FAILS: - Stock Cooldown: Selling today forces rest tomorrow (choices conflict) → Use DP - 0/1 Knapsack: Can’t take fractions, greedy by ratio fails → Use DP - Coin Change (minimum coins): Greedy by largest coin first fails for [1,3,4], target 6 → Use DP

How to Verify Greedy Works: 1. Try counterexamples - If you find one where greedy fails, use DP 2. Prove via exchange argument - Show swapping greedy choice with another makes solution worse 3. Check for conflicts - Do current choices block better future choices?

2.2.4 When to Use Dynamic Programming

Use DP when the problem asks for an optimal value or count, and exhibits:

  1. Optimal Substructure: Solution to the problem can be built from solutions to subproblems
  2. Overlapping Subproblems: Same subproblem is solved multiple times

DP Question Patterns: - “Find the minimum/maximum …” - “Count the number of ways …” - “Is it possible to reach …?” (when combined with optimization) - “What is the longest/shortest …?”

Examples:

Problem Question Why DP?
Coin Change Minimum coins to make amount? Asks for minimum count
Climbing Stairs How many ways to reach top? Asks for total count of ways
Longest Increasing Subsequence Length of longest subsequence? Asks for maximum length
0/1 Knapsack Maximum value with weight limit? Asks for maximum value
House Robber Maximum money without alerting? Asks for maximum sum
Minimum Plans (HackerRank) Minimum plans to reach bandwidth? Asks for minimum count

Common DP Patterns: - Unbounded Knapsack: Unlimited use of items (Coin Change, Minimum Plans) - 0/1 Knapsack: Each item used at most once - Longest/Shortest Path: In DAGs or grids - Counting Ways: Climbing Stairs, Unique Paths

2.2.5 When to Use Backtracking

Use Backtracking when the problem asks for all solutions or one valid path, requiring exhaustive search:

Backtracking Question Patterns: - “Find all combinations that …” - “Generate all permutations …” - “Return all valid arrangements …” - “List all subsets …” - “Find any path that satisfies …”

Examples:

Problem Question Why Backtracking?
Combination Sum All combinations summing to target? Asks for all solutions
Permutations All permutations of array? Asks for all arrangements
Subsets All possible subsets? Asks for all subsets
N-Queens All valid queen placements? Asks for all valid boards
Word Search Does path exist in grid? Asks for path existence
Index Combinations (HackerRank) All index combinations for target? Asks for all combinations

Common Backtracking Patterns: - Choose-Explore-Unchoose: Combinations, subsets - Permutations: Track used elements - Grid/Graph Traversal: Mark-unmark visited cells

2.2.6 The Critical Distinction: Same Problem, Different Questions

How you frame the question determines which algorithm to use. Consider these examples:

2.2.6.1 Example 1: Stock Trading

Question Approach Answer Why?
“Maximum profit with unlimited transactions?” Greedy Sum all price increases No conflicts - capture every gain
“Maximum profit with cooldown?” DP Track states (hold/sold/rest) Choices conflict - selling blocks buying
“All transaction sequences for profit \(\ge\) X?” Backtracking List all valid sequences Need all solutions

2.2.6.2 Example 2: Coin Change

Consider coins = [1, 2, 5] and target = 5:

Question Approach Answer Why?
“Minimum coins to make 5?” DP 2 (use coin 5, or coins 2+3) Asks for optimal count
“Can greedy (largest first) work?” No Fails for [1,3,4], target 6 Greedy gives [4,1,1]=3, optimal is [3,3]=2
“All ways to make 5?” Backtracking [[1,1,1,1,1], [1,1,1,2], [1,2,2], [5]] Must enumerate all solutions
“Number of ways to make 5?” DP 4 Count without listing

2.2.6.3 Example 3: Activity Selection

Question Approach Answer Why?
“Maximum non-overlapping activities?” Greedy Pick earliest finish time Greedy choice property proven
“All valid schedules?” Backtracking List all combinations Need all solutions

Key Observations: - Greedy: Works when local choices don’t conflict → Fast (\(O(n \log n)\)) - DP: Works when choices conflict but exhibit optimal substructure → Moderate (\(O(n^2)\)) - Backtracking: Necessary when you need all solutions → Slow (\(O(2^n)\))

2.2.7 Hybrid Cases: DP with Path Reconstruction

Sometimes you need both the optimal value AND the actual solution:

Example: “Find the minimum coins AND which coins to use”

Approach: DP + Path Reconstruction 1. Use DP to find the optimal value 2. Backtrack through the DP table to reconstruct the solution

// Step 1: DP to find minimum
int[] dp = new int[amount + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;

for (int i = 1; i <= amount; i++) {
    for (int coin : coins) {
        if (coin <= i && dp[i - coin] != Integer.MAX_VALUE) {
            dp[i] = Math.min(dp[i], dp[i - coin] + 1);
        }
    }
}

// Step 2: Reconstruct the solution
List<Integer> result = new ArrayList<>();
int current = amount;
while (current > 0) {
    for (int coin : coins) {
        if (current >= coin && dp[current - coin] == dp[current] - 1) {
            result.add(coin);
            current -= coin;
            break;
        }
    }
}

This is NOT pure backtracking because we don’t enumerate all solutions—we reconstruct one optimal solution from the DP table.

2.2.8 Overlapping Subproblems: The Deciding Factor

Without Memoization, backtracking can be extremely inefficient for optimization problems:

Example: Minimum coins for amount 11 with coins [1, 5]

Backtracking (without memo):
minCoins(11) calls:
  minCoins(10) + 1  [using coin 1]
    minCoins(9) + 1
      minCoins(8) + 1
        ... (exponential explosion)
  minCoins(6) + 1   [using coin 5]
    minCoins(5) + 1
      minCoins(4) + 1  ← Recalculates minCoins(4) again!
      ...

With DP:
dp[0] = 0
dp[1] = 1
dp[2] = 2
...
dp[4] = 4  ← Calculated once, reused
...
dp[11] = 3

When Overlapping Subproblems Exist: - For optimization → Use DP (polynomial time) - For enumeration → Backtracking is necessary (exponential but unavoidable)

2.2.9 Decision Tree

                    ┌────────────────────────────────┐
                    │ Start: Understand the problem  │
                    └──────────────┬─────────────────┘
                                   │
                    ┌──────────────┴─────────────┐
                    │ Does it ask for ALL        │
                    │ solutions/combinations?    │
                    └──────┬──────────────┬──────┘
                           │              │
                          YES            NO
                           │              │
                           ▼              │
                   ┌──────────────┐       │
                   │ Backtracking │       │
                   │              │       │
                   │ O(b^d), O(n!)│       │
                   └──────────────┘       │
                                          │
                           ┌──────────────┴────────────┐
                           │ Does it ask for optimal   │
                           │ value (min/max/count)?    │
                           └──────┬──────────────┬─────┘
                                  │              │
                                 YES            NO
                                  │              │
                   ┌──────────────┴──────┐       │
                   │ Can you make local  │       │
                   │ optimal choices     │       │
                   │ without conflicts?  │       │
                   └──────┬──────────┬───┘       │
                          │          │           │
                         YES        NO           │
                          │          │           │
                          ▼          ▼           ▼
                  ┌───────────┐ ┌──────────┐ ┌──────────┐
                  │  Greedy   │ │    DP    │ │  Other   │
                  │           │ │          │ │(Two Ptr, │
                  │ O(n logn) │ │  O(n×m)  │ │ MinMax)  │
                  └───────────┘ └──────────┘ └──────────┘
                       ↑              ↑
                       │              │
                  Must PROVE     Has optimal
                  it works!      substructure +
                                overlapping
                                subproblems

Decision Steps:

  1. Need ALL solutions?Backtracking

    • Example: All permutations, all subsets, all combinations
  2. Need ONE optimal value? → Check greedy first, then DP

    1. Try Greedy:

      • Can you make locally optimal choices?
      • Do choices conflict with each other?
      • Can you PROVE greedy works?

      If YES → Use Greedy If NO → Use DP

    2. Use DP if:

      • Choices have dependencies
      • Overlapping subproblems exist
      • Optimal substructure present
  3. Neither optimization nor enumeration? → Other techniques

    • Two Pointers, MinMax, Sliding Window, etc.

2.2.10 Common Mistakes

1. Using Greedy When It Doesn’t Work

// ❌ WRONG: Greedy for 0/1 Knapsack
// Take items by value/weight ratio - FAILS!
Arrays.sort(items, (a,b) -> (b.value/b.weight) - (a.value/a.weight));
int totalValue = 0;
for (Item item : items) {
    if (item.weight <= capacity) {
        totalValue += item.value;
        capacity -= item.weight;
    }
}
// Counterexample: items=[(60,10), (100,20), (120,30)], capacity=50
// Greedy: takes (60,10) + (100,20) = 160, leaves 20 capacity wasted
// Optimal: takes (100,20) + (120,30) = 220
// ✅ CORRECT: Use DP for 0/1 Knapsack
int[][] dp = new int[n+1][capacity+1];
for (int i = 1; i <= n; i++) {
    for (int w = 1; w <= capacity; w++) {
        if (items[i-1].weight <= w) {
            dp[i][w] = Math.max(dp[i-1][w],
                               items[i-1].value + dp[i-1][w - items[i-1].weight]);
        } else {
            dp[i][w] = dp[i-1][w];
        }
    }
}
// Time: O(n × capacity) - Guaranteed optimal

2. Using Backtracking for Optimization Problems

// ❌ WRONG: Using backtracking to find minimum coins
int minCoins = Integer.MAX_VALUE;

void backtrack(int[] coins, int remaining, int count) {
    if (remaining == 0) {
        minCoins = Math.min(minCoins, count);
        return;
    }
    for (int coin : coins) {
        if (coin <= remaining) {
            backtrack(coins, remaining - coin, count + 1);
        }
    }
}
// Time: O(n^m) - Exponential, recalculates subproblems
// ✅ CORRECT: Using DP for optimization
int[] dp = new int[amount + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;

for (int i = 1; i <= amount; i++) {
    for (int coin : coins) {
        if (coin <= i && dp[i - coin] != Integer.MAX_VALUE) {
            dp[i] = Math.min(dp[i], dp[i - coin] + 1);
        }
    }
}
// Time: O(n × m) - Polynomial, solves each subproblem once

2. Using DP for Enumeration Problems

// ❌ WRONG: DP can't enumerate all combinations
int[] dp = new int[target + 1];
dp[0] = 1;
// This counts the number of ways, but doesn't list them!
// ✅ CORRECT: Backtracking enumerates all solutions
void backtrack(int[] nums, List<Integer> current, int target, List<List<Integer>> result) {
    if (target == 0) {
        result.add(new ArrayList<>(current));  // Store each combination
        return;
    }
    // ... explore all paths
}

2.2.11 Summary Table: Which Algorithm for Which Problem?

Problem Type Example Algorithm Complexity
Optimal with Independent Choices Stock II (unlimited trades) Greedy \(O(n)\) to \(O(n \log n)\)
Optimal with Conflicting Choices Stock Cooldown DP \(O(n)\) to \(O(n \times m)\)
Minimum/Maximum Value Minimum coins for amount DP \(O(n \times m)\)
Count Ways (no enumeration) Number of paths in grid DP \(O(n \times m)\)
All Combinations All subsets of array Backtracking \(O(2^n)\)
All Permutations All orderings of array Backtracking \(O(n!)\)
Path Exists (simple) Can reach target sum? DP (boolean) \(O(n \times m)\)
All Paths All paths to target sum Backtracking \(O(b^d)\)
Longest/Shortest Sequence Longest increasing subsequence DP \(O(n^2)\) or \(O(n \log n)\)
Generate All Sequences All valid parentheses Backtracking \(O(4^n / \sqrt{n})\)
Tracking Min/Max Best Time to Buy Stock I MinMax \(O(n)\)

2.2.12 Key Takeaway

Ask yourself these questions in order:

  1. “Does it ask for ALL solutions?”
    • YESBacktracking (no choice - must enumerate)
    • NO → Continue to question 2
  2. “Does it ask for an optimal value?”
    • NO → Other techniques (Two Pointers, MinMax, etc.)
    • YES → Continue to question 3
  3. “Can I make locally optimal choices without conflicts?”
    • YES and can PROVE itGreedy (fastest)
    • NO (choices conflict) → DP (moderate)
    • UNSURE → Try counterexample: if greedy fails → DP
  4. “What does the problem return?”
    • int, boolean, long → Likely Greedy or DP
    • List<List<...>> → Likely Backtracking

Decision Priority: 1. Greedy (if applicable) - Try first, fastest 2. DP (if greedy fails) - Handles complex dependencies 3. Backtracking (if need all solutions) - Last resort, slowest