Table of Contents
- # Overview
- # Key Properties
- # Core Characteristics
- # Step
- # References
- # Problem Categories
- # Category 1: Linear DP
- # Category 2: Grid/2D DP
- # Category 3: Interval DP
- # Category 4: Tree DP
- # Category 5: State Machine DP
- # Category 6: Knapsack DP
- # Category 7: String DP
- # Category 8: State Compression DP
- # Templates & Algorithms
- # Template Comparison Table
- # Universal DP Template
- # Template 1: 1D Linear DP
- # 1D DP: Array Sizing and Loop Bounds (n vs n+1)
- # Template 2: 2D Grid DP
- # Template 3: Interval DP
- # Template 3-2: Classic Interval DP Pattern (LC 312 Burst Balloons Style)
- # Template 4: 0/1 Knapsack
- # Template 5: State Machine DP
- # Template 5-2: State Machine DP with Cooldown (LC 309 Pattern)
- # Template 6: Top-Down Memoization
- # Template 7: String DP (Edit Distance / Levenshtein Distance)
- # Template 8: Longest Common Subsequence (LCS)
- # Template 9: State Compression (Bitmask DP)
- # Template 10: Palindrome DP
- # Template 11: Fibonacci-like Patterns
- # Comprehensive Pattern Analysis
- # 1D DP Patterns
- # 2D DP Patterns
- # Knapsack Patterns
- # Critical Pattern: Loop Order in Unbounded Knapsack (Combinations vs Permutations)
- # Critical Pattern: Understanding if (i - coin >= 0) in Coin Change DP
- # String DP Patterns
- # Classic String DP Patterns (Detailed)
- # Valid Parenthesis String Pattern (LC 678) π
- # State Compression Patterns
- # Advanced DP Patterns
- # Problems by Pattern
- # Linear DP Problems
- # 2D Grid Problems
- # Interval DP Problems
- # Knapsack Problems
- # State Machine Problems
- # LC Examples
- # 2-1) Unique Paths
- # 2-2) Maximum Product Subarray
- # 2-3) Best Time to Buy and Sell Stock with Transaction Fee
- # 2-3-2) Best Time to Buy and Sell Stock with Cooldown (LC 309)
- # 2-3-3) N-th Tribonacci Number
- # 2-4) Longest Increasing Subsequence (LIS)
- # Decision Framework
- # Pattern Selection Strategy
- # When to Use DP vs Other Approaches
- # Summary & Quick Reference
- # Complexity Quick Reference
- # State Definition Guidelines
- # Common Recurrence Relations
- # Problem-Solving Steps
- # Common Mistakes & Tips
- # Space Optimization Techniques
- # Interview Tips
- # State Machine DP Interview Pattern Recognition
- # Related Topics
# Dynamic Programming (DP)
# Overview
Dynamic Programming is an algorithmic paradigm that solves complex problems by breaking them down into simpler subproblems and storing their solutions to avoid redundant computations.
# Key Properties
- Time Complexity: Problem-specific, typically O(nΒ²) or O(nΒ³)
- Space Complexity: O(n) to O(nΒ²) for memoization table
- Core Idea: Trade space for time by memoizing overlapping subproblems
- When to Use: Problems with optimal substructure and overlapping subproblems
- Key Techniques: Memoization (top-down) and Tabulation (bottom-up)
# Core Characteristics
-
Optimal Substructure: Optimal solution contains optimal solutions to subproblems
-
Overlapping Subproblems: Same subproblems solved multiple times
-
Memoization: Store results to avoid recomputation
-
State Transition: Define relationship between states
-
Core var:
state,transition -
NOTE !!! can put
everythingintostate

# Step
Step 1. define dp def
Step 2. define dp eq
Step 3. check boundaru condition, req, edge case
Step 4. get the result
# References
# Problem Categories
# Category 1: Linear DP
- Description: Single sequence problems with linear dependencies
- Examples: LC 70 (Climbing Stairs), LC 198 (House Robber), LC 300 (LIS)
- Pattern: dp[i] depends on dp[i-1], dp[i-2], etc.
# Category 2: Grid/2D DP
- Description: Problems on 2D grids or matrices
- Examples: LC 62 (Unique Paths), LC 64 (Minimum Path Sum), LC 221 (Maximal Square)
- Pattern: dp[i][j] depends on neighbors
# Category 3: Interval DP
- Description: Problems on intervals or subarrays
- Examples: LC 312 (Burst Balloons), LC 1000 (Minimum Cost to Merge Stones)
- Pattern: dp[i][j] for interval [i, j]
# Category 4: Tree DP
- Description: DP on tree structures
- Examples: LC 337 (House Robber III), LC 968 (Binary Tree Cameras)
- Pattern: State at each node depends on children
# Category 5: State Machine DP
- Description: Problems with multiple states and transitions
- Examples: LC 714 (Stock with Fee), LC 309 (Stock with Cooldown), LC 122 (Stock II)
- Pattern: Multiple DP arrays for different states
- Key Characteristic: State transitions depend on previous state + action constraints
Sub-patterns:
-
2-State Machine (Buy/Sell without cooldown)
- States:
hold,cash - Example: LC 122 (unlimited transactions)
- States:
-
3-State Machine (Buy/Sell with cooldown) β
- States:
hold,sold,rest - Example: LC 309 (cooldown after sell)
- Key:
reststate prevents immediate buy after sell
- States:
-
Multi-State Machine (Limited transactions)
- States:
buy1,sell1,buy2,sell2, β¦ - Example: LC 123 (at most 2 transactions), LC 188 (at most k transactions)
- States:
# Category 6: Knapsack DP
- Description: Selection problems with constraints
- Examples: LC 416 (Partition Equal Subset), LC 494 (Target Sum)
- Pattern: dp[i][j] for items and capacity/target
# Category 7: String DP
- Description: String matching, transformation, and subsequence problems
- Examples: LC 72 (Edit Distance), LC 1143 (LCS), LC 5 (Longest Palindromic Substring)
- Pattern: dp[i][j] for positions in two strings
# Category 8: State Compression DP
- Description: Use bitmask to represent states, optimize space complexity
- Examples: LC 691 (Stickers to Spell Word), LC 847 (Shortest Path Visiting All Nodes)
- Pattern: dp[mask] where mask represents visited/selected items
# Templates & Algorithms
# Template Comparison Table
| Template Type | Use Case | State Definition | When to Use |
|---|---|---|---|
| 1D Linear | Single sequence | dp[i] = state at position i | Fibonacci-like problems |
| 2D Grid | Matrix paths | dp[i][j] = state at (i,j) | Path counting, min/max |
| Interval | Subarray/substring | dp[i][j] = [i,j] interval | Palindrome, partition |
| Knapsack | Selection with limit | dp[i][w] = items & weight | 0/1, unbounded selection |
| State Machine | Multiple states | dp[i][state] = at i in state | Buy/sell stocks |
# Universal DP Template
def dp_solution(input_data):
# Step 1: Define state
# dp[i] represents...
# Step 2: Initialize base cases
dp = initialize_dp_array()
dp[0] = base_value
# Step 3: State transition
for i in range(1, n):
# Apply recurrence relation
dp[i] = f(dp[i-1], dp[i-2], ...)
# Step 4: Return answer
return dp[n-1]
# Template 1: 1D Linear DP
def linear_dp(nums):
"""Classic 1D DP for sequence problems"""
n = len(nums)
if n == 0:
return 0
# State: dp[i] = optimal value at position i
dp = [0] * n
dp[0] = nums[0]
for i in range(1, n):
# Transition: current vs previous
dp[i] = max(dp[i-1], nums[i])
# Or with skip: dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[n-1]
# 1D DP: Array Sizing and Loop Bounds (n vs n+1)
Key Question: Why do some 1D DP problems loop from 0 to n, while others loop from 0 to n+1?
The difference comes down to what a single index in your DP array represents. Here are the three main reasons:
# 1. βIndicesβ vs βCountβ (The Offset)
This is the most frequent reason for the difference.
-
Loop to
n(Array size =n): You are treating the index as a specific element in the input arraydp[i]means βthe best result using the i-th elementβ- Example:
dp[3]represents βresult at element index 3β
-
Loop to
n+1(Array size =n+1): You are treating the index as a quantity or lengthdp[i]means βthe best result using the first i elementsβ- Example:
dp[3]represents βresult using first 3 elementsβ
Example: LC 198 (House Robber)
# Approach 1: Array size = n (index-based)
def rob_v1(nums):
n = len(nums)
if n == 0: return 0
if n == 1: return nums[0]
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, n): # Loop to n
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[n-1] # Answer at last index
# Approach 2: Array size = n+1 (count-based)
def rob_v2(nums):
n = len(nums)
dp = [0] * (n + 1) # Extra space for "0 houses"
dp[0] = 0 # Robbing 0 houses = 0
dp[1] = nums[0] if n > 0 else 0
for i in range(2, n + 1): # Loop to n+1
dp[i] = max(dp[i-1], dp[i-2] + nums[i-1]) # Note: nums[i-1]
return dp[n] # Answer at position n
# 2. Physical βStepsβ vs βGoalsβ
In problems involving movement (stairs, paths), the βgoalβ is one step past the last index.
Example: LC 70 (Climbing Stairs) / LC 746 (Min Cost Climbing Stairs)
If cost = [10, 15, 20] (indices 0, 1, 2):
- These are the steps you can stand on
- The βFloorβ (goal) is at index 3
- Therefore,
dparray needs sizen + 1to include the landing
# LC 746: Min Cost Climbing Stairs
def minCostClimbingStairs(cost):
n = len(cost)
dp = [0] * (n + 1) # Need n+1 for the "top floor"
# You can start from step 0 or step 1
dp[0] = 0
dp[1] = 0
for i in range(2, n + 1): # Loop to n+1
# You can arrive from i-1 or i-2
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
return dp[n] # The top floor is at position n
# 3. Handling the βEmptyβ Base Case
Many DP problems need a base case representing βnothingβ (target sum = 0, empty string, etc.).
Examples:
- Knapsack/Coin Change: Need
dp[target + 1]becausedp[0]represents sum = 0 - Longest Common Subsequence: Use
(n+1) x (m+1)matrix where first row/column = empty string
# LC 322: Coin Change
def coinChange(coins, amount):
dp = [float('inf')] * (amount + 1) # Need amount+1
dp[0] = 0 # Base case: 0 coins needed for amount 0
for i in range(1, amount + 1): # Loop to amount+1
for coin in coins:
if i >= coin:
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
# Comparison Summary
| Feature | Loop 0 to n |
Loop 0 to n+1 |
|---|---|---|
| Array Size | new int[n] |
new int[n + 1] |
dp[i] meaning |
Result at element i |
Result considering first i items |
| Typical Base Case | dp[0] and dp[1] |
dp[0] is the βemptyβ state |
| Access Pattern | dp[i] β nums[i] |
dp[i] β nums[i-1] |
| Final Answer | dp[n - 1] |
dp[n] |
| Use Case | Direct element mapping | Count/quantity problems, βgoalβ beyond array |
# π‘ Pro Tips
-
Struggling with off-by-one errors? Try the
n+1approach- It allows index
ito represent the i-th item - Keeps
dp[0]as a βsafeβ dummy value for base case - Cleaner alignment between problem size and array index
- It allows index
-
When to use which?
- Use
n+1when: Problem describes βfirst i itemsβ, βi stepsβ, or needs βemptyβ base case - Use
nwhen: Direct element-to-index mapping makes more sense
- Use
-
Rewriting between styles:
nβn+1: Add 1 to array size, shift base cases, adjustnums[i]tonums[i-1]n+1βn: Remove dummy index, handle base cases explicitly, use direct indexing
# Side-by-Side Example: LC 70 (Climbing Stairs)
# Style 1: Array size = n+1 (RECOMMENDED for this problem)
def climbStairs_v1(n):
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 2
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
# Style 2: Array size = n (requires careful handling)
def climbStairs_v2(n):
if n <= 2:
return n
dp = [0] * n
dp[0] = 1 # 1 way to reach step 1
dp[1] = 2 # 2 ways to reach step 2
for i in range(2, n):
dp[i] = dp[i-1] + dp[i-2]
return dp[n-1]
Note: For Climbing Stairs, the n+1 style is more intuitive because:
dp[i]naturally represents βnumber of ways to reach step iβ- Step
nis the goal, sodp[n]is the answer - Avoids the mental overhead of mapping βstep iβ to βindex i-1β
# Template 2: 2D Grid DP
def grid_dp(grid):
"""2D DP for grid/matrix problems"""
if not grid or not grid[0]:
return 0
m, n = len(grid), len(grid[0])
dp = [[0] * n for _ in range(m)]
# Initialize first row and column
dp[0][0] = grid[0][0]
for i in range(1, m):
dp[i][0] = dp[i-1][0] + grid[i][0]
for j in range(1, n):
dp[0][j] = dp[0][j-1] + grid[0][j]
# Fill DP table
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[m-1][n-1]
# Template 3: Interval DP
def interval_dp(arr):
"""DP for interval/subarray problems"""
n = len(arr)
# dp[i][j] = optimal value for interval [i, j]
dp = [[0] * n for _ in range(n)]
# Base case: single elements
for i in range(n):
dp[i][i] = arr[i]
# Iterate by interval length
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
# Try all split points
for k in range(i, j):
dp[i][j] = max(dp[i][j],
dp[i][k] + dp[k+1][j] + cost(i, j))
return dp[0][n-1]
# Template 3-2: Classic Interval DP Pattern (LC 312 Burst Balloons Style)
π― Key Insight: Think about which element to process LAST, not first!
This is the hallmark of interval DP problems like Burst Balloons, Matrix Chain Multiplication, and similar problems where the order of operations matters.
Core Pattern:
- State:
dp[i][j]= optimal value for interval(i, j)(often exclusive) - Transition: For each element
kin(i, j), assumekis the last element processed - Why βLastβ? When
kis last, the subproblems on left and right are independent
The 3-Level Nested Loop Structure:
for length in [2, 3, ..., n+1]: # Build from small to large intervals
for left in [0, 1, ..., n-length]: # Try all possible left boundaries
right = left + length # Calculate right boundary
for k in [left+1, ..., right-1]: # Try each element as LAST
# dp[left][right] = combine(dp[left][k], dp[k][right], cost)
# Pattern 1: Burst Balloons (LC 312) - Exclusive Boundaries
Problem: Burst all balloons to maximize coins. Bursting balloon i gives nums[i-1] * nums[i] * nums[i+1] coins.
Key Insight:
- Add boundaries
[1, ...nums..., 1]to handle edge cases dp[i][j]= max coins from bursting balloons betweeniandj(exclusive)- When
kis the last balloon burst in(i, j), its neighbors areiandj
Why This Works:
- If we think βwhich balloon to burst first?β, the problem is hard because neighbors change
- If we think βwhich balloon to burst last?β, when we burst
klast:- All balloons in
(i, k)are already gone β subproblemdp[i][k] - All balloons in
(k, j)are already gone β subproblemdp[k][j] - Only
i,k,jremain β coins =balloons[i] * balloons[k] * balloons[j]
- All balloons in
Python Implementation:
def maxCoins(nums):
"""LC 312: Burst Balloons - Classic Interval DP"""
n = len(nums)
# Step 1: Add boundary balloons with value 1
balloons = [1] + nums + [1]
# Step 2: dp[i][j] = max coins from bursting balloons between i and j (exclusive)
dp = [[0] * (n + 2) for _ in range(n + 2)]
# Step 3: Build from small intervals to large
for length in range(2, n + 2): # length of interval
for left in range(n + 2 - length): # left boundary
right = left + length # right boundary
# Step 4: Try each balloon k as the LAST to burst in (left, right)
for k in range(left + 1, right):
# Coins from bursting k last (only left, k, right remain)
coins = balloons[left] * balloons[k] * balloons[right]
# Add coins from left and right subproblems
total = coins + dp[left][k] + dp[k][right]
dp[left][right] = max(dp[left][right], total)
# Answer: burst all balloons between boundaries 0 and n+1
return dp[0][n + 1]
Java Implementation:
// LC 312: Burst Balloons
public int maxCoins(int[] nums) {
int n = nums.length;
// Add boundaries: [1, ...nums..., 1]
int[] balloons = new int[n + 2];
balloons[0] = 1;
balloons[n + 1] = 1;
for (int i = 0; i < n; i++) {
balloons[i + 1] = nums[i];
}
// dp[i][j] = max coins from bursting balloons between i and j (exclusive)
int[][] dp = new int[n + 2][n + 2];
// Iterate over interval lengths (from 2 up to n+1)
for (int len = 2; len <= n + 1; len++) {
// i is the left boundary
for (int i = 0; i <= n + 1 - len; i++) {
int j = i + len; // j is the right boundary
// Pick k as the LAST balloon to burst in interval (i, j)
for (int k = i + 1; k < j; k++) {
int currentCoins = balloons[i] * balloons[k] * balloons[j];
int total = currentCoins + dp[i][k] + dp[k][j];
dp[i][j] = Math.max(dp[i][j], total);
}
}
}
return dp[0][n + 1];
}
Example Trace: nums = [3,1,5,8]
After adding boundaries: [1, 3, 1, 5, 8, 1] (indices 0-5)
Building dp[0][5] (entire interval):
Try k=1 (value 3) as LAST:
coins = balloons[0] * balloons[1] * balloons[5] = 1 * 3 * 1 = 3
total = 3 + dp[0][1] + dp[1][5]
Try k=2 (value 1) as LAST:
coins = balloons[0] * balloons[2] * balloons[5] = 1 * 1 * 1 = 1
total = 1 + dp[0][2] + dp[2][5]
Try k=3 (value 5) as LAST:
coins = balloons[0] * balloons[3] * balloons[5] = 1 * 5 * 1 = 5
total = 5 + dp[0][3] + dp[3][5]
Try k=4 (value 8) as LAST:
coins = balloons[0] * balloons[4] * balloons[5] = 1 * 8 * 1 = 8
total = 8 + dp[0][4] + dp[4][5]
Result: dp[0][5] = 167
# Pattern 2: Inclusive Boundaries Variant
Some interval DP problems use inclusive boundaries where dp[i][j] includes elements i and j.
Python Implementation:
def maxCoins_inclusive(nums):
"""Alternative: dp[i][j] includes balloons i through j"""
n = len(nums)
balloons = [1] + nums + [1]
dp = [[0] * (n + 2) for _ in range(n + 2)]
# Iterate through window lengths (len) from 1 to n
for length in range(1, n + 1):
for left in range(1, n - length + 2):
right = left + length - 1
# Try every balloon k in [left, right] as LAST to burst
for k in range(left, right + 1):
coins = (dp[left][k - 1] + dp[k + 1][right] +
balloons[left - 1] * balloons[k] * balloons[right + 1])
dp[left][right] = max(dp[left][right], coins)
return dp[1][n]
# Top-Down (Memoization) Approach
def maxCoins_topdown(nums):
"""Top-down with memoization"""
balloons = [1] + nums + [1]
memo = {}
def dp(left, right):
"""Max coins from bursting balloons between left and right (exclusive)"""
if left + 1 == right: # No balloons between left and right
return 0
if (left, right) in memo:
return memo[(left, right)]
max_coins = 0
# Try each balloon k as the last to burst
for k in range(left + 1, right):
coins = (balloons[left] * balloons[k] * balloons[right] +
dp(left, k) + dp(k, right))
max_coins = max(max_coins, coins)
memo[(left, right)] = max_coins
return max_coins
return dp(0, len(balloons) - 1)
Java Top-Down:
public int maxCoins(int[] nums) {
int n = nums.length;
int[] balloons = new int[n + 2];
balloons[0] = balloons[n + 1] = 1;
for (int i = 0; i < n; i++) {
balloons[i + 1] = nums[i];
}
int[][] dp = new int[n + 2][n + 2];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
dp[i][j] = -1; // -1 means not computed yet
}
}
return burst(balloons, 0, n + 1, dp);
}
private int burst(int[] balloons, int left, int right, int[][] dp) {
if (left + 1 == right) return 0; // No balloons between left and right
if (dp[left][right] != -1) {
return dp[left][right];
}
int maxCoins = 0;
for (int k = left + 1; k < right; k++) {
int coins = balloons[left] * balloons[k] * balloons[right];
coins += burst(balloons, left, k, dp) + burst(balloons, k, right, dp);
maxCoins = Math.max(maxCoins, coins);
}
dp[left][right] = maxCoins;
return maxCoins;
}
# Key Characteristics of This Pattern
| Aspect | Detail |
|---|---|
| State Definition | dp[i][j] = optimal value for interval (i, j) or [i, j] |
| Loop Order | Length (outer) β Left boundary β Split point k |
| Transition | Try each k as the last element processed |
| Time Complexity | O(nΒ³) - three nested loops |
| Space Complexity | O(nΒ²) - 2D DP table |
| Key Insight | Process elements in reverse order of dependency |
# Common Problems Using This Pattern
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Burst Balloons | 312 | Last balloon to burst | Hard |
| Matrix Chain Multiplication | N/A | Last matrix multiply | Classic |
| Minimum Cost to Merge Stones | 1000 | Last merge operation | Hard |
| Remove Boxes | 546 | Last box to remove | Hard |
| Palindrome Partitioning II | 132 | Min cuts (variant) | Hard |
| Strange Printer | 664 | Last character to print | Hard |
# Pattern Recognition Checklist
Use this interval DP pattern when:
- β Problem involves processing elements in an array/sequence
- β Order of operations affects the result
- β Subproblems become independent after choosing an operation
- β Optimal solution can be built from optimal subproblems
- β Keywords: βmergeβ, βburstβ, βremoveβ, βsplitβ, βmultiplyβ
# Common Mistakes to Avoid
-
Thinking βfirstβ instead of βlastβ:
- β βWhich balloon to burst first?β β Neighbors change, dependencies unclear
- β βWhich balloon to burst last?β β Subproblems are independent
-
Wrong boundary handling:
- Add explicit boundaries (like
[1, ...nums..., 1]) to simplify edge cases - Decide if boundaries are inclusive or exclusive
- Add explicit boundaries (like
-
Off-by-one errors:
- Be consistent:
dp[i][j]means(i, j)exclusive or[i, j]inclusive - Adjust loop ranges accordingly
- Be consistent:
-
Incorrect loop order:
- Always build from smaller intervals to larger ones
- Length must be the outermost loop
# Complexity Analysis
Time Complexity: O(nΒ³)
- Outer loop (length): O(n)
- Middle loop (left boundary): O(n)
- Inner loop (split point k): O(n)
- Each cell takes O(n) time to compute
Space Complexity: O(nΒ²)
- 2D DP table of size
(n+2) Γ (n+2) - Can be optimized in some cases, but generally requires O(nΒ²)
Reference: See leetcode_java/src/main/java/LeetCodeJava/DynamicProgramming/BurstBalloons.java for multiple implementation variants.
# Template 4: 0/1 Knapsack
def knapsack_01(weights, values, capacity):
"""0/1 Knapsack problem"""
n = len(weights)
# dp[i][w] = max value with first i items, capacity w
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
# Don't take item i-1
dp[i][w] = dp[i-1][w]
# Take item i-1 if possible
if weights[i-1] <= w:
dp[i][w] = max(dp[i][w],
dp[i-1][w-weights[i-1]] + values[i-1])
return dp[n][capacity]
# Template 5: State Machine DP
def state_machine_dp(prices, fee=0):
"""DP with multiple states (stock problems)"""
if not prices:
return 0
n = len(prices)
# States: hold stock, not hold stock
hold = -prices[0]
cash = 0
for i in range(1, n):
# Transition between states
prev_hold = hold
hold = max(hold, cash - prices[i]) # Buy
cash = max(cash, prev_hold + prices[i] - fee) # Sell
return cash
# Template 5-2: State Machine DP with Cooldown (LC 309 Pattern)
def state_machine_with_cooldown(prices):
"""
DP with 3 states for stock with cooldown
State Definition:
- hold: Currently holding a stock
- sold: Just sold a stock today (enters cooldown)
- rest: In cooldown or doing nothing (not holding stock)
Key Constraint: After selling, must cooldown for 1 day before buying again
"""
if not prices:
return 0
# Initialize states
hold = -prices[0] # Buy on day 0
sold = 0 # Can't sell on day 0
rest = 0 # Doing nothing
for i in range(1, len(prices)):
prev_sold = sold
# State transitions
# 1. SOLD: Sell today (must have held yesterday)
sold = hold + prices[i]
# 2. HOLD: Either continue holding OR buy today (after cooldown)
hold = max(hold, rest - prices[i])
# 3. REST: Either rest again OR just finished cooldown from yesterday's sale
rest = max(rest, prev_sold)
# Max profit when not holding stock
return max(sold, rest)
State Transition Diagram for LC 309:
βββββββββββββββββββββββββββββββββββββββββββ
β State Machine Flow β
βββββββββββββββββββββββββββββββββββββββββββ
buy sell cooldown
REST βββββ HOLD βββββ SOLD ββββββ REST
β β
βββββββββββββββββββββββββββββββββββββ
Transitions:
β’ REST β HOLD: Buy stock (rest - price)
β’ HOLD β SOLD: Sell stock (hold + price)
β’ SOLD β REST: Cooldown (no transaction)
β’ REST β REST: Do nothing (rest)
β’ HOLD β HOLD: Keep holding (hold)
Key Insights:
- 3 States vs 2 States: Unlike simple stock problems (buy/sell), this needs 3 states due to cooldown
- Cooldown Enforcement:
reststate ensures you canβt buy immediately after selling - Space Optimization: Can use O(1) space with 3 variables instead of 2D array
- Critical Transition:
hold = max(hold, rest - prices[i])- can only buy after rest, not after sold
# Template 6: Top-Down Memoization
def top_down_dp(nums):
"""Top-down DP with memoization"""
memo = {}
def dp(i):
# Base case
if i < 0:
return 0
if i == 0:
return nums[0]
# Check memo
if i in memo:
return memo[i]
# Recurrence relation
result = max(dp(i-1), dp(i-2) + nums[i])
memo[i] = result
return result
return dp(len(nums) - 1)
# Template 7: String DP (Edit Distance / Levenshtein Distance)
Problem: LC 72 - Edit Distance Given two strings word1 and word2, find the minimum number of operations required to convert word1 to word2. Operations allowed: Insert, Delete, Replace (each counts as 1 step)
Core Pattern: Two-String Grid DP with three transition choices
State Definition:
dp[i][j]= minimum operations to convertword1[0...i-1]toword2[0...j-1]
Base Cases:
dp[i][0] = i(delete all i characters from word1)dp[0][j] = j(insert all j characters to reach word2)
Transition:
- If
word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1](no operation needed) - Else:
dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])dp[i-1][j] + 1: Delete from word1dp[i][j-1] + 1: Insert into word1dp[i-1][j-1] + 1: Replace in word1
Python Implementation:
def string_dp(s1, s2):
"""String DP for edit distance problems"""
m, n = len(s1), len(s2)
# dp[i][j] = min operations to convert s1[:i] to s2[:j]
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Initialize base cases
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] # No operation needed
else:
dp[i][j] = 1 + min(
dp[i-1][j], # Delete
dp[i][j-1], # Insert
dp[i-1][j-1] # Replace
)
return dp[m][n]
Java Implementation (Alternative indexing style):
// LC 72: Edit Distance
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];
// Base cases: converting empty string
for(int i = 0; i <= m; i++) {
dp[i][0] = i; // Delete all characters
}
for(int i = 0; i <= n; i++) {
dp[0][i] = i; // Insert all characters
}
// Fill DP table
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(word1.charAt(i) == word2.charAt(j)) {
// Characters match - no operation needed
dp[i + 1][j + 1] = dp[i][j];
} else {
// Try all three operations and take minimum
int replace = dp[i][j]; // Replace word1[i] with word2[j]
int delete = dp[i][j + 1]; // Delete word1[i]
int insert = dp[i + 1][j]; // Insert word2[j]
dp[i + 1][j + 1] = Math.min(replace, Math.min(delete, insert)) + 1;
}
}
}
return dp[m][n];
}
Key Insights:
-
Indexing Styles: Two common approaches:
- Style 1 (Python above): Loop
ifrom 1 to m, accesss1[i-1], store indp[i][j] - Style 2 (Java above): Loop
ifrom 0 to m-1, accessword1[i], store indp[i+1][j+1]
- Style 1 (Python above): Loop
-
The Three Operations:
dp[i-1][j] dp[i-1][j-1] dp[i-1][j] dp[i-1][j-1] β (delete) β (replace) dp[i][j-1] dp[i][j] => dp[i][j-1] β dp[i][j] (insert) -
Complexity: Time O(mΓn), Space O(mΓn) (optimizable to O(n))
Example Trace: word1 = "horse", word2 = "ros"
"" r o s
"" 0 1 2 3
h 1 1 2 3
o 2 2 1 2
r 3 2 2 2
s 4 3 3 2
e 5 4 4 3
Result: 3 operations (delete 'h', delete 'r', delete 'e')
Or: replace 'h'β'r', remove 'r', remove 'e'
Related Problems:
- LC 583: Delete Operation for Two Strings (variant: only delete allowed)
- LC 712: Minimum ASCII Delete Sum (variant: minimize ASCII sum)
- LC 1143: Longest Common Subsequence (maximize matches instead of minimize edits)
File References:
- Java Implementation:
ref_code/interviews-master/leetcode/string/EditDistance.java - See also: Two-String Grid Pattern section below for more context
# Template 8: Longest Common Subsequence (LCS)
def lcs_dp(text1, text2):
"""LCS pattern for string matching"""
m, n = len(text1), len(text2)
# dp[i][j] = LCS length of text1[:i] and text2[:j]
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
# Template 9: State Compression (Bitmask DP)
def state_compression_dp(graph):
"""Traveling Salesman Problem using bitmask DP"""
n = len(graph)
# dp[mask][i] = min cost to visit all cities in mask, ending at city i
dp = [[float('inf')] * n for _ in range(1 << n)]
dp[1][0] = 0 # Start at city 0
for mask in range(1 << n):
for u in range(n):
if not (mask & (1 << u)):
continue
for v in range(n):
if mask & (1 << v):
continue
new_mask = mask | (1 << v)
dp[new_mask][v] = min(dp[new_mask][v],
dp[mask][u] + graph[u][v])
# Return to starting city
return min(dp[(1 << n) - 1][i] + graph[i][0] for i in range(1, n))
# Template 10: Palindrome DP
def palindrome_dp(s):
"""Check palindromic substrings"""
n = len(s)
# dp[i][j] = True if s[i:j+1] is palindrome
dp = [[False] * n for _ in range(n)]
# Single characters are palindromes
for i in range(n):
dp[i][i] = True
# Check 2-character palindromes
for i in range(n - 1):
if s[i] == s[i + 1]:
dp[i][i + 1] = True
# Check longer palindromes
for length in range(3, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j] and dp[i + 1][j - 1]:
dp[i][j] = True
return dp
Key Palindrome DP Pattern:
Core Transition Equation:
dp[i][j] = true
if:
s[i] == s[j] AND
(j - i <= 2 OR dp[i + 1][j - 1])
Explanation:
- dp[i][j] represents whether substring s[i...j] is a palindrome
- s[i] == s[j]: Characters at both ends must match
- j - i <= 2: Handles base cases (length 1, 2, or 3 substrings)
- dp[i + 1][j - 1]: Inner substring must also be a palindrome
Example: For "babad"
- dp[0][2] = true because s[0]='b' == s[2]='b' AND j-i=2 (length 3)
- dp[1][3] = true because s[1]='a' == s[3]='a' AND dp[2][2]=true
# Template 11: Fibonacci-like Patterns
def fibonacci_variants():
"""Common Fibonacci-like patterns"""
# 1. Classic Fibonacci
def fibonacci(n):
if n <= 1:
return n
prev2, prev1 = 0, 1
for _ in range(2, n + 1):
current = prev1 + prev2
prev2, prev1 = prev1, current
return prev1
# 2. Climbing Stairs
def climbStairs(n):
if n <= 2:
return n
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
current = prev1 + prev2
prev2, prev1 = prev1, current
return prev1
# 3. House Robber
def rob(nums):
if not nums:
return 0
if len(nums) <= 2:
return max(nums)
prev2, prev1 = nums[0], max(nums[0], nums[1])
for i in range(2, len(nums)):
current = max(prev1, prev2 + nums[i])
prev2, prev1 = prev1, current
return prev1
# Comprehensive Pattern Analysis
# 1D DP Patterns
| Problem Type | Recurrence | Example | Time | Space |
|---|---|---|---|---|
| Fibonacci | dp[i] = dp[i-1] + dp[i-2] | LC 70 Climbing Stairs | O(n) | O(1) |
| House Robber | dp[i] = max(dp[i-1], dp[i-2] + nums[i]) | LC 198 House Robber | O(n) | O(1) |
| Decode Ways | dp[i] = dp[i-1] + dp[i-2] (if valid) | LC 91 Decode Ways | O(n) | O(1) |
| Word Break | dp[i] = OR(dp[j] AND s[j:i] in dict) | LC 139 Word Break | O(nΒ²) | O(n) |
Template for 1D Linear DP:
def linear_dp_optimized(nums):
"""Space-optimized 1D DP"""
n = len(nums)
if n == 0:
return 0
if n == 1:
return nums[0]
# Only need previous two states
prev2 = nums[0]
prev1 = max(nums[0], nums[1])
for i in range(2, n):
current = max(prev1, prev2 + nums[i])
prev2, prev1 = prev1, current
return prev1
# 2D DP Patterns
| Problem Type | Recurrence | Example | Time | Space |
|---|---|---|---|---|
| Unique Paths | dp[i][j] = dp[i-1][j] + dp[i][j-1] | LC 62 Unique Paths | O(mΓn) | O(n) |
| Min Path Sum | dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j] | LC 64 Min Path Sum | O(mΓn) | O(n) |
| LCS | dp[i][j] = dp[i-1][j-1] + 1 if match else max(β¦) | LC 1143 LCS | O(mΓn) | O(n) |
| Edit Distance | dp[i][j] = min(insert, delete, replace) | LC 72 Edit Distance | O(mΓn) | O(n) |
Template for 2D DP with Space Optimization:
def grid_dp_optimized(grid):
"""Space-optimized 2D DP"""
if not grid or not grid[0]:
return 0
m, n = len(grid), len(grid[0])
# Only need previous row
prev = [0] * n
prev[0] = grid[0][0]
# Initialize first row
for j in range(1, n):
prev[j] = prev[j-1] + grid[0][j]
# Process remaining rows
for i in range(1, m):
curr = [0] * n
curr[0] = prev[0] + grid[i][0]
for j in range(1, n):
curr[j] = min(prev[j], curr[j-1]) + grid[i][j]
prev = curr
return prev[n-1]
# Knapsack Patterns
| Variant | State Definition | Transition | Example |
|---|---|---|---|
| 0/1 Knapsack | dp[i][w] = max value with i items, weight w | dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight[i]] + value[i]) | LC 416 Partition |
| Unbounded | dp[w] = max value with weight w | dp[w] = max(dp[w], dp[w-weight[i]] + value[i]) | LC 322 Coin Change |
| Multiple | dp[i][w] with count constraint | Similar to 0/1 but with quantity limits | LC 1449 Form Largest Number |
Space-Optimized Knapsack:
def knapsack_optimized(weights, values, capacity):
"""1D array knapsack"""
dp = [0] * (capacity + 1)
for i in range(len(weights)):
# Iterate backwards to avoid using updated values
for w in range(capacity, weights[i] - 1, -1):
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[capacity]
# Critical Pattern: Loop Order in Unbounded Knapsack (Combinations vs Permutations)
π Key Insight: In unbounded knapsack problems (like Coin Change), the order of nested loops determines whether you count combinations or permutations.
# π― Ultimate Cheat Sheet: When to Use Which Pattern
| When Problem Says⦠| Pattern to Use | Loop Order | Direction | DP Transition | Example LC |
|---|---|---|---|---|---|
| βCount waysβ + order doesnβt matter | Combinations | Item β Target | Forward | dp[i] += dp[i-item] |
518 |
| βCount waysβ + order matters | Permutations | Target β Item | Forward | dp[i] += dp[i-item] |
377 |
| βUse each item onceβ + find max/min | 0/1 Knapsack | Item β Capacity | Backward | dp[w] = max(dp[w], ...) |
416 |
| βUnlimited itemsβ + find max/min | Unbounded Knapsack | Item β Capacity | Forward | dp[i] = min(dp[i], ...) |
322 |
β‘ Quick Recognition (θ―ε«):
- See βdifferent sequencesβ or βdifferent orderingsβ β Permutations (Target outer)
- See βnumber of combinationsβ or βunique waysβ β Combinations (Item outer)
- See βeach element at most onceβ β 0/1 Knapsack (Backward)
- See βminimum coinsβ or βfewest itemsβ β Unbounded Knapsack (Forward)
# π Visual Summary: The Four Core Patterns
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DP KNAPSACK PATTERN MATRIX β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
COUNT WAYS FIND MIN/MAX
ββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β β β
ORDER MATTERS? β PERMUTATIONS β Not typically used β
(Yes) β LC 377 β (Use Permutations β
β TargetβItem β for counting) β
β Forward β β
ββββββββββββββββββββΌβββββββββββββββββββββββββββ€
β β β
ORDER DOESN'T β COMBINATIONS β UNBOUNDED KNAPSACK β
MATTER β LC 518 β LC 322 β
(No) β ItemβTarget β ItemβCapacity β
β Forward β Forward β
ββββββββββββββββββββΌβββββββββββββββββββββββββββ€
β β β
USE EACH ONCE β Not typical β 0/1 KNAPSACK β
(Constraint) β (Can adapt β LC 416 β
β 0/1 pattern) β ItemβCapacity β
β β BACKWARD β οΈ β
ββββββββββββββββββββ΄βββββββββββββββββββββββββββ
Legend:
ItemβTarget = Outer loop: items, Inner loop: target
TargetβItem = Outer loop: target, Inner loop: items
Forward = Inner loop: i to target (allows reuse)
BACKWARD β οΈ = Inner loop: target to i (prevents reuse)
π― Decision Flow:
Start
β
ββ Question asks "count ways"?
β β
β ββ YES β Order matters?
β β ββ YES β Permutations (TargetβItem) [LC 377]
β β ββ NO β Combinations (ItemβTarget) [LC 518]
β β
β ββ NO β Question asks "min/max"?
β β
β ββ Each item once?
β β ββ YES β 0/1 Knapsack (BACKWARD) [LC 416]
β β ββ NO β Unbounded (FORWARD) [LC 322]
β β
β ββ Unknown β Check problem constraints
# π Master Pattern Table: DP Transitions by Problem Type
| Pattern Type | Loop Order | DP Transition | What It Counts | Mental Model | Example | Result |
|---|---|---|---|---|---|---|
| COMBINATIONS (Order doesnβt matter) |
ITEM β TARGETfor item in items:Β Β for i in range(item, target+1): |
dp[i] += dp[i - item] |
Unique sets [1,2] = [2,1] |
βProcess all uses of item-1, then all uses of item-2β Forces canonical order |
LC 518 coins=[1,2] amount=3 |
2 ways {1,1,1} {1,2} |
| PERMUTATIONS (Order matters) |
TARGET β ITEMfor i in range(1, target+1):Β Β for item in items: |
dp[i] += dp[i - item] |
Different orderings [1,2] β [2,1] |
βFor each target, try every item as the βlastβ oneβ Allows any order |
LC 377 nums=[1,2] target=3 |
3 ways {1,1,1} {1,2} {2,1} |
| 0/1 KNAPSACK (Use each once) |
ITEM β CAPACITY (backwards) for item in items:Β Β for w in range(W, weight-1, -1): |
dp[w] = max(dp[w],dp[w-weight[i]] + value[i]) |
Max/min with constraint Each item used β€ 1 time |
βMust iterate backwards to avoid using same item twice in one passβ | LC 416 Partition Subset |
True/False or Max value |
| UNBOUNDED KNAPSACK (Unlimited use) |
ITEM β CAPACITY (forwards) for item in items:Β Β for w in range(weight, W+1): |
dp[w] = max(dp[w],dp[w-weight[i]] + value[i]) |
Max/min without constraint Each item used unlimited |
βIterate forwards - can use updated values in same passβ | LC 322 Coin Change (min coins) |
Min count or -1 |
# π» Code Templates by Pattern
// ============================================
// PATTERN 1: COMBINATIONS (Item β Target)
// ============================================
// LC 518: Coin Change II
public int countCombinations(int target, int[] items) {
int[] dp = new int[target + 1];
dp[0] = 1; // Base: one way to make 0
// OUTER: Items/Coins
for (int item : items) {
// INNER: Target/Amount (forward)
for (int i = item; i <= target; i++) {
dp[i] += dp[i - item]; // β Same transition
}
}
return dp[target];
}
// ============================================
// PATTERN 2: PERMUTATIONS (Target β Item)
// ============================================
// LC 377: Combination Sum IV
public int countPermutations(int target, int[] items) {
int[] dp = new int[target + 1];
dp[0] = 1; // Base: one way to make 0
// OUTER: Target/Amount
for (int i = 1; i <= target; i++) {
// INNER: Items/Coins
for (int item : items) {
if (i >= item) {
dp[i] += dp[i - item]; // β Same transition
}
}
}
return dp[target];
}
// ============================================
// PATTERN 3: 0/1 KNAPSACK (Item β Capacity BACKWARDS)
// ============================================
// LC 416: Partition Equal Subset Sum
public boolean canPartition(int[] nums, int target) {
boolean[] dp = new boolean[target + 1];
dp[0] = true; // Base: can make 0
// OUTER: Items
for (int num : nums) {
// INNER: Capacity (BACKWARDS to prevent reuse)
for (int w = target; w >= num; w--) {
dp[w] = dp[w] || dp[w - num]; // β Different transition (OR)
}
}
return dp[target];
}
// ============================================
// PATTERN 4: UNBOUNDED KNAPSACK (Item β Capacity FORWARDS)
// ============================================
// LC 322: Coin Change (minimum coins)
public int minCoins(int target, int[] coins) {
int[] dp = new int[target + 1];
Arrays.fill(dp, target + 1); // Infinity
dp[0] = 0; // Base: 0 coins for 0 amount
// OUTER: Items/Coins
for (int coin : coins) {
// INNER: Target (FORWARDS allows reuse)
for (int i = coin; i <= target; i++) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1); // β Different transition (MIN)
}
}
return dp[target] > target ? -1 : dp[target];
}
π Key Observations:
-
Same DP Transition (
dp[i] += dp[i - item]) for:- Combinations (Item β Target)
- Permutations (Target β Item)
- Only difference: Loop order!
-
Different DP Transitions for:
- 0/1 Knapsack:
dp[w] = dp[w] || dp[w - num](boolean OR or MAX) - Unbounded Knapsack:
dp[i] = min(dp[i], dp[i - coin] + 1)(MIN/MAX)
- 0/1 Knapsack:
-
Direction Matters for knapsack:
- Backwards β prevents reuse (0/1)
- Forwards β allows reuse (unbounded)
# π― Pattern Selection Decision Tree
Question: What does the problem ask for?
ββ "Count number of ways/combinations to reach target"
β ββ Order matters? (e.g., [1,2] β [2,1])
β β ββ YES β Use PERMUTATIONS pattern (Target β Item)
β β β Example: LC 377 Combination Sum IV
β β ββ NO β Use COMBINATIONS pattern (Item β Target)
β β Example: LC 518 Coin Change II
β β
β ββ Can reuse items?
β ββ YES β Unbounded, iterate forwards
β ββ NO β 0/1 Knapsack, iterate backwards
β
ββ "Find minimum/maximum value"
ββ Can reuse items?
β ββ YES β Unbounded Knapsack (forwards)
β β Example: LC 322 Coin Change (min coins)
β ββ NO β 0/1 Knapsack (backwards)
β Example: LC 416 Partition Equal Subset Sum
β
ββ Always use (Item β Capacity) order
# β‘ Quick Reference: Loop Order β Problem Type
| Outer Loop | Inner Loop | Pattern Name | Use When | Problems |
|---|---|---|---|---|
| Items/Coins | Target/Amount | Combinations | Count unique sets (order doesnβt matter) | LC 518 |
| Target/Amount | Items/Coins | Permutations | Count sequences (order matters) | LC 377 |
| Items (backwards) | Capacity | 0/1 Knapsack | Each item used once, find max/min | LC 416, 494 |
| Items (forwards) | Capacity | Unbounded Knapsack | Unlimited items, find max/min | LC 322 |
# Quick Comparison Table
| Aspect | Combinations (LC 518) | Permutations (LC 377) |
|---|---|---|
| Loop Order | Coin β Amount | Amount β Coin |
| Order Matters? | β No: [1,2] = [2,1] | β Yes: [1,2] β [2,1] |
| Problem Type | Coin Change II | Combination Sum IV |
| Outer Loop | for (int coin : coins) |
for (int i = 1; i <= target; i++) |
| Inner Loop | for (int i = coin; i <= amount; i++) |
for (int num : nums) |
| Example | amount=3, coins=[1,2] β 2 ways | target=3, nums=[1,2] β 3 ways |
# Pattern 1: Combinations (Outer: Coins, Inner: Amount)
// LC 518: Coin Change II - Count combinations
// Example: [1,2] and [2,1] are the SAME combination
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1; // Base case: 1 way to make amount 0
// OUTER LOOP: Iterate through each coin
// This ensures we process all uses of one coin before moving to the next,
// which prevents duplicate combinations like [1,2] and [2,1].
for (int coin : coins) {
// INNER LOOP: Update dp table for all amounts reachable by this coin
for (int i = coin; i <= amount; i++) {
// Number of ways to make amount 'i' is:
// (Current ways) + (Ways to make 'i - coin')
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
Why This Works:
- Process coins one at a time (e.g., first all 1s, then all 2s, then all 5s)
- By the time you use coin
2, youβve finished all calculations with coin1 - Impossible to place a
1after a2, forcing non-decreasing order - Result: Only combinations (order doesnβt matter)
Example Trace: coins = [1,2], amount = 3
After coin 1: dp = [1, 1, 1, 1] // {}, {1}, {1,1}, {1,1,1}
After coin 2: dp = [1, 1, 2, 2] // + {2}, {1,2}
Result: 2 combinations β {1,1,1}, {1,2}
# Pattern 2: Permutations (Outer: Amount, Inner: Coins)
// LC 377: Combination Sum IV - Count permutations
// Example: [1,2] and [2,1] are DIFFERENT permutations
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
// OUTER LOOP: Iterate through each amount
// For each amount, try all coins to see which was "last added"
for (int i = 1; i <= target; i++) {
// INNER LOOP: Try each coin for current amount
for (int num : nums) {
if (i >= num) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
Why This Counts Permutations:
- For each amount, ask: βWhat was the last coin I added?β
- Every coin can be the βlastβ coin at each step
- Result: Permutations (order matters)
Example Trace: nums = [1,2], target = 3
dp[1]: Use 1 β [1] (1 way)
dp[2]: Use 1 β [1,1], Use 2 β [2] (2 ways)
dp[3]: From dp[2] add 1 β [1,1,1], [2,1]
From dp[1] add 2 β [1,2]
Result: 3 permutations β {1,1,1}, {1,2}, {2,1}
# Comparison Table
| Loop Order | Result Type | Problem Example | Use Case |
|---|---|---|---|
| Outer: Coin Inner: Amount |
Combinations (Order doesnβt matter) |
LC 518 Coin Change II | Count unique coin combinations |
| Outer: Amount Inner: Coin |
Permutations (Order matters) |
LC 377 Combination Sum IV | Count different orderings |
# π₯ Side-by-Side Code Comparison
LC 518: Coin Change II (Combinations)
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1; // Base: 1 way to make 0
// CRITICAL: Coin outer loop = COMBINATIONS
for (int coin : coins) { // β Process coins one by one
for (int i = coin; i <= amount; i++) { // β Update all amounts for this coin
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
// Example: amount=3, coins=[1,2]
// Result: 2 combinations
// {1,1,1}, {1,2} (Note: [1,2] and [2,1] counted as same)
LC 377: Combination Sum IV (Permutations)
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1; // Base: 1 way to make 0
// CRITICAL: Amount outer loop = PERMUTATIONS
for (int i = 1; i <= target; i++) { // β Process each amount
for (int num : nums) { // β Try every number for this amount
if (i >= num) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
// Example: target=3, nums=[1,2]
// Result: 3 permutations
// {1,1,1}, {1,2}, {2,1} (Note: [1,2] and [2,1] are different)
# π Detailed Trace Comparison: Why Loop Order Matters
Example: nums/coins = [1, 2], target/amount = 3
LC 518 (Combinations - Coin Outer):
Initialize: dp = [1, 0, 0, 0]
Process coin 1:
i=1: dp[1] += dp[0] = 1 β [1, 1, 0, 0] // ways: {1}
i=2: dp[2] += dp[1] = 1 β [1, 1, 1, 0] // ways: {1,1}
i=3: dp[3] += dp[2] = 1 β [1, 1, 1, 1] // ways: {1,1,1}
Process coin 2:
i=2: dp[2] += dp[0] = 1+1=2 β [1, 1, 2, 1] // ways: {1,1}, {2}
i=3: dp[3] += dp[1] = 1+1=2 β [1, 1, 2, 2] // ways: {1,1,1}, {1,2}
// Note: Can't get {2,1} because
// all coin-1 uses are done before coin-2
Final: dp[3] = 2 β
Only {1,1,1} and {1,2}
LC 377 (Permutations - Amount Outer):
Initialize: dp = [1, 0, 0, 0]
i=1 (building sum 1):
Try 1: dp[1] += dp[0] = 1 β [1, 1, 0, 0] // ways: {1}
Try 2: skip (2 > 1)
i=2 (building sum 2):
Try 1: dp[2] += dp[1] = 1 β [1, 1, 1, 0] // {1} + 1 = {1,1}
Try 2: dp[2] += dp[0] = 1+1=2 β [1, 1, 2, 0] // {} + 2 = {2}
i=3 (building sum 3):
Try 1: dp[3] += dp[2] = 2 β [1, 1, 2, 2] // {1,1} + 1 = {1,1,1}
// {2} + 1 = {2,1} β
Try 2: dp[3] += dp[1] = 2+1=3 β [1, 1, 2, 3] // {1} + 2 = {1,2} β
Final: dp[3] = 3 β
All three: {1,1,1}, {1,2}, {2,1}
Key Insight:
- LC 518 (Coin Outer): Once you finish processing coin-1, you never revisit it. This forces a canonical order (all 1s before all 2s), preventing duplicates like {1,2} and {2,1}.
- LC 377 (Amount Outer): For each sum, you ask βwhat was the last number added?β Every number can be βlastβ, allowing both {1,2} and {2,1}.
# When to Use Which
Use Combinations (Coin β Amount) when:
- Problem asks for βnumber of waysβ without considering order
- [1,2,5] and [2,1,5] should be counted once
- Keywords: βcombinationsβ, βunique setsβ
Use Permutations (Amount β Coin) when:
- Problem asks for different sequences/orderings
- [1,2] and [2,1] should be counted separately
- Keywords: βpermutationsβ, βdifferent orderingsβ, βsequencesβ
# Complete Java Example: LC 518 Coin Change II
public int change(int amount, int[] coins) {
// dp[i] = total number of combinations that make up amount i
int[] dp = new int[amount + 1];
// Base case: There is exactly 1 way to make 0 amount (empty set)
dp[0] = 1;
// CRITICAL: Coin outer loop = COMBINATIONS
for (int coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
Test Cases:
Input: amount = 5, coins = [1,2,5]
Output: 4
Combinations: {5}, {2,2,1}, {2,1,1,1}, {1,1,1,1,1}
Input: amount = 3, coins = [2]
Output: 0
Explanation: Cannot make 3 with only coins of 2
# π Problem References
| Problem | LC # | Loop Order | What it Counts | File Reference |
|---|---|---|---|---|
| Coin Change II | 518 | Coin β Amount | Combinations (order doesnβt matter) | leetcode_java/.../CoinChange2.java |
| Combination Sum IV | 377 | Amount β Coin | Permutations (order matters) | leetcode_java/.../CombinationSumIV.java |
π‘ Memory Trick:
- βCoin firstβ = Combinations (both start with βCβ)
- βAmount firstβ = Arrangements/Permutations (both start with βAβ)
# π Final Summary: Complete Pattern Comparison
| Aspect | LC 518: Coin Change II (Combinations) |
LC 377: Combination Sum IV (Permutations) |
|---|---|---|
| What it counts | Unique sets (order doesnβt matter) | Different sequences (order matters) |
| Example | [1,2] = [2,1] (same) | [1,2] β [2,1] (different) |
| Outer Loop | for (int coin : coins) |
for (int i = 1; i <= target; i++) |
| Inner Loop | for (int i = coin; i <= amount; i++) |
for (int num : nums) |
| DP Transition | dp[i] += dp[i - coin] |
dp[i] += dp[i - num] |
| Base Case | dp[0] = 1 |
dp[0] = 1 |
| Result for nums=[1,2], target=3 |
2 combinations: {1,1,1}, {1,2} |
3 permutations: {1,1,1}, {1,2}, {2,1} |
| Why it works | Processing coin-1 completely before coin-2 forces canonical order β no {2,1} | For each sum, try every number as βlastβ β allows all orderings |
| File Reference | CoinChange2.java |
CombinationSumIV.java |
π₯ The ONLY Difference:
// LC 518: Combinations
for (int coin : coins) // β ITEM OUTER
for (int i = coin; i <= amount; i++)
// LC 377: Permutations
for (int i = 1; i <= target; i++) // β TARGET OUTER
for (int num : nums)
Both use the EXACT SAME transition: dp[i] += dp[i - item]
# Critical Pattern: Understanding if (i - coin >= 0) in Coin Change DP
π The Question: Why use if (i >= coin) instead of if (i == coin)?
This is a fundamental concept in understanding how Dynamic Programming builds on previously solved subproblems.
# The Short Answer
i == coinonly checks if a single coin matches the amounti >= coinchecks if a coin can be combined with a previous sum to reach the amount
# The Logic of i - coin > 0
When we calculate dp[i], we arenβt just looking for one coin that equals i. We are looking for a coin coin that, when subtracted from i, leaves a remainder that we already know how to solve.
i: The total amount we are trying to reach right nowcoin: The value of the coin we just picked upi - coin: The βremainderβ or the amount left over
If i - coin > 0, it means we still need more coins to reach i. But since we are filling the table from 0 to amount, we have already calculated the best way to make the remainder i - coin.
The DP looks back at dp[i - coin] to reuse that solution!
# A Concrete Example
Imagine coins = [2] and we want to find dp[4] (how to make 4 cents).
- We try the coin
coin = 2 i - coinis4 - 2 = 2- Since
2 > 0, we donβt stop. We look atdp[2] - We already calculated
dp[2] = 1(it took one 2-cent coin to make 2 cents) - So,
dp[4] = dp[2] + 1 = 2
If we only used if (i - coin == 0):
- We would only ever find that
dp[2] = 1 - When we got to
dp[4], the condition4 - 2 == 0would be false - We would incorrectly conclude that we canβt make 4 cents!
# The Three Scenarios
When checking i - coin:
Result of i - coin |
Meaning | Action |
|---|---|---|
Negative (< 0) |
The coin is too big for this amount | Skip this coin |
Zero (== 0) |
This single coin matches the amount perfectly | dp[i] = 1 |
Positive (> 0) |
This coin fits, and we need to check the βremainderβ | dp[i] = dp[remainder] + 1 |
# π‘ Key Insight
The condition if (i >= coin) covers both the case where a coin matches exactly and the case where a coin is just one piece of a larger puzzle.
# Complete Example with Trace
Input: coins = [1,2,5], amount = 11
Setup:
- DP Array:
int[12](Indices 0 to 11) - Initialization:
dp[0] = 0, all others =12(our βInfinityβ)
Step-by-Step Trace:
Amounts 1 through 4:
- At
i=1: Only coin1fits (1 >= 1).dp[1] = dp[0] + 1 = 1 - At
i=2:- Coin
1:dp[2] = dp[1] + 1 = 2 - Coin
2:dp[2] = dp[0] + 1 = 1(Winner: Min is 1)
- Coin
- At
i=3:- Coin
1:dp[3] = dp[2] + 1 = 2 - Coin
2:dp[3] = dp[1] + 1 = 2 dp[3] = 2(e.g.,2+1or1+1+1)
- Coin
- At
i=4:- Coin
1:dp[4] = dp[3] + 1 = 3 - Coin
2:dp[4] = dp[2] + 1 = 2 dp[4] = 2(e.g.,2+2)
- Coin
Amount 5 (The first big jump):
- Coin
1:dp[5] = dp[4] + 1 = 3 - Coin
2:dp[5] = dp[3] + 1 = 3 - Coin
5:dp[5] = dp[0] + 1 = 1 - Result:
dp[5] = 1(Matches perfectly)
Amount 10:
- Coin
1:dp[10] = dp[9] + 1 = 4 - Coin
2:dp[10] = dp[8] + 1 = 4 - Coin
5:dp[10] = dp[5] + 1 = 2 - Result:
dp[10] = 2(this represents5+5)
The Final Goal: Amount 11:
-
Try Coin
1:- Remainder:
11 - 1 = 10 - Look up
dp[10]: It is2 - Calculation:
dp[11] = dp[10] + 1 = 3
- Remainder:
-
Try Coin
2:- Remainder:
11 - 2 = 9 - Look up
dp[9]: It is3(e.g.,5+2+2) - Calculation:
dp[11] = dp[9] + 1 = 4
- Remainder:
-
Try Coin
5:- Remainder:
11 - 5 = 6 - Look up
dp[6]: It is2(e.g.,5+1) - Calculation:
dp[11] = dp[6] + 1 = 3
- Remainder:
Final Comparison: dp[11] = min(3, 4, 3) = 3
# Why the remainder i - coin > 0 worked
When calculating for 11, the algorithm didnβt have to βre-solveβ how to make 10 or 6. It just looked at the table:
- βOh, I know the best way to make 10 is 2 coins (
5+5)β - βIf I add my 1 coin to that, I get 11 using 3 coins (
5+5+1)β
# Summary Table (Simplified)
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| dp[i] | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 3 | 2 | 3 |
# The DP Code Pattern
public int coinChange(int[] coins, int amount) {
if (amount == 0) return 0;
// dp[i] = min coins to make amount i
int[] dp = new int[amount + 1];
// Initialize with "Infinity" (amount + 1 is safe)
Arrays.fill(dp, amount + 1);
// Base case: 0 coins needed for 0 amount
dp[0] = 0;
// Iterate through every amount from 1 to amount
for (int i = 1; i <= amount; i++) {
// For each amount, try every coin
for (int coin : coins) {
// CRITICAL CONDITION: Check if coin fits
if (i >= coin) {
// DP equation: Min of (current value) OR
// (1 coin + coins needed for remainder)
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
// If value is still "Infinity", we couldn't reach it
return dp[amount] > amount ? -1 : dp[amount];
}
Reference: See leetcode_java/src/main/java/LeetCodeJava/DynamicProgramming/CoinChange.java:356-408 for detailed implementation.
# String DP Patterns
# The βTwo-String / Two-Sequence Gridβ Pattern π§©
This is one of the most important DP patterns for string problems. Once you recognize this pattern, a whole class of problems becomes much easier to solve.
Core Structure:
- Create a 2D array
dp[m+1][n+1]where:- Rows (i): Represent the prefix of String A (first i characters)
- Columns (j): Represent the prefix of String B (first j characters)
- Cell
dp[i][j]: Stores the answer for those two specific prefixes
Grid Movements (How to Choose the Move):
Think of the grid as a game where you move from (0,0) to (m,n):
- Diagonal Move (
dp[i-1][j-1]): You βuseβ or βmatchβ a character from both strings simultaneously - Vertical Move (
dp[i-1][j]): You βskipβ or βdeleteβ a character from String A - Horizontal Move (
dp[i][j-1]): You βskipβ or βinsertβ a character from String B
Pattern Comparison Table:
| Problem | Goal | Match Logic (s1[i-1] == s2[j-1]) |
Mismatch Logic | Key Insight |
|---|---|---|---|---|
| LC 1143: LCS | Longest common length | 1 + dp[i-1][j-1] (Diagonal + 1) |
max(dp[i-1][j], dp[i][j-1]) |
Take diagonal when match, else max of skip either string |
| LC 97: Interleaving String | Can s3 interleave s1+s2? | dp[i-1][j] || dp[i][j-1] |
false |
Check if we can form by taking from either string |
| LC 115: Distinct Subsequences | Count occurrences | dp[i-1][j-1] + dp[i-1][j] |
dp[i-1][j] |
Can either use match or skip s char |
| LC 72: Edit Distance | Min edits to match | dp[i-1][j-1] (No cost) |
1 + min(top, left, diagonal) |
No operation needed if match, else try all 3 operations |
| LC 583: Delete Operation | Min deletions to make equal | dp[i-1][j-1] |
1 + min(dp[i-1][j], dp[i][j-1]) |
Delete from either string |
| LC 712: Min ASCII Delete Sum | Min ASCII sum to make equal | dp[i-1][j-1] |
min(dp[i-1][j] + s1[i], dp[i][j-1] + s2[j]) |
Track ASCII costs |
The βEmpty Stringβ Base Case Pattern π‘
This is THE MOST IMPORTANT pattern in Two-String DP:
dp[0][0]: State where both strings are empty (usually0ortrue)- First row
dp[0][j]: String A is empty, only String B has characters - First column
dp[i][0]: String B is empty, only String A has characters
Why m+1 and n+1?
- The
+1gives us room for the βempty stringβ base case - Without this, transitions like
dp[i-1][j]would crash on the first character dp[i][j]represents using the firsticharacters of string 1 and firstjcharacters of string 2
Universal Template:
public int stringDP(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[][] dp = new int[m + 1][n + 1];
// Step 1: Initialize base cases (empty string states)
dp[0][0] = 0; // Both strings empty
// Initialize first row (s1 is empty)
for (int j = 1; j <= n; j++) {
dp[0][j] = initValueForEmptyS1(j);
}
// Initialize first column (s2 is empty)
for (int i = 1; i <= m; i++) {
dp[i][0] = initValueForEmptyS2(i);
}
// Step 2: Fill the DP table
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// NOTE: Use i-1 and j-1 to access string characters
if (s1.charAt(i-1) == s2.charAt(j-1)) {
// Characters match
dp[i][j] = transitionOnMatch(dp, i, j);
} else {
// Characters don't match
dp[i][j] = transitionOnMismatch(dp, i, j);
}
}
}
return dp[m][n];
}
Space Optimization Secret β‘
In every βTwo-Stringβ problem, you only ever look at:
- The current row (
dp[i][j]) - The row above (
dp[i-1][j]) - The diagonal (
dp[i-1][j-1])
This means you can always reduce space from O(mΓn) to O(n) by using:
- A 1D array for the previous row
- A variable to store the diagonal value
- Rolling updates as you process each row
Example Space-Optimized LCS:
public int longestCommonSubsequence(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[] prev = new int[n + 1];
for (int i = 1; i <= m; i++) {
int[] curr = new int[n + 1];
for (int j = 1; j <= n; j++) {
if (s1.charAt(i-1) == s2.charAt(j-1)) {
curr[j] = prev[j-1] + 1; // Diagonal
} else {
curr[j] = Math.max(prev[j], curr[j-1]); // Top or left
}
}
prev = curr; // Roll forward
}
return prev[n];
}
Quick Recognition Checklist β
Use βTwo-String Gridβ pattern when you see:
- [ ] Two strings/sequences as input
- [ ] Need to compare characters from both strings
- [ ] Answer depends on prefixes (first i chars of s1, first j chars of s2)
- [ ] Keywords: βcommonβ, βmatchingβ, βtransformβ, βinterleavingβ, βsubsequenceβ
Common Problems:
- LC 1143 (LCS) - Find longest common subsequence
- LC 72 (Edit Distance) - Minimum edits to transform
- LC 97 (Interleaving String) - Can s3 be formed by interleaving?
- LC 115 (Distinct Subsequences) - Count occurrences
- LC 583 (Delete Operation) - Min deletions to make equal
- LC 712 (Min ASCII Delete Sum) - Min ASCII cost to make equal
- LC 10 (Regular Expression Matching) - Pattern matching with * and .
- LC 44 (Wildcard Matching) - Pattern matching with * and ?
# Classic String DP Patterns (Detailed)
| Problem Type | Pattern | Complexity | Notes |
|---|---|---|---|
| Edit Distance | dp[i][j] = operations to transform s1[:i] to s2[:j] | O(mΓn) | Insert/Delete/Replace |
| LCS/LIS | dp[i][j] = length of common subsequence | O(mΓn) | Can optimize LIS to O(n log n) |
| Palindrome | dp[i][j] = is s[i:j+1] palindrome | O(nΒ²) | Expand around centers |
| Word Break | dp[i] = can break s[:i] | O(nΒ³) | Check all possible breaks |
# Valid Parenthesis String Pattern (LC 678) π
Problem: Given a string containing β(β, β)β and β', where 'β can be treated as β(β, β)β or empty string, determine if the string is valid.
This problem demonstrates multiple DP paradigms and is excellent for understanding:
- State tracking with wildcards
- Greedy vs DP trade-offs
- Interval DP patterns
- Space optimization techniques
# Approach 1: Greedy (Min/Max Balance Tracking) β‘ OPTIMAL
Time: O(n) | Space: O(1)
Key Insight: Track the range of possible unmatched open parentheses at each position.
public boolean checkValidString(String s) {
int minParenCnt = 0; // minimum possible unmatched '('
int maxParenCnt = 0; // maximum possible unmatched '('
for (char c : s.toCharArray()) {
if (c == '(') {
minParenCnt++;
maxParenCnt++;
} else if (c == ')') {
minParenCnt--;
maxParenCnt--;
} else { // '*' - wildcard
minParenCnt--; // treat '*' as ')'
maxParenCnt++; // treat '*' as '('
}
// If maxParenCnt < 0: too many unmatched ')'
if (maxParenCnt < 0) return false;
// If minParenCnt < 0: reset to 0 (can use '*' as empty)
if (minParenCnt < 0) minParenCnt = 0;
}
// Valid if we can have 0 unmatched '('
return minParenCnt == 0;
}
Why this works:
maxParenCnt < 0β impossible to balance (too many β)β)minParenCnt = 0β can reset negative balance using β*β as empty- Final
minParenCnt == 0β at least one valid way to match all
# Approach 2: 2D DP (Position Γ Open Count) π
Time: O(nΒ²) | Space: O(nΒ²)
DP Definition:
dp[i][j]: Can we have exactlyjunmatched β(β after processing firsticharacters?
public boolean checkValidString(String s) {
int n = s.length();
boolean[][] dp = new boolean[n + 1][n + 1];
dp[0][0] = true; // empty string, 0 open parens
for (int i = 1; i <= n; i++) {
char c = s.charAt(i - 1);
for (int j = 0; j <= n; j++) {
if (c == '(') {
// Add one open paren
if (j > 0) dp[i][j] = dp[i - 1][j - 1];
} else if (c == ')') {
// Close one open paren
if (j < n) dp[i][j] = dp[i - 1][j + 1];
} else { // '*'
// Option 1: treat '*' as empty
dp[i][j] = dp[i - 1][j];
// Option 2: treat '*' as '('
if (j > 0) dp[i][j] |= dp[i - 1][j - 1];
// Option 3: treat '*' as ')'
if (j < n) dp[i][j] |= dp[i - 1][j + 1];
}
}
}
return dp[n][0]; // n chars processed, 0 open parens
}
State Transitions:
'(':dp[i][j] = dp[i-1][j-1](increase open count)')':dp[i][j] = dp[i-1][j+1](decrease open count)'*':dp[i][j] = dp[i-1][j] || dp[i-1][j-1] || dp[i-1][j+1](try all 3)
# Approach 3: Interval DP (Range Validity) π―
Time: O(nΒ³) | Space: O(nΒ²)
DP Definition:
dp[i][j]: Is substrings[i..j]valid?
public boolean checkValidString(String s) {
int n = s.length();
if (n == 0) return true;
boolean[][] dp = new boolean[n][n];
// Base case: single character valid only if '*'
for (int i = 0; i < n; i++) {
if (s.charAt(i) == '*') dp[i][i] = true;
}
// Fill table for increasing lengths
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
// Option A: s[i] and s[j] form a matching pair
if ((s.charAt(i) == '(' || s.charAt(i) == '*') &&
(s.charAt(j) == ')' || s.charAt(j) == '*')) {
if (len == 2 || dp[i + 1][j - 1]) {
dp[i][j] = true;
}
}
// Option B: Split at some point k
if (!dp[i][j]) {
for (int k = i; k < j; k++) {
if (dp[i][k] && dp[k + 1][j]) {
dp[i][j] = true;
break;
}
}
}
}
}
return dp[0][n - 1];
}
Key Pattern: This is classic Interval DP similar to:
- LC 312 (Burst Balloons)
- LC 1039 (Minimum Score Triangulation)
- LC 1547 (Minimum Cost to Cut a Stick)
# Approach 4: Top-Down DP (Recursion + Memoization) π
Time: O(nΒ²) | Space: O(nΒ²) + recursion stack
public boolean checkValidString(String s) {
int n = s.length();
Boolean[][] memo = new Boolean[n + 1][n + 1];
return dfs(0, 0, s, memo);
}
private boolean dfs(int i, int open, String s, Boolean[][] memo) {
// Too many closing parens
if (open < 0) return false;
// End of string: valid if all matched
if (i == s.length()) return open == 0;
// Memoization
if (memo[i][open] != null) return memo[i][open];
boolean result;
if (s.charAt(i) == '(') {
result = dfs(i + 1, open + 1, s, memo);
} else if (s.charAt(i) == ')') {
result = dfs(i + 1, open - 1, s, memo);
} else { // '*'
result = dfs(i + 1, open, s, memo) || // empty
dfs(i + 1, open + 1, s, memo) || // '('
dfs(i + 1, open - 1, s, memo); // ')'
}
memo[i][open] = result;
return result;
}
# Approach 5: Bottom-Up DP π
Time: O(nΒ²) | Space: O(nΒ²)
public boolean checkValidString(String s) {
int n = s.length();
boolean[][] dp = new boolean[n + 1][n + 1];
dp[n][0] = true; // base: end with 0 open parens
for (int i = n - 1; i >= 0; i--) {
for (int open = 0; open < n; open++) {
boolean res = false;
if (s.charAt(i) == '*') {
res |= dp[i + 1][open + 1]; // treat as '('
if (open > 0) res |= dp[i + 1][open - 1]; // treat as ')'
res |= dp[i + 1][open]; // treat as empty
} else {
if (s.charAt(i) == '(') {
res |= dp[i + 1][open + 1];
} else if (open > 0) {
res |= dp[i + 1][open - 1];
}
}
dp[i][open] = res;
}
}
return dp[0][0];
}
# Approach 6: Space-Optimized DP β‘
Time: O(nΒ²) | Space: O(n)
public boolean checkValidString(String s) {
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for (int i = n - 1; i >= 0; i--) {
boolean[] newDp = new boolean[n + 1];
for (int open = 0; open < n; open++) {
if (s.charAt(i) == '*') {
newDp[open] = dp[open + 1] ||
(open > 0 && dp[open - 1]) ||
dp[open];
} else if (s.charAt(i) == '(') {
newDp[open] = dp[open + 1];
} else if (open > 0) {
newDp[open] = dp[open - 1];
}
}
dp = newDp;
}
return dp[0];
}
Space Optimization Technique: Rolling array - only keep current and previous row.
# Approach 7: Stack-Based (Two Stacks) π
Time: O(n) | Space: O(n)
public boolean checkValidString(String s) {
Stack<Integer> leftStack = new Stack<>(); // indices of '('
Stack<Integer> starStack = new Stack<>(); // indices of '*'
// First pass: match ')' with '(' or '*'
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (ch == '(') {
leftStack.push(i);
} else if (ch == '*') {
starStack.push(i);
} else { // ')'
if (!leftStack.isEmpty()) {
leftStack.pop();
} else if (!starStack.isEmpty()) {
starStack.pop();
} else {
return false; // unmatched ')'
}
}
}
// Second pass: match remaining '(' with '*'
while (!leftStack.isEmpty() && !starStack.isEmpty()) {
// '*' must come after '(' to be valid
if (leftStack.pop() > starStack.pop()) {
return false;
}
}
return leftStack.isEmpty();
}
Key Insight: Store indices to ensure β*β comes after β(β when used as β)β.
# Pattern Comparison Summary
| Approach | Time | Space | Best For | Trade-offs |
|---|---|---|---|---|
| Greedy (min/max) | O(n) | O(1) | Production code | Hardest to understand initially |
| 2D DP (pos Γ count) | O(nΒ²) | O(nΒ²) | Learning state transitions | Space-heavy but intuitive |
| Interval DP | O(nΒ³) | O(nΒ²) | Understanding range problems | Slowest but shows interval pattern |
| Top-Down DP | O(nΒ²) | O(nΒ²) | Natural recursion thinkers | Stack overhead |
| Bottom-Up DP | O(nΒ²) | O(nΒ²) | Avoiding recursion | Requires reverse thinking |
| Space-Optimized | O(nΒ²) | O(n) | Memory-constrained | More complex implementation |
| Stack-Based | O(n) | O(n) | Index-tracking insight | Two-pass algorithm |
# Key Takeaways π‘
- Greedy is optimal for this problem - recognizing when greedy works is crucial
- Wildcard handling: Always consider all possibilities (β(β, β)β, empty)
- Balance tracking: Many paren problems reduce to tracking open count
- Index matters: When wildcards can be different things, position matters (stack approach)
- Multiple paradigms: Same problem solvable with interval DP, state DP, greedy, and stacks
# Related Problems
- LC 20 (Valid Parentheses) - simpler version without β*β
- LC 32 (Longest Valid Parentheses) - find longest valid substring
- LC 301 (Remove Invalid Parentheses) - remove minimum to make valid
- LC 921 (Minimum Add to Make Parentheses Valid) - min additions needed
Reference: leetcode_java/src/main/java/LeetCodeJava/String/ValidParenthesisString.java
# State Compression Patterns
When to Use Bitmask DP:
- Small state space (β€ 20 items)
- Need to track which items are selected/visited
- Permutation/combination problems
- Traveling salesman variants
Common Bitmask Operations:
# Check if i-th bit is set
if mask & (1 << i):
pass
# Set i-th bit
new_mask = mask | (1 << i)
# Unset i-th bit
new_mask = mask & ~(1 << i)
# Iterate through all submasks
submask = mask
while submask:
# Process submask
submask = (submask - 1) & mask
# Advanced DP Patterns
# Interval DP Template:
def interval_dp(arr):
"""Matrix chain multiplication style"""
n = len(arr)
dp = [[0] * n for _ in range(n)]
# Length of interval
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
dp[i][j] = float('inf')
# Try all possible split points
for k in range(i, j):
cost = dp[i][k] + dp[k+1][j] + arr[i] * arr[k+1] * arr[j+1]
dp[i][j] = min(dp[i][j], cost)
return dp[0][n-2] if n > 1 else 0
# Problems by Pattern
# Linear DP Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Climbing Stairs | 70 | dp[i] = dp[i-1] + dp[i-2] | Easy |
| House Robber | 198 | Max with skip | Medium |
| Longest Increasing Subsequence | 300 | O(nΒ²) or O(nlogn) | Medium |
| Maximum Subarray | 53 | Kadaneβs algorithm | Easy |
| Decode Ways | 91 | String DP | Medium |
| Word Break | 139 | Dictionary DP | Medium |
| Coin Change | 322 | Min coins | Medium |
# 2D Grid Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Unique Paths | 62 | Path counting | Medium |
| Minimum Path Sum | 64 | Min cost path | Medium |
| Maximal Square | 221 | 2D expansion | Medium |
| Dungeon Game | 174 | Backward DP | Hard |
| Cherry Pickup | 741 | 3D DP | Hard |
| Number of Paths with Max Score | 1301 | Multi-value DP | Hard |
# Interval DP Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Longest Palindromic Substring | 5 | Expand or DP | Medium |
| Palindrome Partitioning II | 132 | Min cuts | Hard |
| Burst Balloons | 312 | Interval multiplication | Hard |
| Minimum Cost to Merge Stones | 1000 | K-way merge | Hard |
| Strange Printer | 664 | Interval printing | Hard |
# Knapsack Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Partition Equal Subset Sum | 416 | 0/1 Knapsack | Medium |
| Target Sum | 494 | Sum to target | Medium |
| Last Stone Weight II | 1049 | Min difference | Medium |
| Ones and Zeroes | 474 | 2D Knapsack | Medium |
| Coin Change 2 | 518 | Unbounded (CoinβAmount = Combinations) | Medium |
| Combination Sum IV | 377 | Unbounded (AmountβCoin = Permutations) | Medium |
# State Machine Problems
| Problem | LC # | Key Technique | Difficulty | States | Pattern |
|---|---|---|---|---|---|
| Best Time to Buy and Sell Stock II | 122 | Multiple transactions | Easy | 2 states | hold/cash |
| Stock with Cooldown | 309 | 3-state transitions | Medium | 3 states | hold/sold/rest |
| Stock with Transaction Fee | 714 | Fee consideration | Medium | 2 states | hold/cash |
| Stock III | 123 | At most 2 transactions | Hard | 4 states | buy1/sell1/buy2/sell2 |
| Stock IV | 188 | At most k transactions | Hard | 2k states | Dynamic states |
Core Pattern Analysis: Stock Problems
| Problem | Constraint | States Needed | Key Difference |
|---|---|---|---|
| LC 122 | Unlimited transactions | 2 (hold/cash) | Simple buy/sell |
| LC 309 | Cooldown after sell | 3 (hold/sold/rest) | Need rest state |
| LC 714 | Transaction fee | 2 (hold/cash) | Deduct fee when sell |
| LC 123 | At most 2 transactions | 4 (2 buy/sell pairs) | Track transaction count |
| LC 188 | At most k transactions | 2k states | Generalized k transactions |
State Machine Pattern Recognition:
Question asks... β Use this pattern
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
"Cooldown after action" β 3+ states (LC 309)
"Transaction fee/cost" β 2 states with cost
"Limited transactions (k times)" β 2k states
"Unlimited transactions" β 2 states (hold/cash)
# LC Examples
# 2-1) Unique Paths
// java
// LC 62
// V0
// IDEA: 2D DP (fixed by gpt)
public int uniquePaths(int m, int n) {
if (m == 0 || n == 0)
return 0;
int[][] dp = new int[m][n];
/** NOTE !!! init val as below
*
* -> First row and first column = 1 path
* (only one way to go right/down)
*/
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n; j++) {
dp[0][j] = 1;
}
// Fill the rest of the DP table
// NOTE !!! i, j both start from 1
// `(0, y), (x, 0)` already been initialized
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
/** DP equation
*
* dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
*/
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
# 62. Unique Paths
# V0
# IDEA : BFS + dp (memory)
class Solution:
def uniquePaths(self, m, n):
# NOTE !!! we init paths as below
paths = [[1]*n for _ in range(m)]
q = deque()
q.append((0,0))
while q:
row, col = q.popleft()
if row == m or col == n or paths[row][col] > 1:
continue
if row-1 >= 0 and col-1 >= 0:
paths[row][col] = paths[row-1][col] + paths[row][col-1]
q.append((row+1, col))
q.append((row, col+1))
return paths[-1][-1]
# V0'
# IDEA : DP
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
d = [[1] * n for _ in range(m)]
for col in range(1, m):
for row in range(1, n):
d[col][row] = d[col - 1][row] + d[col][row - 1]
return d[m - 1][n - 1]
# 2-2) Maximum Product Subarray
# NOTE : there is also brute force approach
# V0
# IDEA : brute force + product
class Solution(object):
def maxProduct(self, A):
global_max, local_max, local_min = float("-inf"), 1, 1
for x in A:
local_max = max(1, local_max)
if x > 0:
local_max, local_min = local_max * x, local_min * x
else:
local_max, local_min = local_min * x, local_max * x
global_max = max(global_max, local_max)
return global_max
# V1
# IDEA : BRUTE FORCE (TLE)
# https://leetcode.com/problems/maximum-product-subarray/solution/
class Solution:
def maxProduct(self, nums: List[int]) -> int:
if len(nums) == 0:
return 0
result = nums[0]
for i in range(len(nums)):
accu = 1
for j in range(i, len(nums)):
accu *= nums[j]
result = max(result, accu)
return result
# V1
# IDEA : DP
# https://leetcode.com/problems/maximum-product-subarray/solution/
# LC 152
class Solution:
def maxProduct(self, nums: List[int]) -> int:
if len(nums) == 0:
return 0
max_so_far = nums[0]
min_so_far = nums[0]
result = max_so_far
for i in range(1, len(nums)):
curr = nums[i]
temp_max = max(curr, max_so_far * curr, min_so_far * curr)
min_so_far = min(curr, max_so_far * curr, min_so_far * curr)
max_so_far = temp_max
result = max(max_so_far, result)
return result
// java
// LC 152
// V0
// IDEA : DP
// https://github.com/yennanliu/CS_basics/blob/master/leetcode_python/Dynamic_Programming/maximum-product-subarray.py#L69
// IDEA : cur max = max (cur, cur * dp[k-1])
// But, also needs to consider "minus number"
// -> e.g. (-1) * (-3) = 3
// -> so we NEED to track maxSoFar, and minSoFar
public int maxProduct(int[] nums) {
// null check
if (nums.length == 0){
return 0;
}
// init
int maxSoFar = nums[0];
int minSoFar = nums[0];
int res = maxSoFar;
for (int i = 1; i < nums.length; i++){
int cur = nums[i];
/**
* or, can use below trick to get max in 3 numbers
*
* max = Math.max(Math.max(max * nums[i], min * nums[i]), nums[i]);
* min = Math.min(Math.min(temp * nums[i], min * nums[i]), nums[i]);
*
*/
int tmpMax = findMax(cur, maxSoFar * cur, minSoFar * cur);
minSoFar = findMin(cur, maxSoFar * cur, minSoFar * cur);
maxSoFar = tmpMax;
res = Math.max(maxSoFar, res);
}
return res;
}
private int findMax(int a, int b, int c){
if (a >= b && a >= c){
return a;
}
else if (b >= a && b >= c){
return b;
}else{
return c;
}
}
private int findMin(int a, int b, int c){
if (a <= b && a <= c){
return a;
}
else if (b <= a && b <= c){
return b;
}else{
return c;
}
}
# 2-3) Best Time to Buy and Sell Stock with Transaction Fee
// java
// LC 714
// V0-1
// IDEA: DP (gpt)
/**
* Solution Explanation:
*
*
* - Use two variables to represent the state:
* 1. hold: The maximum profit achievable
* while holding a stock at day i.
*
* 2. cash: The maximum profit achievable
* while not holding a stock at day i.
*
* - Transition equations:
* - If holding a stock:
* hold = max(hold, cash - price[i])
*
* NOTE: 2 cases we hold th stock: 1) already hold from previous day 2) buy a new stock today
* (`hold`: You already held the stock from a previous day -> If you decided not to make any changes today, then the profit remains the same as the previous hold.)
* (`cash - price[i]`: You buy the stock today -> To buy the stock today, you need to spend money, reducing your profit. The cost to buy the stock is prices[i]. However, the amount of money you can spend is the maximum profit you had when you were not holding a stock previously (cash).)
*
* (You either keep holding or buy a new stock.)
* - If not holding a stock:
* cash = max(cash, hold + price[i] - fee)
*
*
* (You either keep not holding or sell the stock and pay the fee.)
* - Initialize:
* - hold = -prices[0] (If you buy the stock on the first day).
* - cash = 0 (You havenβt made any transactions yet).
*
*/
/**
* Example Walkthrough:
*
* Input:
* β’ Prices: [1, 3, 2, 8, 4, 9]
* β’ Fee: 2
*
* Steps:
* 1. Day 0:
* β’ hold = -1 (Buy the stock at price 1).
* β’ cash = 0.
* 2. Day 1:
* β’ cash = max(0, -1 + 3 - 2) = 0 (No selling since profit is 0).
* β’ hold = max(-1, 0 - 3) = -1 (No buying since itβs already better to hold).
* 3. Day 2:
* β’ cash = max(0, -1 + 2 - 2) = 0.
* β’ hold = max(-1, 0 - 2) = -1.
* 4. Day 3:
* β’ cash = max(0, -1 + 8 - 2) = 5 (Sell at price 8).
* β’ hold = max(-1, 5 - 8) = -1.
* 5. Day 4:
* β’ cash = max(5, -1 + 4 - 2) = 5.
* β’ hold = max(-1, 5 - 4) = 1.
* 6. Day 5:
* β’ cash = max(5, 1 + 9 - 2) = 8 (Sell at price 9).
* β’ hold = max(1, 5 - 9) = 1.
*
* Output:
* β’ cash = 8 (Max profit).
*
*/
public int maxProfit_0_1(int[] prices, int fee) {
// Edge case
if (prices == null || prices.length == 0) {
return 0;
}
// Initialize states
int hold = -prices[0]; // Maximum profit when holding a stock
int cash = 0; // Maximum profit when not holding a stock
// Iterate through prices
for (int i = 1; i < prices.length; i++) {
/**
* NOTE !!! there are 2 dp equations (e.g. cash, hold)
*/
// Update cash and hold states
cash = Math.max(cash, hold + prices[i] - fee); // Sell the stock
hold = Math.max(hold, cash - prices[i]); // Buy the stock
}
// The maximum profit at the end is when not holding any stock
return cash;
}
# 2-3-2) Best Time to Buy and Sell Stock with Cooldown (LC 309)
// java
// LC 309. Best Time to Buy and Sell Stock with Cooldown
/**
* Problem: You can buy and sell stock multiple times, but after selling,
* you must cooldown for 1 day before buying again.
*
* Key Insight: This requires 3 states instead of the typical 2 states
* because we need to track the cooldown period.
*/
// V0-1: 2D DP (n x 3 array) - Most Intuitive
/**
* State Definition:
* dp[i][0] = Max profit on day i if we HOLD a stock
* dp[i][1] = Max profit on day i if we just SOLD a stock
* dp[i][2] = Max profit on day i if we are RESTING (cooldown/do nothing)
*
* State Transition Equations:
* 1. HOLD: dp[i][0] = max(dp[i-1][0], dp[i-1][2] - prices[i])
* - Either held from yesterday OR bought today (after rest)
*
* 2. SOLD: dp[i][1] = dp[i-1][0] + prices[i]
* - Must have held stock yesterday, sell at today's price
*
* 3. REST: dp[i][2] = max(dp[i-1][2], dp[i-1][1])
* - Either rested yesterday OR just finished cooldown from sale
*
* Why 3 States?
* - HOLD: Represents actively holding stock
* - SOLD: Triggers the cooldown (can't buy tomorrow)
* - REST: Free to make any action (cooldown complete or never started)
*/
public int maxProfit(int[] prices) {
if (prices == null || prices.length <= 1)
return 0;
int n = prices.length;
int[][] dp = new int[n][3];
// Base Case: Day 0
dp[0][0] = -prices[0]; // Bought on day 0
dp[0][1] = 0; // Can't sell on day 0
dp[0][2] = 0; // Doing nothing
for (int i = 1; i < n; i++) {
// HOLD: Either held yesterday OR bought today (after rest)
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][2] - prices[i]);
// SOLD: Held yesterday and sell today
dp[i][1] = dp[i-1][0] + prices[i];
// REST: Either rested yesterday OR cooldown from yesterday's sale
dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1]);
}
// Max profit when not holding stock on last day
return Math.max(dp[n-1][1], dp[n-1][2]);
}
// V0-2: Space Optimized (O(1) space) - Interview Favorite
/**
* Since we only need previous day's state, we can use 3 variables
* instead of a 2D array.
*
* This is the preferred solution for interviews due to O(1) space.
*/
public int maxProfit_optimized(int[] prices) {
if (prices == null || prices.length == 0)
return 0;
int hold = -prices[0]; // Holding a stock
int sold = 0; // Just sold (in cooldown trigger)
int rest = 0; // Resting (free to act)
for (int i = 1; i < prices.length; i++) {
// Save previous sold state (needed for rest calculation)
int prevSold = sold;
// State transitions
sold = hold + prices[i]; // Sell today
hold = Math.max(hold, rest - prices[i]); // Hold or buy today
rest = Math.max(rest, prevSold); // Rest or finish cooldown
}
// Max profit when not holding stock
return Math.max(sold, rest);
}
Example Walkthrough: prices = [1,2,3,0,2]
Day | Price | HOLD | SOLD | REST | Action Taken
----|-------|-------|------|------|-------------
0 | 1 | -1 | 0 | 0 | Buy at 1
1 | 2 | -1 | 1 | 0 | Sell at 2 (profit = 1)
2 | 3 | -1 | 2 | 1 | Sell at 3 (profit = 2)
3 | 0 | 1 | 2 | 2 | Buy at 0 (after cooldown)
4 | 2 | 1 | 3 | 2 | Sell at 2 (profit = 3)
Optimal path: Buy@1 β Sell@2 β Cooldown β Buy@0 β Sell@2
Max Profit: 3
State Transition Trace (Day 4):
Previous State (Day 3):
hold = 1, sold = 2, rest = 2
Current Price: prices[4] = 2
Calculate New States:
prevSold = sold = 2 (save before update)
sold = hold + prices[4] = 1 + 2 = 3 β
(sell the stock we bought at 0)
hold = max(hold, rest - prices[4])
= max(1, 2 - 2)
= max(1, 0) = 1 (keep holding, don't buy)
rest = max(rest, prevSold)
= max(2, 2) = 2 (stay in rest)
Final Answer: max(sold, rest) = max(3, 2) = 3
Key Differences from Regular Stock Problems:
| Aspect | Regular Stock (LC 122) | Stock with Cooldown (LC 309) |
|---|---|---|
| States | 2 (hold, cash) | 3 (hold, sold, rest) |
| Constraint | None | Must cooldown after sell |
| Buy Transition | hold = max(hold, cash - price) |
hold = max(hold, rest - price) |
| Why Different? | Can buy anytime | Can only buy after rest (not immediately after sold) |
| Space | O(1) - 2 variables | O(1) - 3 variables |
| Complexity | O(n) time | O(n) time |
Common Mistakes:
- β Using 2 states instead of 3 (ignores cooldown)
- β
hold = max(hold, sold - prices[i])(canβt buy right after selling!) - β Forgetting to save
prevSoldbefore updating (wrong rest calculation) - β Returning
max(hold, sold, rest)(canβt end while holding)
Why This Pattern Works:
- SOLD state: Acts as a βgateβ - after entering, you must go through REST
- REST state: βUnlocksβ the ability to BUY again
- HOLD state: Blocks you from RESTING (must sell first)
This creates a forced flow: HOLD β SOLD β REST β HOLD, ensuring cooldown compliance.
Similar Problems:
- LC 122: Best Time to Buy and Sell Stock II (no cooldown, simpler)
- LC 714: Best Time to Buy and Sell Stock with Transaction Fee (2 states + fee)
- LC 123: Best Time to Buy and Sell Stock III (4 states for 2 transactions)
- LC 188: Best Time to Buy and Sell Stock IV (2k states for k transactions)
# 2-3-3) N-th Tribonacci Number
// java
// LC 1137. N-th Tribonacci Number
// V0
// IDEA: DP (fixed by gpt)
public int tribonacci(int n) {
if (n == 0)
return 0;
if (n == 1 || n == 2)
return 1;
// NOTE !!! below, array size is `n + 1`
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
dp[2] = 1;
// NOTE !!! below, we loop from i = 3 to `i <= n`
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
}
return dp[n];
}
# 2-4) Longest Increasing Subsequence (LIS)
// java
// LC 300. Longest Increasing Subsequence
/** NOTE !!!
*
* 1. use 1-D DP
* 2. Key Insight (Important):
*
* - dp[i] = best LIS ending exactly at index i
*
* - Inner loop checks:
* - "Can I extend a smaller LIS ending at j by appending nums[i]?"
*
* - maxLen tracks the global maximum across all endpoints
*
*/
// V0
// IDEA: 1D DP - O(nΒ²) solution
public int lengthOfLIS(int[] nums) {
if(nums == null || nums.length < 1) {
return 0;
}
int n = nums.length;
int[] dp = new int[n];
// Each element itself is an increasing subsequence of length 1
for(int i = 0; i < n; i++) {
dp[i] = 1;
}
int res = 1;
for(int i = 1; i < n; i++) {
for(int j = 0; j < i; j++) {
/**
* NOTE !!!
*
* `nums[i] > nums[j]` condition !!!
*
* -> ONLY if `right element is bigger than left element`,
* new length is calculated and DP array is updated
*
* -> This ensures we're building an INCREASING subsequence
*
* -> We check all previous elements (j < i) to see if we can
* extend their subsequences by adding nums[i]
*
*/
if(nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
res = Math.max(res, dp[i]);
}
}
}
return res;
}
LIS Pattern Explanation:
| Aspect | Explanation |
|---|---|
| State Definition | dp[i] = length of longest increasing subsequence ending at index i |
| Initialization | dp[i] = 1 for all i (each element is a subsequence of length 1) |
| Transition | dp[i] = max(dp[i], dp[j] + 1) if nums[i] > nums[j] for all j < i |
| Key Condition | nums[i] > nums[j] ensures we only extend increasing subsequences |
| Time Complexity | O(nΒ²) - nested loops through array |
| Space Complexity | O(n) - 1D DP array |
| Result | max(dp[i]) for all i - maximum value in DP array |
Why the condition nums[i] > nums[j] is critical:
- We iterate through all previous elements
j(wherej < i) - We check if current element
nums[i]can extend the subsequence ending atj - Only when
nums[i] > nums[j], we can appendnums[i]to maintain increasing order dp[j] + 1represents extending the LIS ending atjby addingnums[i]
Example Walkthrough:
Input: nums = [10, 9, 2, 5, 3, 7, 101, 18]
Initial: dp = [1, 1, 1, 1, 1, 1, 1, 1]
i=1, nums[1]=9: No j where nums[j] < 9, dp[1] = 1
i=2, nums[2]=2: No j where nums[j] < 2, dp[2] = 1
i=3, nums[3]=5: nums[2]=2 < 5, dp[3] = dp[2]+1 = 2
i=4, nums[4]=3: nums[2]=2 < 3, dp[4] = dp[2]+1 = 2
i=5, nums[5]=7: nums[2]=2,nums[3]=5,nums[4]=3 < 7
dp[5] = max(dp[2]+1, dp[3]+1, dp[4]+1) = 3
i=6, nums[6]=101: Can extend from multiple, dp[6] = 4
i=7, nums[7]=18: Can extend from multiple, dp[7] = 4
Result: max(dp) = 4
LIS: [2, 3, 7, 101] or [2, 5, 7, 101] or others
# Decision Framework
# Pattern Selection Strategy
DP Problem Identification Flowchart:
1. Can the problem be broken into subproblems?
βββ NO β Not a DP problem
βββ YES β Continue to 2
2. Do subproblems overlap?
βββ NO β Use Divide & Conquer
βββ YES β Continue to 3
3. Does it have optimal substructure?
βββ NO β Not a DP problem
βββ YES β Use DP, continue to 4
4. What type of DP pattern?
βββ Single sequence β Linear DP (Template 1)
βββ 2D grid/matrix β Grid DP (Template 2)
βββ Interval/substring β Interval DP (Template 3)
βββ Selection with limit β Knapsack (Template 4)
βββ Multiple states β State Machine (Template 5)
5. Implementation approach?
βββ Recursive structure clear β Top-down memoization
βββ Iterative structure clear β Bottom-up tabulation
# When to Use DP vs Other Approaches
| Problem Type | Use DP | Use Alternative | Alternative |
|---|---|---|---|
| Optimization (min/max) | β | Sometimes | Greedy if optimal |
| Count ways/paths | β | - | - |
| Decision (yes/no) | β | Sometimes | Greedy/DFS |
| All solutions needed | β | β | Backtracking |
| No overlapping subproblems | β | β | Divide & Conquer |
| Greedy choice property | β | β | Greedy |
# Summary & Quick Reference
# Complexity Quick Reference
| Pattern | Time Complexity | Space Complexity | Space Optimization |
|---|---|---|---|
| 1D Linear | O(n) | O(n) | O(1) with variables |
| 2D Grid | O(mΓn) | O(mΓn) | O(n) with rolling array |
| Interval | O(nΒ³) typical | O(nΒ²) | Usually not possible |
| 0/1 Knapsack | O(nΓW) | O(nΓW) | O(W) with 1D array |
| State Machine | O(nΓk) | O(k) | Already optimized |
# State Definition Guidelines
# 1D: Position/index based
dp[i] = "optimal value considering first i elements"
# 2D: Two dimensions
dp[i][j] = "optimal value for subproblem (i, j)"
# Interval: Range based
dp[i][j] = "optimal value for interval [i, j]"
# Boolean: Decision problems
dp[i] = "whether target i is achievable"
# Common Recurrence Relations
# Sum/Count Patterns
# Fibonacci-like
dp[i] = dp[i-1] + dp[i-2]
# Include/exclude current
dp[i] = dp[i-1] + (dp[i-2] + nums[i])
# Min/Max Patterns
# Take or skip
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
# Best from all previous
dp[i] = max(dp[j] + score(j, i) for j < i)
# Grid Patterns
# Path counting
dp[i][j] = dp[i-1][j] + dp[i][j-1]
# Min path
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
# Problem-Solving Steps
- Identify if DP applicable: Check for overlapping subproblems
- Define state: What does dp[i] represent?
- Find recurrence: How do states relate?
- Identify base cases: Initial values
- Determine iteration order: Bottom-up direction
- Optimize space: Can we use rolling array?
# Common Mistakes & Tips
π« Common Mistakes:
- Wrong state definition
- Missing base cases
- Incorrect iteration order
- Not handling edge cases
- Integer overflow in large problems
β Best Practices:
- Start with recursive solution, then optimize
- Draw small examples to find patterns
- Check array bounds carefully
- Consider space optimization after correctness
- Use meaningful variable names for states
# Space Optimization Techniques
# Rolling Array
# From O(nΒ²) to O(n)
# Instead of dp[i][j], use dp[2][j]
curr = [0] * n
prev = [0] * n
for i in range(m):
curr, prev = prev, curr
# Update curr based on prev
# State Compression
# From O(n) to O(1) for Fibonacci-like
prev2, prev1 = 0, 1
for i in range(2, n):
curr = prev1 + prev2
prev2, prev1 = prev1, curr
# Interview Tips
- Start simple: Write recursive solution first
- Identify subproblems: Draw recursion tree
- Add memoization: Convert to top-down DP
- Consider bottom-up: Often more efficient
- Optimize space: Impress with rolling array
- Test with examples: Trace through small inputs
# State Machine DP Interview Pattern Recognition
Quick Decision Tree:
Stock/Transaction Problem?
ββ NO β Check other DP patterns
ββ YES β Continue below
Are there any constraints on transactions?
ββ NO constraints (unlimited) β 2 states (hold/cash) [LC 122]
ββ Cooldown period β 3 states (hold/sold/rest) [LC 309]
ββ Transaction fee β 2 states + fee deduction [LC 714]
ββ Limited k transactions β 2k states [LC 123, LC 188]
ββ Buy only once β Kadane's algorithm [LC 121]
State Machine Pattern Comparison:
| Constraint Type | States | State Names | Buy Condition | Sell Condition | Example LC |
|---|---|---|---|---|---|
| None | 2 | hold, cash | cash - price |
hold + price |
122 |
| Cooldown | 3 | hold, sold, rest | rest - price β οΈ |
hold + price |
309 |
| Transaction fee | 2 | hold, cash | cash - price |
hold + price - fee |
714 |
| k transactions | 2k | buy1, sell1, β¦ | Track transaction # | Track transaction # | 123, 188 |
β οΈ Critical Difference in Cooldown Pattern:
- Regular:
hold = max(hold, cash - price)- can buy anytime - Cooldown:
hold = max(hold, rest - price)- can only buy after rest!
Pattern Recognition Cheat Sheet:
| Problem Says⦠| Pattern | States | Key Transition |
|---|---|---|---|
| βCooldown 1 day after sellβ | 3-state | hold/sold/rest | Buy from rest only |
| βTransaction fee of kβ | 2-state | hold/cash | cash = hold + price - fee |
| βAt most 2 transactionsβ | 4-state | buy1/sell1/buy2/sell2 | Track transaction count |
| βAt most k transactionsβ | 2k-state | Dynamic | Generalized k transactions |
| βUnlimited transactionsβ | 2-state | hold/cash | Simple buy/sell |
Common Interview Follow-ups:
- βWhat if cooldown is k days?β β Need k+2 states
- βWhat if both cooldown AND fee?β β 3 states + fee deduction
- βSpace optimize itβ β Use variables instead of arrays
- βProve correctnessβ β Show state transitions enforce constraints
# Related Topics
- Greedy: When local optimal leads to global
- Backtracking: When need all solutions
- Divide & Conquer: No overlapping subproblems
- Graph Algorithms: DP on graphs (shortest path)
- Binary Search: Optimization problems with monotonicity