Skip to content

learn: solve Coin Change (#322) via guided TDD#237

Merged
pertrai1 merged 5 commits into
mainfrom
tdd/0322-coin-change
Feb 13, 2026
Merged

learn: solve Coin Change (#322) via guided TDD#237
pertrai1 merged 5 commits into
mainfrom
tdd/0322-coin-change

Conversation

@pertrai1

@pertrai1 pertrai1 commented Feb 13, 2026

Copy link
Copy Markdown
Owner

Problem

322. Coin Change — Medium

Approach

Dynamic Programming (unbounded knapsack variant). Build up solutions for amounts 0 to target by trying each coin and taking the minimum across all options.

Complexity

  • Time: O(amount × coins.length)
  • Space: O(amount)

Pattern

Dynamic Programming (Unbounded Knapsack)

Learning Summary

  • Mode: Coach (learner wrote all implementation code)
  • Cycles: 4 guided cycles
  • Tests: 4 test cases covering base case, impossible cases, and optimal solution
  • Hint Levels Used: Mostly L1 (guiding questions), L2 in cycle 2 for recurrence relation
  • Key Insight: The recurrence dp[i] = min(dp[i], dp[i - coin] + 1) builds optimal solutions by trying all coins for each amount
  • Pattern Checkpoints: 2/2 correct (understood state structure and recurrence after Q&A)

🧠 Self-Review Questions

  1. Why initialize with MAX_SAFE_INTEGER instead of MIN_SAFE_INTEGER?
Answer We use `MAX_SAFE_INTEGER` (or `amount + 1`) because we need a sentinel value **larger** than any possible answer. The recurrence uses `Math.min()` to find the smallest coin count. If we initialize with a very negative number like `MIN_SAFE_INTEGER`, it will always be chosen as the minimum, breaking the logic. We want invalid states to start "impossibly large" so any valid coin combination will be smaller and get selected.
  1. Why is the time complexity O(amount × coins.length) and not O(amount²)?
Answer The outer loop iterates `amount` times, and the inner loop iterates `coins.length` times. These are **different variables**, so the complexity is O(n × m), not O(n²). If both loops iterated over `amount`, then it would be O(amount²). For this problem with constraints `amount ≤ 10^4` and `coins.length ≤ 12`, the worst case is ~120,000 operations, much better than 100,000,000 if it were O(amount²).

Files

  • README.md — Problem statement
  • coin-change.ts — Solution
  • coin-change.test.ts — Test suite (4 tests)
  • POST_MORTEM.md — Interview debrief with scoring and analysis

Summary by CodeRabbit

  • New Features

    • Added solution for Coin Change problem (LeetCode #322) implementing a dynamic programming approach to find the minimum number of coins needed for a given amount.
  • Documentation

    • Added README documenting problem statement, examples, and constraints.
    • Added interview debrief document with problem-solving approach, complexity analysis, and session notes.
  • Tests

    • Added unit tests covering success scenarios, edge cases, and unreachable amount scenarios.

@pertrai1 pertrai1 added the code challenge this is a coding challenge label Feb 13, 2026
@pertrai1

Copy link
Copy Markdown
Owner Author

💡 Educational Note — DP Array Initialization

The initialization pattern here is crucial for correctness:

const dp = Array.from({ length: amount + 1 }, () => Number.MAX_SAFE_INTEGER);
dp[0] = 0;

Why MAX_SAFE_INTEGER?

  • We use Math.min() to find the smallest coin count
  • The sentinel value must be larger than any possible answer
  • This ensures any valid coin combination will be smaller and get selected
  • Using -1 or MIN_SAFE_INTEGER breaks the Math.min() logic!

Complexity: O(amount) space for the DP array.

@github-actions

github-actions Bot commented Feb 13, 2026

Copy link
Copy Markdown

Claude finished @pertrai1's task in 1m 40s —— View job


PR Review - Coin Change Solution ✅

Great work on solving this classic DP problem through guided TDD! The solution is correct, well-tested, and demonstrates strong understanding of the unbounded knapsack pattern. The POST_MORTEM documentation is exceptional and provides valuable learning insights.


1. Correctness ✅

Algorithmic Soundness: The solution is correct and handles all edge cases properly:

  • Base case: amount = 0 returns 0 (coin-change.ts:2-4)
  • Impossible cases: Returns -1 when no combination works (coin-change.ts:17)
  • Optimal solution: Correctly finds minimum coins using DP recurrence
  • Boundary checking: i - coin >= 0 prevents array underflow (coin-change.ts:11)

Mental Trace Verification:
For coins = [1,2,5], amount = 11:

dp[0] = 0
dp[1] = 1 (one 1-coin)
dp[2] = 1 (one 2-coin)
dp[5] = 1 (one 5-coin)
dp[11] = min(dp[10]+1, dp[9]+1, dp[6]+1) = 3 ✓

Test Coverage: The 4 test cases strategically cover:

  • Base case (amount = 0)
  • Simple impossible case (odd amount, even coins)
  • Optimal multi-coin solution
  • Complex impossible case (reinforcing pattern recognition)

2. Complexity Analysis ✅

Time Complexity: O(amount × coins.length)Accurate

The nested loops create amount iterations × coins.length iterations. This is not O(amount²) because the inner loop iterates over a different variable (coins, not amount).

Why this matters:

  • With constraints amount ≤ 10⁴ and coins.length ≤ 12, worst case = ~120,000 operations
  • If it were O(amount²), it would be 100,000,000 operations — much worse!

Space Complexity: O(amount)Accurate

Only the dp array of size amount + 1 is used. The solution uses a 1D DP array instead of 2D (coin × amount), which is a space optimization.

Excellent self-review questions in the PR description! The explanations for MAX_SAFE_INTEGER initialization and O(n × m) vs O(n²) are spot-on and demonstrate deep understanding.


3. Learning Focus: Dynamic Programming Pattern

Pattern: Unbounded Knapsack (DP) — Classic variant where items (coins) can be used unlimited times.

Why DP is suitable here:

  1. Optimal substructure: If we know the minimum coins for amount i - coin, we can compute minimum for amount i
  2. Overlapping subproblems: Computing dp[11] reuses dp[10], dp[9], dp[6], etc.
  3. Unlimited supply: Unlike 0/1 knapsack, each coin can be used multiple times

The Key Insight (coin-change.ts:12):

dp[i] = Math.min(dp[i], dp[i - coin] + 1);

Why this works:

  • For each amount i, try every coin denomination
  • If we use a coin of value coin, we need 1 coin + minimum coins for i - coin
  • Take the minimum across all coin choices
  • The sentinel value MAX_SAFE_INTEGER ensures invalid states are ignored by Math.min()

Why MAX_SAFE_INTEGER initialization is critical:

  • We use Math.min() to find the smallest count
  • Invalid states must start larger than any possible answer so valid combinations replace them
  • Using MIN_SAFE_INTEGER or -1 breaks the logic (they'd always be chosen as minimum!)

4. Code Quality ✅

Strengths:

  • ✅ Clean, readable code with proper TypeScript types
  • ✅ Clear variable names (dp, amount, coins)
  • ✅ Early return for base case improves readability
  • ✅ Proper bounds checking before array access
  • ✅ Modern ES6+ syntax (const, for...of)

Minor Observations:

  • The early return on line 2-4 is good practice but technically redundant (the DP array handles it)
  • Variable i could be renamed currentAmount for extra clarity, but i is acceptable for DP loops

Test Quality:

  • ✅ Descriptive test names clearly state intent
  • ✅ Strategic progression from simple to complex cases
  • ✅ Good use of Vitest's describe and it blocks

5. Related Problems for Practice

Since you've mastered the unbounded knapsack pattern, try these variations:

  1. 518. Coin Change II (Medium)

    • Count number of ways to make change (not minimum)
    • Same DP structure but uses addition instead of Math.min()
    • Key difference: dp[i] += dp[i - coin] instead of taking minimum
  2. 377. Combination Sum IV (Medium)

    • Order matters (permutations vs combinations)
    • Requires swapping loop order: iterate coins inside amount loop
    • Great for understanding when loop order changes the result
  3. 279. Perfect Squares (Medium)

    • Same pattern but "coins" are perfect squares (1, 4, 9, 16...)
    • Find minimum perfect squares that sum to n
  4. 983. Minimum Cost For Tickets (Medium)

    • Another unbounded knapsack variant with day passes
    • Good practice for similar DP state transitions

6. Alternative Approaches

Current Solution: Bottom-Up DP (Tabulation)

  • Time: O(amount × coins.length)
  • Space: O(amount)
  • Best approach for this problem — iterative, efficient, easy to understand

Alternative 1: Top-Down DP (Memoization)

  • Same complexity but uses recursion + memoization
  • More intuitive for some people (think "what do I need?")
  • Trade-off: Slightly higher space due to call stack

Alternative 2: BFS (Breadth-First Search)

  • Treat as shortest-path problem (amount as distance, coins as steps)
  • Time: O(amount × coins.length) worst case
  • Space: O(amount) for visited set + queue
  • Trade-off: More complex code for same complexity

Why your approach is optimal: Bottom-up DP is the standard solution — efficient, clear, and avoids recursion overhead.


7. Common Mistakes & Debugging Journey

The POST_MORTEM documents an excellent learning moment around DP initialization:

The Bug: Initially used Number.MIN_SAFE_INTEGER instead of MAX_SAFE_INTEGER

Why it matters: This is a classic DP initialization mistake that reveals understanding of how Math.min() works with sentinel values. The debugging journey (MIN_SAFE_INTEGER-1MAX_SAFE_INTEGER) shows methodical problem-solving.

Common Pitfalls with this pattern:

  1. ❌ Using 0 as sentinel (can't distinguish impossible from valid 0 result)
  2. ❌ Using negative sentinel with Math.min() (always chosen as minimum)
  3. ❌ Forgetting to check i - coin >= 0 (array underflow)
  4. ❌ Misunderstanding time complexity as O(n²) when loops iterate different variables

Learning Summary

Key Concepts Demonstrated:

  • Unbounded Knapsack DP pattern with optimal space complexity
  • Sentinel value initialization for DP arrays (MAX_SAFE_INTEGER)
  • Multi-variable complexity analysis (O(n × m) vs O(n²))
  • Strategic test design with edge cases and impossible scenarios
  • Methodical debugging through systematic hypothesis testing

What Makes This Solution Work:
The recurrence dp[i] = min(dp[i], dp[i - coin] + 1) captures the essence of the problem: to make amount i optimally, try every coin and pick the best option. The DP array builds solutions incrementally from 0 to amount, ensuring each subproblem is solved before being reused.

Excellent Documentation:
The POST_MORTEM is exceptional — it provides interview-style scoring, debugging narrative, and pattern checkpoints. This level of self-reflection is exactly what accelerates learning. The self-review questions in the PR description are also excellent learning artifacts.


Final Verdict: Approve

This is a textbook implementation of the unbounded knapsack pattern. The solution is correct, efficient, well-tested, and thoroughly documented. The guided TDD approach successfully built understanding incrementally, and the debugging journey around initialization is a valuable learning artifact.

Strengths:

  • Correct algorithm with optimal complexity
  • Strategic test coverage
  • Exceptional documentation (POST_MORTEM + self-review questions)
  • Clean, readable code

Growth Opportunities (from POST_MORTEM):

  • Practice mental simulation to catch initialization bugs earlier
  • Continue building precision in complexity analysis for multi-variable loops

Great work on this solution! The TDD journey and documentation make this a valuable learning reference for future DP problems. 🎉


@pertrai1

Copy link
Copy Markdown
Owner Author

💡 Educational Note — The Unbounded Knapsack Recurrence

The heart of this solution is the DP recurrence:

for (let i = 1; i <= amount; i++) {
  for (const coin of coins) {
    if (i - coin >= 0) {
      dp[i] = Math.min(dp[i], dp[i - coin] + 1);
    }
  }
}

Breaking it down:

  • dp[i - coin] = minimum coins needed for the remaining amount after using this coin
  • + 1 = accounts for using this coin once
  • Math.min(dp[i], ...) = choose the best option among all coins

Why it's "unbounded": We can use each coin multiple times (unlike 0/1 knapsack).

Complexity: O(amount × coins.length) — the outer loop runs amount times, inner loop runs coins.length times. These are different variables, so it's O(n × m), not O(n²)!

@github-actions

Copy link
Copy Markdown

✅ Test Results

Tests have been executed for this pull request.

📊 Coverage Summary

Metric Coverage
Statements 94.43%
Branches 91.04%
Functions 88%
Lines 94.34%

Check the workflow logs for detailed test output.

@coderabbitai

coderabbitai Bot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR adds a complete solution for LeetCode problem "322 - Coin Change," including a TypeScript implementation using bottom-up dynamic programming, comprehensive unit tests, problem documentation, and an interview debrief. It also updates the problems index to reference the new entry.

Changes

Cohort / File(s) Summary
Documentation Index
docs/PROBLEMS.md
Adds two references to "0322 - Coin Change" in the Medium Dynamic Programming section and Problems by Pattern index.
Problem Solution: Coin Change
leetcode/medium/0322-coin-change/coin-change.ts, leetcode/medium/0322-coin-change/coin-change.test.ts, leetcode/medium/0322-coin-change/README.md, leetcode/medium/0322-coin-change/POST_MORTEM.md
Implements a bottom-up DP solution for the coin change problem, includes four test cases covering zero amount, unreachable amounts, and optimal solutions. Provides problem statement, examples, constraints, and detailed interview debrief documenting the solution approach, debugging process, and assessment.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A shiny coin problem hops into place,
With tests and docs at a reasonable pace,
Dynamic programming in TypeScript's embrace,
The change we needed—a complete solution case! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'learn: solve Coin Change (#322) via guided TDD' clearly and specifically identifies the main change: adding a solution to LeetCode problem 322 using test-driven development methodology, matching the PR's core objective and all included changes.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tdd/0322-coin-change

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
leetcode/medium/0322-coin-change/coin-change.test.ts (1)

4-19: Good test coverage for the core scenarios.

The four tests align well with the README examples and cover base case, impossible cases, and optimal solution. Consider adding a test for a single-coin exact-match scenario (e.g., coinChange([1], 1)1) and a larger amount to exercise more of the DP table, but this is minor given the guided TDD scope.

As per coding guidelines: "Add edge case tests: empty inputs, single elements, nulls, boundary conditions in test files" and "Test all examples from problem description in test files."

leetcode/medium/0322-coin-change/coin-change.ts (1)

1-18: Add complexity analysis and algorithmic pattern comments.

The solution is correct and clean. However, per coding guidelines, LeetCode solutions should document both time/space complexity in Big O notation and the algorithmic pattern used.

📝 Suggested comment additions
+/**
+ * 322. Coin Change
+ * Pattern: Dynamic Programming (Unbounded Knapsack)
+ * 
+ * Time: O(amount × coins.length) — for each amount 1..amount, we try each coin
+ * Space: O(amount) — 1D DP array of length amount + 1
+ */
 export function coinChange(coins: number[], amount: number): number {

As per coding guidelines: "Always document both time and space complexity in Big O notation with clear reasoning" and "Document the algorithmic pattern used in solution code comments."


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

🤖 Gemini Code Review (Model: gemini-2.5-flash)

Here's a detailed review of the coin-change.ts solution:

1. Pattern Identification

This solution uses Dynamic Programming (DP), specifically a variation of the Unbounded Knapsack problem. The core idea is to build up the solution for larger amounts by leveraging the already computed optimal solutions for smaller amounts.

2. Complexity Verification

  • Stated Time Complexity: The POST_MORTEM.md indicates an initial misstatement of O(n^2) which was corrected to O(n * m).

    • Correct Time Complexity: O(amount * coins.length).
      • Why: The outer loop iterates amount times (from 1 to amount).
      • The inner loop iterates coins.length times (for each coin in coins).
      • Therefore, the total operations are proportional to amount * coins.length. If n is amount and m is coins.length, this is O(n * m).
      • The POST_MORTEM.md correctly identified this for improvement.
  • Stated Space Complexity: The POST_MORTEM.md correctly identified O(n).

    • Correct Space Complexity: O(amount).
      • Why: An array dp of size amount + 1 is created to store the minimum number of coins for each amount from 0 to amount. This storage scales linearly with the amount.

3. Key Insight

The key insight for solving the Coin Change problem with Dynamic Programming is recognizing that the minimum number of coins needed to make a target amount i can be found by considering each available coin and asking: "If I use this coin, what's the minimum number of coins needed for the remaining amount (i - coin) plus this one coin?"

More formally, the "aha!" moment is the recurrence relation:
dp[i] = min(dp[i], dp[i - coin] + 1)

Here:

  • dp[i] represents the minimum number of coins required to make the amount i.
  • We initialize dp[i] to a very large value (or infinity) to ensure Math.min correctly picks the first valid option.
  • For each amount i from 1 to amount, we iterate through all coins.
  • If i - coin is a valid previous amount (i.e., i - coin >= 0) and dp[i - coin] is reachable (not infinity), then we can potentially form amount i by using coin and the minimum coins for i - coin.
  • We take the minimum of all such possibilities to find the optimal dp[i].
  • The + 1 accounts for the current coin being used.

This bottom-up approach ensures that when we calculate dp[i], all necessary dp[i - coin] values have already been optimally computed.

4. Edge Case Analysis

The current solution handles several important edge cases:

  • amount = 0: Explicitly handled at the beginning, returning 0 coins. This is the base case for the DP.
  • coins array empty: The problem constraints state 1 <= coins.length, so this is not a valid input according to LeetCode. If it were, the solution would correctly result in dp[amount] remaining Number.MAX_SAFE_INTEGER, leading to a -1 return.
  • amount cannot be made: If no combination of coins can sum up to amount, dp[amount] will remain Number.MAX_SAFE_INTEGER (its initial value), and the solution correctly returns -1. (e.g., coins = [2], amount = 3).
  • Single coin type: Handled correctly (e.g., coins = [2], amount = 4 -> 2).
  • Large amount or coins[i] values: Number.MAX_SAFE_INTEGER is a robust sentinel value for large amount values up to 10^4. The number of coins can potentially exceed MAX_SAFE_INTEGER if amount is very large and coins are all 1, but amount <= 10^4 means max coins is 10^4, which fits MAX_SAFE_INTEGER.

Potential edge case not explicitly tested (but handled):

  • coins array contains 1: This ensures any positive amount can always be made. The current tests don't explicitly isolate this, but [1, 2, 5] covers it.

5. Learning Points

  • Similar problems using this pattern:

    • 518. Coin Change II: Instead of the minimum number of coins, this problem asks for the number of ways to make change. The DP state and recurrence relation are slightly different (dp[i] += dp[i - coin]).
    • 377. Combination Sum IV: Count combinations where order matters (permutations). Similar DP structure, but the inner and outer loops are typically swapped compared to Coin Change I/II to account for permutations.
    • 279. Perfect Squares: Find the minimum number of perfect square numbers that sum to a given n. Here, the "coins" are dynamically generated (1, 4, 9, 16, ...).
    • Unbounded Knapsack: This is a classic variant where items can be chosen multiple times. The "items" are coins, and the "capacity" is the amount.
  • Common mistakes people make with this pattern:

    • Incorrect Initialization of dp array: As noted in the POST_MORTEM.md, a common mistake is using Number.MIN_SAFE_INTEGER or -1 when Number.MAX_SAFE_INTEGER is required for Math.min() to work correctly. The sentinel value must be larger than any possible valid answer so that any valid calculation dp[i - coin] + 1 will always be smaller.
    • Off-by-one errors in loop bounds: Incorrectly iterating from 0 to amount - 1 instead of 1 to amount, or mismanaging i - coin >= 0 condition.
    • Forgetting the + 1: When using a coin, you're adding one more coin to the count for the subproblem.
    • Incorrect order of loops: For "minimum coins" or "count ways" (where order doesn't matter), the outer loop should typically be over the amount and the inner loop over coins. Swapping them can change the problem (e.g., counting permutations vs. combinations).
  • Variations of this problem:

    • Limited number of each coin: This would turn it into a 0/1 Knapsack type problem or require a slightly more complex DP state (e.g., dp[i][j] = min coins for amount i using first j coin types).
    • Maximum number of coins allowed: Add another constraint to the DP state or a pruning condition.
    • Counting permutations vs. combinations: As mentioned with Coin Change II and Combination Sum IV.

6. Code Quality

The code quality is excellent:

  • Variable Naming: coins, amount, dp, i, coin are all descriptive and standard for this problem.
  • Code Structure: The function is clean, concise, and has a clear early exit for the base case. The DP logic is well-contained.
  • Readability: The use of Array.from for initialization and Number.MAX_SAFE_INTEGER makes the intent clear. Math.min is used appropriately.
  • TypeScript Best Practices: Uses const and let correctly, and export for the function.

No significant improvements are needed for code quality; it's already very good.

7. Alternative Approaches

  1. Recursion with Memoization (Top-Down Dynamic Programming):

    • Approach: Define a recursive function solve(currentAmount) that returns the minimum coins needed for currentAmount. If currentAmount is 0, return 0. If currentAmount is negative, return infinity. Iterate through each coin, and recursively call solve(currentAmount - coin). Take the minimum of 1 + solve(currentAmount - coin) for all coins. Use a memoization table (e.g., a Map or an array) to store and return results for currentAmount if already computed.
    • Trade-offs:
      • Pros: Often more intuitive to formulate initially as it directly translates the problem statement ("What's the minimum for this amount? Well, it's 1 + min for (amount - coin)"). Can be more efficient if not all subproblems need to be solved (though for Coin Change, most usually are).
      • Cons: Can lead to stack overflow errors for very large amount values due to deep recursion. Overhead of function calls.
  2. Breadth-First Search (BFS):

    • Approach: This problem can be modeled as finding the shortest path in an unweighted graph. The nodes of the graph are amounts from 0 to amount. An edge exists from u to v if v = u + coin for some coin in coins. We want to find the minimum number of edges (coins) to reach amount from 0. Start a BFS from amount = 0, adding 0 + coin to the queue for each coin, keeping track of the "level" (number of coins used).
    • Trade-offs:
      • Pros: Conceptually simple for shortest path problems. Guarantees finding the minimum number of coins because BFS explores layer by layer.
      • Cons: Can be less performant than DP for this specific problem due to potentially larger queue sizes and visited set management. The DP approach is often more optimized for this "minimum value to reach target" type of problem. The state representation (dp array) is essentially the distance array in BFS, and the loops implicitly handle the exploration.

@github-actions

Copy link
Copy Markdown

🤖 Gemini Code Review (Model: gemini-2.5-flash)

Here's a review of the provided LeetCode solution for the "Coin Change" problem:


1. Pattern Identification

This solution uses the Dynamic Programming (DP) pattern. Specifically, it's a classic example of the Unbounded Knapsack problem variant, where we want to find the minimum number of items (coins) to reach a target sum (amount), with an unlimited supply of each item. It builds up the solution from smaller subproblems to solve the larger problem.

2. Complexity Verification

  • Time Complexity: O(amount * coins.length)

    • Verification: The outer for loop iterates amount times (from 1 to amount). The inner for...of loop iterates coins.length times for each iteration of the outer loop. Therefore, the total number of operations is proportional to amount * coins.length.
    • Accuracy: The stated complexity in the POST_MORTEM.md (O(n * m) where n is amount and m is coins.length) is accurate. It correctly avoids the common mistake of assuming O(n^2) when the inner loop depends on a different variable.
  • Space Complexity: O(amount)

    • Verification: An array dp of size amount + 1 is created to store the minimum coins needed for each amount from 0 to amount.
    • Accuracy: The stated complexity in the POST_MORTEM.md (O(n)) is accurate.

3. Key Insight

The key insight for this problem is that the minimum number of coins required to make a certain amount i can be derived from the minimum number of coins required for smaller amounts.

  1. Subproblem Definition: Define dp[i] as the minimum number of coins needed to make amount i.
  2. Base Case: dp[0] = 0, because zero amount requires zero coins.
  3. Recurrence Relation: To find dp[i], we iterate through each coin available. If i - coin is a valid (non-negative) amount, it means we can potentially make amount i by taking one coin and adding it to the minimum coins needed for i - coin. So, dp[i] = min(dp[i], dp[i - coin] + 1). We initialize dp[i] to a very large number (infinity) to ensure Math.min correctly finds the smallest value.
  4. Building Up: By computing dp[i] for i from 1 up to amount, we guarantee that dp[i - coin] will always have been computed before we need it for dp[i].

This bottom-up approach ensures that when we calculate dp[i], all necessary subproblems dp[j] (where j < i) have already been optimally solved.

4. Edge Case Analysis

The current solution and its tests handle several important edge cases:

  • amount = 0: Explicitly handled at the beginning, returning 0. This is the base case for the DP.
  • amount cannot be made: If dp[amount] remains Number.MAX_SAFE_INTEGER after all calculations, it means the amount is impossible to make, and -1 is returned. This is crucial for cases like coins = [2], amount = 3.
  • Single coin type: The logic works correctly even if only one coin type is available.
  • Large amount / coins: The constraints (amount <= 10^4) mean the dp array size is manageable. Number.MAX_SAFE_INTEGER is a suitable sentinel value.
  • coins array guaranteed non-empty: The problem constraints state 1 <= coins.length <= 12, so an empty coins array doesn't need to be handled.

The solution is robust for these common edge cases.

5. Learning Points

  • Similar Problems:

    • 518. Coin Change II: Instead of the minimum number of coins, this asks for the total number of ways to make the amount. The DP state and recurrence are similar, but instead of min, you use sum. The order of loops might also change depending on whether combinations or permutations are desired.
    • 377. Combination Sum IV: Asks for the number of permutations (order matters) to make the target sum. Here, the outer loop iterates through amount and the inner loop iterates through coins to count permutations.
    • 279. Perfect Squares: Find the minimum number of perfect square numbers (1, 4, 9, ...) that sum to n. This is essentially Coin Change where the "coins" are perfect squares.
    • Unbounded Knapsack: General class of problems where you can take items repeatedly to reach a target value (e.g., Rod Cutting).
  • Common Mistakes with this Pattern:

    • Incorrect Initialization: A very common mistake, as noted in the POST_MORTEM.md, is using Number.MIN_SAFE_INTEGER or -1 for impossible states when Math.min is used. The sentinel value must be greater than any possible valid answer (Number.MAX_SAFE_INTEGER or Infinity).
    • Off-by-one Errors: Incorrect array indexing (i - coin) or loop bounds can lead to errors.
    • Incorrect Loop Order: For "minimum coins" or "count combinations" problems, the outer loop should generally iterate through the amount and the inner loop through the coins. If the problem asks for "number of permutations" (order matters), the loop order is often reversed (amount inner, coins outer).
    • Not Handling Impossible Cases: Forgetting to check if the final dp[amount] value is still the sentinel (infinity) and returning -1.
  • Variations of this Problem:

    • Bounded Knapsack: Each coin has a limited quantity available.
    • Path Reconstruction: After finding the minimum number of coins, reconstruct which coins were used to achieve that minimum. This typically involves storing predecessor information during the DP calculation.
    • Constraints on Coin Usage: E.g., at most k coins can be used, or a specific coin can only be used x times.

6. Code Quality

The code quality is high:

  • Variable Naming: coins, amount, dp, i, coin are all clear, concise, and standard for this type of problem.
  • Code Structure: The function is well-organized with a clear base case, DP array initialization, nested loops for the core logic, and a final conditional return.
  • Readability: The code is very readable and easy to understand.
  • TypeScript Best Practices: Correct type annotations are used. Array.from for initialization is modern and clean. const is used for dp as the array reference itself doesn't change. Math.min is used appropriately.

No significant improvements are needed for code quality.

7. Alternative Approaches

  1. Recursion with Memoization (Top-Down Dynamic Programming):

    • Approach: Define a recursive function memoizedCoinChange(remAmount) that returns the minimum coins for remAmount. It checks a memoization table first. If remAmount is 0, return 0. If remAmount is negative, return infinity. Otherwise, iterate through each coin, recursively call memoizedCoinChange(remAmount - coin), and take 1 + min(results). Store and return the result.
    • Trade-offs:
      • Pros: Often more intuitive to formulate initially for some developers as it directly mirrors the problem statement ("what's the answer for this amount?").
      • Cons: Can lead to stack overflow issues for very large amount values (though 10^4 is usually fine in JavaScript). Performance is generally comparable to the iterative bottom-up DP.
  2. Breadth-First Search (BFS):

    • Approach: This problem can be modeled as finding the shortest path in a graph. Nodes are amount values from 0 to amount. An edge exists from u to v if v = u + coin for any coin. Start a BFS from amount 0. The level at which targetAmount is first reached represents the minimum number of coins.
    • Trade-offs:
      • Pros: Naturally finds the shortest path (minimum number of operations/coins) due to BFS's level-by-level exploration. Can be conceptually intuitive for "minimum steps" problems.
      • Cons: Can be less efficient in terms of constant factors compared to DP for this specific problem. Requires a queue and a visited set (or distance array) to avoid cycles and redundant calculations, leading to similar O(amount) space complexity. Time complexity is also O(amount * coins.length) in the worst case, as each amount state can be visited once and process coins.length transitions.

@pertrai1

Copy link
Copy Markdown
Owner Author

🎯 How This Solution Works

Think of this problem like making change at a store, but you want to give the fewest coins possible. The DP approach builds up the answer by solving smaller subproblems first.

The Big Idea

Imagine you need to make \ with coins of , , and . Instead of trying every possible combination (exhausting!), we ask:

"If I already know the best way to make , , ... can I use that to figure out the best way to make ?"

Answer: Yes! To make , we can:

  • Use a \ coin + the best way to make \ (that's dp[10] + 1)
  • Use a \ coin + the best way to make \ (that's dp[9] + 1)
  • Use a \ coin + the best way to make \ (that's dp[6] + 1)

We try all three options and pick the one with the fewest total coins.

The Code Journey

Step 1: Set up the table

const dp = Array.from({ length: amount + 1 }, () => Number.MAX_SAFE_INTEGER);
dp[0] = 0;
  • Create a table where dp[i] = "minimum coins to make amount i"
  • Initialize everything to "impossible" (very large number)
  • Except dp[0] = 0 because making /bin/zsh requires 0 coins (duh!)

Step 2: Fill the table bottom-up

for (let i = 1; i <= amount; i++) {
  for (const coin of coins) {
    if (i - coin >= 0) {
      dp[i] = Math.min(dp[i], dp[i - coin] + 1);
    }
  }
}
  • For each amount from 1 to 11...
  • Try each coin...
  • If using this coin is valid (doesn't go negative)...
  • Update the table with the best option so far

Step 3: Check the answer

return dp[amount] === Number.MAX_SAFE_INTEGER ? -1 : dp[amount];
  • If dp[11] is still "impossible" (our sentinel value), return -1
  • Otherwise, return the minimum coins needed!

Why This Works

This is called Dynamic Programming because we're solving each subproblem once and reusing the results. Instead of recalculating "how do I make ?" every time, we look it up in our table!

Time complexity: O(amount × coins.length) — We fill in amount entries, trying each coin for each entry.

Space complexity: O(amount) — Just the DP table.

The Gotcha

The trickiest part? Initialization! We use MAX_SAFE_INTEGER (not -1 or 0) because we need a value that's larger than any real answer. When we use Math.min(), we want invalid states to lose to valid ones. If we used -1, it would always win the minimum contest, and we'd never find the real answer!


Related problems: Coin Change II (518), Combination Sum IV (377), Perfect Squares (279)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new LeetCode medium solution folder for #322 Coin Change to the learning-oriented challenge repository, including implementation, tests, and supporting documentation.

Changes:

  • Added a TypeScript dynamic programming solution for Coin Change (unbounded knapsack variant).
  • Added a Vitest test suite covering base, impossible, and optimal-solution scenarios.
  • Added problem documentation (README + interview-style post-mortem) and updated the central problem index.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
leetcode/medium/0322-coin-change/coin-change.ts Implements bottom-up DP to compute the minimum coins needed per amount.
leetcode/medium/0322-coin-change/coin-change.test.ts Adds Vitest coverage for key reachable/unreachable and base-case scenarios.
leetcode/medium/0322-coin-change/README.md Documents the problem statement, examples, and constraints.
leetcode/medium/0322-coin-change/POST_MORTEM.md Adds a guided-TDD interview debrief with cycle-by-cycle notes and analysis.
docs/PROBLEMS.md Registers the new problem in the main index (difficulty + pattern sections).

@pertrai1 pertrai1 merged commit 6e16e0a into main Feb 13, 2026
11 of 12 checks passed
@pertrai1 pertrai1 deleted the tdd/0322-coin-change branch February 13, 2026 14:23
@github-actions

Copy link
Copy Markdown

📅 Spaced Repetition Reviews Scheduled!

Great job solving #0322 - Coin Change! 🎉

To help you retain this knowledge long-term, I've scheduled 5 review sessions using spaced repetition:

Review Interval Schedule Logic
1st Review 1 day after solving Scheduled now
2nd Review 3 days after 1st review Auto-scheduled when 1st completes
3rd Review 7 days after 2nd review Auto-scheduled when 2nd completes
4th Review 14 days after 3rd review Auto-scheduled when 3rd completes
5th Review 30 days after 4th review Auto-scheduled when 4th completes

What to expect:

  • Your 1st review is scheduled for tomorrow
  • Each subsequent review is scheduled automatically when you complete the previous one
  • This ensures proper spacing even if you complete a review a few days late
  • GitHub issues will be created automatically for each review
  • Each issue will link back to your solution

🧠 Why Spaced Repetition?

Research shows that reviewing material at increasing intervals dramatically improves retention. This adaptive scheduling ensures optimal spacing based on when you actually complete each review!

Check docs/reviews/review-schedule.json to see your review schedule.

github-actions Bot pushed a commit that referenced this pull request Feb 13, 2026
  Problem: #0322 - Coin Change
  PR: #237
  First review scheduled (subsequent reviews auto-scheduled on completion)

  [skip ci]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code challenge this is a coding challenge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants