1d Dp
Phase 3 17 solutions1D Dynamic Programming
mindmap
root((1D DP))
Linear Fibonacci-style
Climbing Stairs LC70
Min Cost Climbing Stairs LC746
N-th Tribonacci LC1137
House Robber LC198
House Robber II LC213
Knapsack
Coin Change LC322
Partition Equal Subset Sum LC416
Combination Sum IV LC377
Perfect Squares LC279
Integer Break LC343
Subsequence
Longest Increasing Subsequence LC300
Word Break LC139
String/Palindrome
Longest Palindromic Substring LC5
Palindromic Substrings LC647
Decode Ways LC91
Game Theory
Stone Game III LC1406
Other
Maximum Product Subarray LC152
Problem List
| # | Problem | LC # | Difficulty | Key Technique |
|---|---|---|---|---|
| 1 | Climbing Stairs | 70 | Easy | Linear DP (Fibonacci) |
| 2 | Min Cost Climbing Stairs | 746 | Easy | Linear DP |
| 3 | N-th Tribonacci | 1137 | Easy | Linear DP (3 vars) |
| 4 | House Robber | 198 | Medium | Linear DP (skip adjacent) |
| 5 | House Robber II | 213 | Medium | Linear DP (circular, 2 passes) |
| 6 | Longest Palindromic Substring | 5 | Medium | Expand around center |
| 7 | Palindromic Substrings | 647 | Medium | Expand around center |
| 8 | Decode Ways | 91 | Medium | Linear DP (conditional transitions) |
| 9 | Coin Change | 322 | Medium | Unbounded knapsack |
| 10 | Maximum Product Subarray | 152 | Medium | Track min & max |
| 11 | Word Break | 139 | Medium | DP + set lookup |
| 12 | Longest Increasing Subsequence | 300 | Medium | DP / Binary search |
| 13 | Partition Equal Subset Sum | 416 | Medium | 0/1 Knapsack |
| 14 | Combination Sum IV | 377 | Medium | Unbounded knapsack (order matters) |
| 15 | Perfect Squares | 279 | Medium | Unbounded knapsack |
| 16 | Integer Break | 343 | Medium | DP / Math |
| 17 | Stone Game III | 1406 | Hard | Game theory DP |
Patterns & Diagrams
1. DP Problem Identification Decision Tree
flowchart TD
A["Optimization / Count problem"] --> B{Overlapping subproblems?}
B -->|No| C[Greedy or divide & conquer]
B -->|Yes| D{1D or 2D state?}
D -->|1D — index or value| E{What varies?}
E -->|Position in array| F["Linear DP<br/>Climbing Stairs, House Robber"]
E -->|Target sum / capacity| G["Knapsack DP<br/>Coin Change, Partition Sum"]
E -->|Subsequence length| H["Subsequence DP<br/>LIS, Word Break"]
D -->|2D — two sequences or grid| I[2D DP — see 2D DP sheet]
2. State Transition — 1D Linear
flowchart LR
A["dp[0]"] --> B["dp[1]"]
B --> C["dp[2]"]
C --> D["dp[i]"]
D --> E["dp[n]"]
A2[base case] --> A
3. Space Optimization — Array vs 2 Variables
flowchart TD
A[dp array needed?] --> B{"dp[i] depends on how many prior states?"}
B -->|Only dp[i-1]| C["1 variable: prev = cur"]
B -->|"dp[i-1] and dp[i-2]"| D["2 variables: prev2, prev1"]
B -->|"dp[i-1]...dp[i-k], small k"| E["Rolling array size k"]
B -->|dp[j] for all j < i| F["Keep full array O(n) space"]
C --> G["O(1) space"]
D --> G
E --> H["O(k) space"]
F --> I["O(n) space"]
4. Knapsack Variant Decision Tree
flowchart TD
A[Knapsack-style DP] --> B{"Each item used<br/>how many times?"}
B -->|Once| C["0/1 Knapsack<br/>Iterate items outer<br/>target inner REVERSE"]
B -->|Unlimited| D{Does order matter?}
D -->|No — combinations| E["Unbounded Knapsack<br/>Iterate items outer<br/>target inner FORWARD"]
D -->|Yes — permutations| F["Combination Sum IV style<br/>Iterate target outer<br/>items inner"]
C --> G[Partition Equal Subset Sum 416]
E --> H["Coin Change 322<br/>Perfect Squares 279"]
F --> I[Combination Sum IV 377]
5. LIS — Binary Search Approach
flowchart TD
A["Init tails = empty list"] --> B[For each num in array]
B --> C{"tails empty or num > tails.last?"}
C -->|Yes| D[Append num to tails]
C -->|No| E["Binary search: first tails[i] >= num"]
E --> F["Replace tails[i] with num"]
D --> B
F --> B
B --> G["len(tails) = LIS length"]
Complexity Summary
| Problem | Time | Space | Pattern |
|---|---|---|---|
| climbing_stairs | O(n) | O(1) | Linear DP (Fibonacci) |
| min_cost_climbing_stairs | O(n) | O(1) | Linear DP |
| n_th_tribonacci_number | O(n) | O(1) | Linear DP (3 vars) |
| house_robber | O(n) | O(1) | Linear DP (skip adjacent) |
| house_robber_ii | O(n) | O(1) | Linear DP (circular, 2 passes) |
| longest_palindromic_substring | O(n²) | O(1) | Expand around center |
| palindromic_substrings | O(n²) | O(1) | Expand around center |
| decode_ways | O(n) | O(1) | Linear DP (conditional transitions) |
| coin_change | O(n·target) | O(target) | Unbounded knapsack |
| maximum_product_subarray | O(n) | O(1) | Track min & max |
| word_break | O(n²) | O(n) | DP + set lookup |
| longest_increasing_subsequence | O(n log n) | O(n) | DP / Binary search |
| partition_equal_subset_sum | O(n·target) | O(target) | 0/1 Knapsack |
| combination_sum_iv | O(n·target) | O(target) | Unbounded knapsack (order matters) |
| perfect_squares | O(n·√n) | O(n) | Unbounded knapsack |
| integer_break | O(n²) | O(n) | DP / Math |
| stone_game_iii | O(n) | O(1) | Game theory DP |
Study Order
- Fibonacci family — 70, 746, 1137 (base linear DP)
- Skip-adjacent — 198, 213 (House Robber pattern)
- Conditional transitions — 91 (Decode Ways)
- Unbounded knapsack — 322, 279 (Coin Change)
- 0/1 knapsack — 416 (Partition Sum)
- Order-matters knapsack — 377 (Combination Sum IV)
- Subsequence DP — 300, 139 (LIS, Word Break)
- Palindrome DP — 5, 647 (expand around center)
- Product DP — 152 (track min & max)
- Game theory — 1406 (Stone Game III)
1D Dynamic Programming
Core Intuition
Break a problem into overlapping subproblems. Each state depends on a fixed number of previous states. Ask: "What is the minimum info I need to compute dp[i]?"
- Linear DP: dp[i] depends on dp[i-1], dp[i-2], etc.
- Knapsack: dp[i] = best result using first i items with capacity constraint
- Subsequence: dp[i] = best result ending at index i
- Palindrome: expand from center or dp[i][j] collapsed to 1D
Decision Flowchart
flowchart TD
A[1D DP Problem] --> B{Choices at each step?}
B -->|Fixed recurrence| C["Linear DP<br/>Climbing Stairs, Tribonacci"]
B -->|Include/Exclude item| D{Bounded quantity?}
D -->|Yes| E["0/1 Knapsack<br/>Partition Equal Subset"]
D -->|No| F["Unbounded Knapsack<br/>Coin Change, Combination Sum IV"]
B -->|Substring/Subsequence| G{Contiguous?}
G -->|Yes| H["Subarray DP<br/>Max Product, Palindromic Substr"]
G -->|No| I["Subsequence DP<br/>LIS, Decode Ways"]
B -->|Game / Optimal play| J["Min-Max DP<br/>Stone Game III"]
Problems
Climbing Stairs — LC 70 | Easy
- Learn: Base case setup; dp[i] = dp[i-1] + dp[i-2]
- Mistake: Off-by-one on base cases (dp[1]=1, dp[2]=2)
- Complexity: O(n) time, O(1) space (two variables)
- Edge: n=1 → return 1
Min Cost Climbing Stairs — LC 746 | Easy
- Learn: Can start at index 0 or 1; dp[i] = cost[i] + min(dp[i-1], dp[i-2])
- Mistake: Forgetting the "free" start; answer is min(dp[n-1], dp[n-2])
- Complexity: O(n) time, O(1) space
- Edge: Array of length 2
N-th Tribonacci — LC 1137 | Easy
- Learn: Three-variable rolling window instead of array
- Mistake: Wrong base cases (T0=0, T1=1, T2=1)
- Complexity: O(n) time, O(1) space
- Edge: n=0 → 0
House Robber — LC 198 | Medium
- Learn: dp[i] = max(dp[i-1], dp[i-2] + nums[i]); skip adjacent
- Mistake: Using array when two variables suffice
- Complexity: O(n) time, O(1) space
- Edge: Single house, two houses
House Robber II — LC 213 | Medium
- Learn: Circular → run House Robber twice: [0..n-2] and [1..n-1], take max
- Mistake: Forgetting that first and last are adjacent
- Complexity: O(n) time, O(1) space
- Edge: n=1 → return nums[0]
Longest Palindromic Substring — LC 5 | Medium
- Learn: Expand-around-center; check odd and even length centers
- Mistake: Using O(n²) space DP when O(1) space expand works
- Complexity: O(n²) time, O(1) space
- Edge: All same characters, single character
Palindromic Substrings — LC 647 | Medium
- Learn: Count expansions; each successful expansion = +1
- Mistake: Counting only odd-length palindromes
- Complexity: O(n²) time, O(1) space
- Edge: Empty string → 0
Decode Ways — LC 91 | Medium
- Learn: dp[i] = ways to decode s[0..i-1]; check single digit and two-digit validity
- Mistake: '0' alone is invalid; "06" is invalid as two-digit
- Complexity: O(n) time, O(1) space
- Edge: Leading zero, "10", "20", "30"
Coin Change — LC 322 | Medium
- Learn: Unbounded knapsack; dp[i] = min coins to make amount i
- Mistake: Initializing dp with 0 instead of infinity; forgetting dp[0]=0
- Complexity: O(n·amount) time, O(amount) space
- Edge: No solution → return -1; amount=0 → 0
Maximum Product Subarray — LC 152 | Medium
- Learn: Track both max and min (negative × negative = positive)
- Mistake: Only tracking max; missing the min flip on negative numbers
- Complexity: O(n) time, O(1) space
- Edge: All negatives, single element, zeros reset the product
Word Break — LC 139 | Medium
- Learn: dp[i] = can s[0..i-1] be segmented; check all valid word endings
- Mistake: Not using a set for O(1) word lookup
- Complexity: O(n²·m) time where m = avg word length, O(n) space
- Edge: Empty string → true; word longer than s
Longest Increasing Subsequence — LC 300 | Medium
- Learn: O(n log n) with patience sorting / binary search on
tailsarray - Mistake: Using O(n²) DP when O(n log n) is expected
- Complexity: O(n log n) time, O(n) space
- Edge: All same elements → LIS = 1; strictly increasing
Partition Equal Subset Sum — LC 416 | Medium
- Learn: 0/1 knapsack; can we reach sum/2? Iterate capacity backwards
- Mistake: Iterating capacity forward (allows reuse of same item)
- Complexity: O(n·sum) time, O(sum) space
- Edge: Odd total sum → false immediately; single element
Combination Sum IV — LC 377 | Medium
- Learn: Unbounded knapsack counting ordered arrangements (permutations, not combinations)
- Mistake: Confusing with Coin Change II (unordered); loop order matters
- Complexity: O(n·target) time, O(target) space
- Edge: target=0 → 1 (empty combination)
Perfect Squares — LC 279 | Medium
- Learn: Unbounded knapsack; dp[i] = min squares summing to i
- Mistake: Not precomputing squares; recomputing sqrt each time
- Complexity: O(n·√n) time, O(n) space
- Edge: n=1 → 1; perfect square input → 1
Integer Break — LC 343 | Medium
- Learn: dp[i] = max product splitting i; or math: use 3s greedily
- Mistake: Not considering keeping a factor as-is (not splitting further)
- Complexity: O(n) time, O(n) space (DP) or O(1) (math)
- Edge: n=2 → 1, n=3 → 2
Stone Game III — LC 1406 | Hard
- Learn: dp[i] = max score difference (current - opponent) from index i onward; pick 1, 2, or 3 stones
- Mistake: Storing absolute scores instead of score difference
- Complexity: O(n) time, O(1) space (rolling 3 vars)
- Edge: Single stone, all negative values
General Edge Cases
- Empty input / n=0
- Single element arrays
- All elements equal
- Negative numbers (product DP)
- Overflow for large sums (use long if needed)
- Target = 0 (usually base case = 1 or true)
| # | Problem | LC # | Difficulty | Key Technique |
|---|---|---|---|---|
| 1 | Climbing Stairs | 70 | Easy | Linear DP (Fibonacci) |
| 2 | Min Cost Climbing Stairs | 746 | Easy | Linear DP |
| 3 | N-th Tribonacci | 1137 | Easy | Linear DP (3 vars) |
| 4 | House Robber | 198 | Medium | Linear DP (skip adjacent) |
| 5 | House Robber II | 213 | Medium | Linear DP (circular, 2 passes) |
| 6 | Longest Palindromic Substring | 5 | Medium | Expand around center |
| 7 | Palindromic Substrings | 647 | Medium | Expand around center |
| 8 | Decode Ways | 91 | Medium | Linear DP (conditional transitions) |
| 9 | Coin Change | 322 | Medium | Unbounded knapsack |
| 10 | Maximum Product Subarray | 152 | Medium | Track min & max |
| 11 | Word Break | 139 | Medium | DP + set lookup |
| 12 | Longest Increasing Subsequence | 300 | Medium | DP / Binary search |
| 13 | Partition Equal Subset Sum | 416 | Medium | 0/1 Knapsack |
| 14 | Combination Sum IV | 377 | Medium | Unbounded knapsack (order matters) |
| 15 | Perfect Squares | 279 | Medium | Unbounded knapsack |
| 16 | Integer Break | 343 | Medium | DP / Math |
| 17 | Stone Game III | 1406 | Hard | Game theory DP |
Climbing Stairs ▼ expand
"""
LC 70 — Climbing Stairs
=======================
You are climbing a staircase. It takes `n` steps to reach the top.
Each time you can either climb 1 or 2 steps.
In how many distinct ways can you climb to the top?
Examples
--------
```text
Input: n = 2
Output: 2
Explanation: 1+1, 2
Input: n = 3
Output: 3
Explanation: 1+1+1, 1+2, 2+1
```
Constraints
-----------
- 1 <= n <= 45
"""
from functools import lru_cache
class Solution:
# Approach 1: Recursion with memoization — O(n) time, O(n) space
def climbStairs(self, n: int) -> int:
# dp(i) = ways to reach step i = dp(i-1) + dp(i-2) (Fibonacci-like)
@lru_cache(maxsize=None)
def dp(i: int) -> int:
if i <= 1:
return 1
return dp(i - 1) + dp(i - 2)
return dp(n)
# Approach 2: Bottom-up DP — O(n) time, O(n) space
def climbStairs_dp(self, n: int) -> int:
# Bottom-up: fill table[i] = table[i-1] + table[i-2]
if n <= 1:
return 1
table = [0] * (n + 1)
table[0], table[1] = 1, 1
for i in range(2, n + 1):
table[i] = table[i - 1] + table[i - 2]
return table[n]
# Approach 3: Space-optimized — O(n) time, O(1) space
def climbStairs_opt(self, n: int) -> int:
# Space-optimized: only keep last two values
a, b = 1, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
if __name__ == "__main__":
sol = Solution()
assert sol.climbStairs(1) == 1
assert sol.climbStairs(2) == 2
assert sol.climbStairs(3) == 3
assert sol.climbStairs(5) == 8
assert sol.climbStairs_dp(3) == 3
assert sol.climbStairs_opt(3) == 3
print("All tests passed.") Coin Change ▼ expand
"""
LC 322 — Coin Change
=====================
You are given an integer array `coins` representing coins of different denominations
and an integer `amount` representing a total amount of money.
Return the fewest number of coins needed to make up that amount.
If that amount cannot be made up, return -1.
You may assume you have an infinite number of each coin.
Examples
--------
```text
Input: coins = [1, 2, 5], amount = 11
Output: 3
Explanation: 5 + 5 + 1 = 11
Input: coins = [2], amount = 3
Output: -1
Input: coins = [1], amount = 0
Output: 0
```
Constraints
-----------
- 1 <= coins.length <= 12
- 1 <= coins[i] <= 2^31 - 1
- 0 <= amount <= 10^4
"""
from typing import List
from functools import lru_cache
class Solution:
# Approach 1: Top-down memoization — O(amount * n) time, O(amount) space
def coinChange(self, coins: List[int], amount: int) -> int:
# dp(rem) = 1 + min(dp(rem-c) for each coin c); base: dp(0)=0
@lru_cache(maxsize=None)
def dp(rem: int) -> int:
if rem == 0:
return 0
if rem < 0:
return float('inf')
return min(1 + dp(rem - c) for c in coins)
result = dp(amount)
return result if result != float('inf') else -1
# Approach 2: Bottom-up DP — O(amount * n) time, O(amount) space
def coinChange_dp(self, coins: List[int], amount: int) -> int:
# dp[i] = min coins to make amount i; try each coin and take minimum
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for c in coins:
if c <= i:
dp[i] = min(dp[i], 1 + dp[i - c])
return dp[amount] if dp[amount] != float('inf') else -1
if __name__ == "__main__":
sol = Solution()
assert sol.coinChange([1, 2, 5], 11) == 3
assert sol.coinChange([2], 3) == -1
assert sol.coinChange([1], 0) == 0
assert sol.coinChange([1, 5, 11], 11) == 1
assert sol.coinChange_dp([1, 2, 5], 11) == 3
assert sol.coinChange_dp([2], 3) == -1
print("All tests passed.") Combination Sum Iv ▼ expand
"""
LC 377 — Combination Sum IV
=============================
Given an array of distinct integers `nums` and a target integer `target`,
return the number of possible combinations that add up to `target`.
The order of the combination matters (permutations count separately).
Examples
--------
```text
Input: nums = [1, 2, 3], target = 4
Output: 7
Explanation:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
Input: nums = [9], target = 3
Output: 0
```
Constraints
-----------
- 1 <= nums.length <= 200
- 1 <= nums[i] <= 1000
- All elements of nums are unique
- 1 <= target <= 1000
"""
from typing import List
from functools import lru_cache
class Solution:
# Approach 1: Top-down memoization — O(target * n) time, O(target) space
def combinationSum4(self, nums: List[int], target: int) -> int:
# dp(rem) = sum of dp(rem-num) for each num <= rem; order matters (permutations)
@lru_cache(maxsize=None)
def dp(rem: int) -> int:
if rem == 0:
return 1
return sum(dp(rem - num) for num in nums if num <= rem)
return dp(target)
# Approach 2: Bottom-up DP — O(target * n) time, O(target) space
def combinationSum4_dp(self, nums: List[int], target: int) -> int:
# dp[i] = number of ordered combinations summing to i
dp = [0] * (target + 1)
dp[0] = 1
for i in range(1, target + 1):
for num in nums:
if num <= i:
dp[i] += dp[i - num]
return dp[target]
if __name__ == "__main__":
sol = Solution()
assert sol.combinationSum4([1, 2, 3], 4) == 7
assert sol.combinationSum4([9], 3) == 0
assert sol.combinationSum4_dp([1, 2, 3], 4) == 7
assert sol.combinationSum4_dp([9], 3) == 0
print("All tests passed.") Decode Ways ▼ expand
"""
LC 91 — Decode Ways
====================
A message containing letters A-Z can be encoded into numbers using:
'A' -> "1", 'B' -> "2", ..., 'Z' -> "26"
Given a string `s` containing only digits, return the number of ways to decode it.
Examples
--------
```text
Input: s = "12"
Output: 2
Explanation: "AB" (1 2) or "L" (12)
Input: s = "226"
Output: 3
Explanation: "BZ" (2 26), "VF" (22 6), "BBF" (2 2 6)
Input: s = "06"
Output: 0
Explanation: "06" cannot be mapped to "F" because of the leading zero.
```
Constraints
-----------
- 1 <= s.length <= 100
- s contains only digits
- s may contain leading zeros
"""
from functools import lru_cache
class Solution:
# Approach 1: Top-down memoization — O(n) time, O(n) space
def numDecodings(self, s: str) -> int:
# dp(i): ways to decode s[i:]; '0' has 0 ways; two-digit check for 10-26
@lru_cache(maxsize=None)
def dp(i: int) -> int:
if i == len(s):
return 1
if s[i] == '0':
return 0
result = dp(i + 1)
if i + 1 < len(s) and int(s[i:i + 2]) <= 26:
result += dp(i + 2)
return result
return dp(0)
# Approach 2: Bottom-up DP — O(n) time, O(n) space
def numDecodings_dp(self, s: str) -> int:
# Bottom-up from end; dp[i] = dp[i+1] + dp[i+2] if two-digit valid
n = len(s)
dp = [0] * (n + 1)
dp[n] = 1
for i in range(n - 1, -1, -1):
if s[i] != '0':
dp[i] = dp[i + 1]
if i + 1 < n and int(s[i:i + 2]) <= 26:
dp[i] += dp[i + 2]
return dp[0]
# Approach 3: Space-optimized — O(n) time, O(1) space
def numDecodings_opt(self, s: str) -> int:
# Space-optimized: next1=dp[i+1], next2=dp[i+2]; roll forward
n = len(s)
next2, next1 = 1, 1 # dp[n+1]=1 (sentinel), dp[n]=1 (base case)
for i in range(n - 1, -1, -1):
curr = 0
if s[i] != '0':
curr = next1
if i + 1 < n and int(s[i:i + 2]) <= 26:
curr += next2
next2, next1 = next1, curr
return next1
if __name__ == "__main__":
sol = Solution()
assert sol.numDecodings("12") == 2
assert sol.numDecodings("226") == 3
assert sol.numDecodings("06") == 0
assert sol.numDecodings("0") == 0
assert sol.numDecodings_dp("226") == 3
assert sol.numDecodings_opt("226") == 3
print("All tests passed.") House Robber ▼ expand
"""
LC 198 — House Robber
======================
You are a professional robber planning to rob houses along a street.
Each house has a certain amount of money stashed. Adjacent houses have
security systems connected — you cannot rob two adjacent houses.
Given an integer array `nums` representing the amount of money of each house,
return the maximum amount of money you can rob tonight without alerting the police.
Examples
--------
```text
Input: nums = [1, 2, 3, 1]
Output: 4
Explanation: Rob house 1 (1) + house 3 (3) = 4
Input: nums = [2, 7, 9, 3, 1]
Output: 12
Explanation: Rob house 1 (2) + house 3 (9) + house 5 (1) = 12
```
Constraints
-----------
- 1 <= nums.length <= 100
- 0 <= nums[i] <= 400
"""
from typing import List
from functools import lru_cache
class Solution:
# Approach 1: Top-down memoization — O(n) time, O(n) space
def rob(self, nums: List[int]) -> int:
# dp(i) = max(rob house i + dp(i+2), skip house i = dp(i+1))
n = len(nums)
@lru_cache(maxsize=None)
def dp(i: int) -> int:
if i >= n:
return 0
return max(nums[i] + dp(i + 2), dp(i + 1))
return dp(0)
# Approach 2: Bottom-up DP — O(n) time, O(n) space
def rob_dp(self, nums: List[int]) -> int:
# dp[i] = max(dp[i-1], dp[i-2] + nums[i]): rob or skip current house
n = len(nums)
if n == 1:
return nums[0]
dp = [0] * n
dp[0], dp[1] = nums[0], max(nums[0], nums[1])
for i in range(2, n):
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
return dp[-1]
# Approach 3: Space-optimized — O(n) time, O(1) space
def rob_opt(self, nums: List[int]) -> int:
# Rolling two variables: prev2=rob two ago, prev1=best so far
prev2, prev1 = 0, 0
for num in nums:
prev2, prev1 = prev1, max(prev1, prev2 + num)
return prev1
if __name__ == "__main__":
sol = Solution()
assert sol.rob([1, 2, 3, 1]) == 4
assert sol.rob([2, 7, 9, 3, 1]) == 12
assert sol.rob([1]) == 1
assert sol.rob_dp([1, 2, 3, 1]) == 4
assert sol.rob_opt([1, 2, 3, 1]) == 4
print("All tests passed.") House Robber Ii ▼ expand
"""
LC 213 — House Robber II
=========================
All houses are arranged in a circle. The first and last houses are adjacent.
You cannot rob two adjacent houses. Return the maximum amount you can rob.
Examples
--------
```text
Input: nums = [2, 3, 2]
Output: 3
Explanation: Cannot rob house 1 (2) and house 3 (2) since they are adjacent.
Input: nums = [1, 2, 3]
Output: 3
Input: nums = [1, 2, 3, 1]
Output: 4
```
Constraints
-----------
- 1 <= nums.length <= 100
- 0 <= nums[i] <= 1000
"""
from typing import List
class Solution:
# Approach 1: Two linear passes (exclude first or exclude last) — O(n) time, O(1) space
def rob(self, nums: List[int]) -> int:
# Circular constraint: can't rob both first and last house
# Run linear rob on [0..n-2] and [1..n-1]; take the max
if len(nums) == 1:
return nums[0]
def rob_linear(houses: List[int]) -> int:
prev2, prev1 = 0, 0
for num in houses:
prev2, prev1 = prev1, max(prev1, prev2 + num)
return prev1
return max(rob_linear(nums[:-1]), rob_linear(nums[1:]))
if __name__ == "__main__":
sol = Solution()
assert sol.rob([2, 3, 2]) == 3
assert sol.rob([1, 2, 3]) == 3
assert sol.rob([1, 2, 3, 1]) == 4
assert sol.rob([1]) == 1
print("All tests passed.") Integer Break ▼ expand
"""
LC 343 — Integer Break
=======================
Given an integer `n`, break it into the sum of k positive integers (k >= 2),
and maximize the product of those integers. Return the maximum product.
Examples
--------
```text
Input: n = 2
Output: 1
Explanation: 2 = 1 + 1, 1 * 1 = 1
Input: n = 10
Output: 36
Explanation: 10 = 3 + 3 + 4, 3 * 3 * 4 = 36
```
Constraints
-----------
- 2 <= n <= 58
"""
class Solution:
# Approach 1: DP — O(n^2) time, O(n) space
def integerBreak(self, n: int) -> int:
# dp[i] = max product from breaking i; try all splits j and (i-j)
# Either keep (i-j) as-is or break it further: max(i-j, dp[i-j])
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
for j in range(1, i):
dp[i] = max(dp[i], j * (i - j), j * dp[i - j])
return dp[n]
# Approach 2: Math (greedy with 3s) — O(n) time, O(1) space
def integerBreak_math(self, n: int) -> int:
# Greedy: break into 3s as much as possible (3s maximize product)
# Handle remainders 2 and 4 specially
if n == 2:
return 1
if n == 3:
return 2
product = 1
while n > 4:
product *= 3
n -= 3
return product * n
if __name__ == "__main__":
sol = Solution()
assert sol.integerBreak(2) == 1
assert sol.integerBreak(10) == 36
assert sol.integerBreak(3) == 2
assert sol.integerBreak_math(2) == 1
assert sol.integerBreak_math(10) == 36
assert sol.integerBreak_math(3) == 2
print("All tests passed.") Longest Increasing Subsequence ▼ expand
"""
LC 300 — Longest Increasing Subsequence
=========================================
Given an integer array `nums`, return the length of the longest strictly
increasing subsequence.
Examples
--------
```text
Input: nums = [10, 9, 2, 5, 3, 7, 101, 18]
Output: 4
Explanation: [2, 3, 7, 101]
Input: nums = [0, 1, 0, 3, 2, 3]
Output: 4
Input: nums = [7, 7, 7, 7, 7, 7, 7]
Output: 1
```
Constraints
-----------
- 1 <= nums.length <= 2500
- -10^4 <= nums[i] <= 10^4
"""
from typing import List
import bisect
class Solution:
# Approach 1: DP — O(n^2) time, O(n) space
def lengthOfLIS(self, nums: List[int]) -> int:
# dp[i] = LIS ending at index i; for each j < i where nums[j] < nums[i], dp[i] = max(dp[i], dp[j]+1)
n = len(nums)
dp = [1] * n
for i in range(1, n):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
# Approach 2: Binary search (patience sorting) — O(n log n) time, O(n) space
def lengthOfLIS_bs(self, nums: List[int]) -> int:
# Patience sorting: maintain 'tails' array; binary search for insertion position
# tails[i] = smallest tail of all LIS of length i+1
tails = []
for num in nums:
pos = bisect.bisect_left(tails, num)
if pos == len(tails):
tails.append(num)
else:
tails[pos] = num
return len(tails)
if __name__ == "__main__":
sol = Solution()
assert sol.lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18]) == 4
assert sol.lengthOfLIS([0, 1, 0, 3, 2, 3]) == 4
assert sol.lengthOfLIS([7, 7, 7, 7]) == 1
assert sol.lengthOfLIS_bs([10, 9, 2, 5, 3, 7, 101, 18]) == 4
assert sol.lengthOfLIS_bs([7, 7, 7, 7]) == 1
print("All tests passed.") Longest Palindromic Substring ▼ expand
"""
LC 5 — Longest Palindromic Substring
======================================
Given a string `s`, return the longest palindromic substring in `s`.
Examples
--------
```text
Input: s = "babad"
Output: "bab"
Note: "aba" is also a valid answer.
Input: s = "cbbd"
Output: "bb"
```
Constraints
-----------
- 1 <= s.length <= 1000
- s consists of only digits and English letters
"""
class Solution:
# Approach 1: Brute force — O(n^3) time, O(1) space
def longestPalindrome_brute(self, s: str) -> str:
# Try all substrings; check each for palindrome property — O(n^3)
def is_palindrome(t: str) -> bool:
return t == t[::-1]
best = s[0]
for i in range(len(s)):
for j in range(i + 1, len(s) + 1):
if j - i > len(best) and is_palindrome(s[i:j]):
best = s[i:j]
return best
# Approach 2: Expand around center — O(n^2) time, O(1) space
def longestPalindrome(self, s: str) -> str:
# Expand around each center (odd and even length) to find longest palindrome
start, end = 0, 0
def expand(l: int, r: int) -> tuple:
while l >= 0 and r < len(s) and s[l] == s[r]:
l -= 1
r += 1
return l + 1, r - 1
for i in range(len(s)):
l1, r1 = expand(i, i)
l2, r2 = expand(i, i + 1)
if r1 - l1 > end - start:
start, end = l1, r1
if r2 - l2 > end - start:
start, end = l2, r2
return s[start:end + 1]
# Approach 3: DP table — O(n^2) time, O(n^2) space
def longestPalindrome_dp(self, s: str) -> str:
# dp[i][j] = True if s[i..j] is palindrome; build from shorter to longer
n = len(s)
dp = [[False] * n for _ in range(n)]
start, max_len = 0, 1
for i in range(n):
dp[i][i] = True
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j]:
dp[i][j] = length == 2 or dp[i + 1][j - 1]
if dp[i][j] and length > max_len:
start, max_len = i, length
return s[start:start + max_len]
if __name__ == "__main__":
sol = Solution()
assert sol.longestPalindrome("babad") in ("bab", "aba")
assert sol.longestPalindrome("cbbd") == "bb"
assert sol.longestPalindrome("a") == "a"
assert sol.longestPalindrome_brute("babad") in ("bab", "aba")
assert sol.longestPalindrome_dp("babad") in ("bab", "aba")
print("All tests passed.") Maximum Product Subarray ▼ expand
"""
LC 152 — Maximum Product Subarray
===================================
Given an integer array `nums`, find a contiguous subarray that has the largest product,
and return the product.
Examples
--------
```text
Input: nums = [2, 3, -2, 4]
Output: 6
Explanation: [2, 3] has the largest product 6.
Input: nums = [-2, 0, -1]
Output: 0
Explanation: The result cannot be 2 because [-2, -1] is not a subarray.
```
Constraints
-----------
- 1 <= nums.length <= 2 * 10^4
- -10 <= nums[i] <= 10
- The product of any subarray of nums is guaranteed to fit in a 32-bit integer.
"""
from typing import List
class Solution:
# Approach 1: Brute force — O(n^2) time, O(1) space
def maxProduct_brute(self, nums: List[int]) -> int:
# Try all subarrays; track running product — O(n^2)
best = nums[0]
for i in range(len(nums)):
prod = 1
for j in range(i, len(nums)):
prod *= nums[j]
best = max(best, prod)
return best
# Approach 2: Track running min and max — O(n) time, O(1) space
def maxProduct(self, nums: List[int]) -> int:
# Track both max and min products (negatives can flip sign)
# At each step, new max/min comes from: num alone, num*prev_max, num*prev_min
cur_max = cur_min = best = nums[0]
for num in nums[1:]:
candidates = (num, cur_max * num, cur_min * num)
cur_max, cur_min = max(candidates), min(candidates)
best = max(best, cur_max)
return best
if __name__ == "__main__":
sol = Solution()
assert sol.maxProduct([2, 3, -2, 4]) == 6
assert sol.maxProduct([-2, 0, -1]) == 0
assert sol.maxProduct([-2]) == -2
assert sol.maxProduct([0, 2]) == 2
assert sol.maxProduct_brute([2, 3, -2, 4]) == 6
print("All tests passed.") Min Cost Climbing Stairs ▼ expand
"""
LC 746 — Min Cost Climbing Stairs
==================================
You are given an integer array `cost` where `cost[i]` is the cost of the i-th step.
Once you pay the cost, you can climb one or two steps.
You can either start from index 0 or index 1.
Return the minimum cost to reach the top of the floor (beyond the last index).
Examples
--------
```text
Input: cost = [10, 15, 20]
Output: 15
Explanation: Start at index 1, pay 15, climb 2 steps to the top.
Input: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
Output: 6
```
Constraints
-----------
- 2 <= cost.length <= 1000
- 0 <= cost[i] <= 999
"""
from typing import List
from functools import lru_cache
class Solution:
# Approach 1: Top-down memoization — O(n) time, O(n) space
def minCostClimbingStairs(self, cost: List[int]) -> int:
# dp(i) = cost[i] + min(dp(i+1), dp(i+2)); can start at 0 or 1
n = len(cost)
@lru_cache(maxsize=None)
def dp(i: int) -> int:
if i >= n:
return 0
return cost[i] + min(dp(i + 1), dp(i + 2))
return min(dp(0), dp(1))
# Approach 2: Bottom-up DP — O(n) time, O(n) space
def minCostClimbingStairs_dp(self, cost: List[int]) -> int:
# dp[i] = min cost to reach step i from step i-1 or i-2
n = len(cost)
dp = [0] * (n + 1)
for i in range(2, n + 1):
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
return dp[n]
# Approach 3: Space-optimized — O(n) time, O(1) space
def minCostClimbingStairs_opt(self, cost: List[int]) -> int:
# Rolling two variables for O(1) space
a, b = 0, 0
for i in range(2, len(cost) + 1):
a, b = b, min(b + cost[i - 1], a + cost[i - 2])
return b
if __name__ == "__main__":
sol = Solution()
assert sol.minCostClimbingStairs([10, 15, 20]) == 15
assert sol.minCostClimbingStairs([1, 100, 1, 1, 1, 100, 1, 1, 100, 1]) == 6
assert sol.minCostClimbingStairs_dp([10, 15, 20]) == 15
assert sol.minCostClimbingStairs_opt([10, 15, 20]) == 15
print("All tests passed.") N Th Tribonacci Number ▼ expand
"""
LC 1137 — N-th Tribonacci Number
==================================
The Tribonacci sequence is defined as:
T(0) = 0, T(1) = 1, T(2) = 1
T(n) = T(n-1) + T(n-2) + T(n-3) for n >= 3
Return the value of T(n).
Examples
--------
```text
Input: n = 4
Output: 4
Explanation: T(4) = T(3) + T(2) + T(1) = 2 + 1 + 1 = 4
Input: n = 25
Output: 1389537
```
Constraints
-----------
- 0 <= n <= 37
"""
class Solution:
# Approach 1: DP array — O(n) time, O(n) space
def tribonacci(self, n: int) -> int:
# dp[i] = dp[i-1] + dp[i-2] + dp[i-3]; base cases T(0)=0, T(1)=T(2)=1
if n == 0:
return 0
if n <= 2:
return 1
dp = [0] * (n + 1)
dp[1] = dp[2] = 1
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]
return dp[n]
# Approach 2: Space-optimized — O(n) time, O(1) space
def tribonacci_opt(self, n: int) -> int:
# Rolling three variables: a, b, c represent T(n-2), T(n-1), T(n)
if n == 0:
return 0
if n <= 2:
return 1
a, b, c = 0, 1, 1
for _ in range(3, n + 1):
a, b, c = b, c, a + b + c
return c
if __name__ == "__main__":
sol = Solution()
assert sol.tribonacci(0) == 0
assert sol.tribonacci(1) == 1
assert sol.tribonacci(2) == 1
assert sol.tribonacci(4) == 4
assert sol.tribonacci(25) == 1389537
assert sol.tribonacci_opt(4) == 4
assert sol.tribonacci_opt(25) == 1389537
print("All tests passed.") Palindromic Substrings ▼ expand
"""
LC 647 — Palindromic Substrings
=================================
Given a string `s`, return the number of palindromic substrings in it.
A string is a palindrome when it reads the same backward as forward.
A substring is a contiguous sequence of characters within the string.
Examples
--------
```text
Input: s = "abc"
Output: 3
Explanation: "a", "b", "c"
Input: s = "aaa"
Output: 6
Explanation: "a", "a", "a", "aa", "aa", "aaa"
```
Constraints
-----------
- 1 <= s.length <= 1000
- s consists of lowercase English letters
"""
class Solution:
# Approach 1: Expand around center — O(n^2) time, O(1) space
def countSubstrings(self, s: str) -> int:
# Expand around each center; count every valid palindrome found
count = 0
def expand(l: int, r: int) -> None:
nonlocal count
while l >= 0 and r < len(s) and s[l] == s[r]:
count += 1
l -= 1
r += 1
for i in range(len(s)):
expand(i, i)
expand(i, i + 1)
return count
# Approach 2: DP table — O(n^2) time, O(n^2) space
def countSubstrings_dp(self, s: str) -> int:
# dp[i][j] = True if s[i..j] is palindrome; count all True entries
n = len(s)
dp = [[False] * n for _ in range(n)]
count = 0
for length in range(1, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j] and (length <= 2 or dp[i + 1][j - 1]):
dp[i][j] = True
count += 1
return count
if __name__ == "__main__":
sol = Solution()
assert sol.countSubstrings("abc") == 3
assert sol.countSubstrings("aaa") == 6
assert sol.countSubstrings_dp("abc") == 3
assert sol.countSubstrings_dp("aaa") == 6
print("All tests passed.") Partition Equal Subset Sum ▼ expand
"""
LC 416 — Partition Equal Subset Sum
=====================================
Given an integer array `nums`, return true if you can partition the array into
two subsets such that the sum of the elements in both subsets is equal.
Examples
--------
```text
Input: nums = [1, 5, 11, 5]
Output: true
Explanation: [1, 5, 5] and [11]
Input: nums = [1, 2, 3, 5]
Output: false
```
Constraints
-----------
- 1 <= nums.length <= 200
- 1 <= nums[i] <= 100
"""
from typing import List
from functools import lru_cache
class Solution:
# Approach 1: Top-down memoization — O(n * sum) time, O(n * sum) space
def canPartition(self, nums: List[int]) -> bool:
# Reduce to subset sum: can we find a subset summing to total/2?
total = sum(nums)
if total % 2:
return False
target = total // 2
@lru_cache(maxsize=None)
def dp(i: int, rem: int) -> bool:
if rem == 0:
return True
if i == len(nums) or rem < 0:
return False
return dp(i + 1, rem - nums[i]) or dp(i + 1, rem)
return dp(0, target)
# Approach 2: Bottom-up DP (boolean set) — O(n * sum) time, O(sum) space
def canPartition_dp(self, nums: List[int]) -> bool:
# dp = set of reachable sums; for each num, add num to all existing sums
total = sum(nums)
if total % 2:
return False
target = total // 2
dp = {0}
for num in nums:
dp |= {s + num for s in dp if s + num <= target}
return target in dp
# Approach 3: Bitset optimization — O(n * sum / 64) time, O(sum / 64) space
def canPartition_bitset(self, nums: List[int]) -> bool:
# Bitset trick: bit i set means sum i is reachable; shift and OR for each num
total = sum(nums)
if total % 2:
return False
target = total // 2
bits = 1
for num in nums:
bits |= bits << num
return bool((bits >> target) & 1)
if __name__ == "__main__":
sol = Solution()
assert sol.canPartition([1, 5, 11, 5]) is True
assert sol.canPartition([1, 2, 3, 5]) is False
assert sol.canPartition_dp([1, 5, 11, 5]) is True
assert sol.canPartition_dp([1, 2, 3, 5]) is False
assert sol.canPartition_bitset([1, 5, 11, 5]) is True
assert sol.canPartition_bitset([1, 2, 3, 5]) is False
print("All tests passed.") Perfect Squares ▼ expand
"""
LC 279 — Perfect Squares
==========================
Given an integer `n`, return the least number of perfect square numbers that sum to `n`.
A perfect square is an integer that is the square of an integer (1, 4, 9, 16, ...).
Examples
--------
```text
Input: n = 12
Output: 3
Explanation: 12 = 4 + 4 + 4
Input: n = 13
Output: 2
Explanation: 13 = 4 + 9
```
Constraints
-----------
- 1 <= n <= 10^4
"""
from collections import deque
import math
class Solution:
# Approach 1: DP — O(n * sqrt(n)) time, O(n) space
def numSquares(self, n: int) -> int:
# dp[i] = min squares summing to i; try all perfect squares <= i
dp = [float('inf')] * (n + 1)
dp[0] = 0
squares = [i * i for i in range(1, int(math.isqrt(n)) + 1)]
for i in range(1, n + 1):
for sq in squares:
if sq > i:
break
dp[i] = min(dp[i], dp[i - sq] + 1)
return dp[n]
# Approach 2: BFS (level = number of squares used) — O(n * sqrt(n)) time, O(n) space
def numSquares_bfs(self, n: int) -> int:
# BFS levels = number of squares used; find shortest path from n to 0
squares = [i * i for i in range(1, int(math.isqrt(n)) + 1)]
visited = {n}
queue = deque([n])
steps = 0
while queue:
steps += 1
for _ in range(len(queue)):
node = queue.popleft()
for sq in squares:
nxt = node - sq
if nxt == 0:
return steps
if nxt > 0 and nxt not in visited:
visited.add(nxt)
queue.append(nxt)
return steps
if __name__ == "__main__":
sol = Solution()
assert sol.numSquares(12) == 3
assert sol.numSquares(13) == 2
assert sol.numSquares(1) == 1
assert sol.numSquares_bfs(12) == 3
assert sol.numSquares_bfs(13) == 2
print("All tests passed.") Stone Game Iii ▼ expand
"""
LC 1406 — Stone Game III
=========================
Alice and Bob take turns, with Alice going first. On each turn, a player can take
1, 2, or 3 stones from the front of the row. The player with the most stones wins.
Return "Alice" if Alice wins, "Bob" if Bob wins, or "Tie" if they end with the same score.
Both players play optimally.
Examples
--------
```text
Input: stoneValue = [1, 2, 3, 7]
Output: "Bob"
Explanation: Alice takes 1, 2, or 3 stones; Bob always wins.
Input: stoneValue = [1, 2, 3, -9]
Output: "Alice"
Input: stoneValue = [1, 2, 3, 6]
Output: "Tie"
```
Constraints
-----------
- 1 <= stoneValue.length <= 5 * 10^4
- -1000 <= stoneValue[i] <= 1000
"""
from typing import List
from functools import lru_cache
class Solution:
# Approach 1: Top-down memoization — O(n) time, O(n) space
def stoneGameIII(self, stoneValue: List[int]) -> str:
# dp(i) = max score advantage current player can get from index i
# Try taking 1, 2, or 3 stones; subtract opponent's optimal play
n = len(stoneValue)
@lru_cache(maxsize=None)
def dp(i: int) -> int:
"""Returns max score advantage current player can achieve from index i."""
if i >= n:
return 0
best = float('-inf')
total = 0
for k in range(1, 4):
if i + k - 1 < n:
total += stoneValue[i + k - 1]
best = max(best, total - dp(i + k))
return best
score = dp(0)
if score > 0:
return "Alice"
elif score < 0:
return "Bob"
return "Tie"
# Approach 2: Bottom-up DP — O(n) time, O(n) space
def stoneGameIII_dp(self, stoneValue: List[int]) -> str:
# Bottom-up: dp[i] = max(sum of k stones - dp[i+k]) for k in 1..3
n = len(stoneValue)
dp = [float('-inf')] * (n + 1)
dp[n] = 0
for i in range(n - 1, -1, -1):
total = 0
for k in range(1, 4):
if i + k <= n:
total += stoneValue[i + k - 1]
dp[i] = max(dp[i], total - dp[i + k])
score = dp[0]
if score > 0:
return "Alice"
elif score < 0:
return "Bob"
return "Tie"
if __name__ == "__main__":
sol = Solution()
assert sol.stoneGameIII([1, 2, 3, 7]) == "Bob"
assert sol.stoneGameIII([1, 2, 3, -9]) == "Alice"
assert sol.stoneGameIII([1, 2, 3, 6]) == "Tie"
assert sol.stoneGameIII_dp([1, 2, 3, 7]) == "Bob"
assert sol.stoneGameIII_dp([1, 2, 3, 6]) == "Tie"
print("All tests passed.") Word Break ▼ expand
"""
LC 139 — Word Break
====================
Given a string `s` and a dictionary of strings `wordDict`, return true if `s`
can be segmented into a space-separated sequence of one or more dictionary words.
The same word in the dictionary may be reused multiple times.
Examples
--------
```text
Input: s = "leetcode", wordDict = ["leet", "code"]
Output: true
Explanation: "leetcode" can be segmented as "leet code".
Input: s = "applepenapple", wordDict = ["apple", "pen"]
Output: true
Explanation: "applepenapple" can be segmented as "apple pen apple".
Input: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
Output: false
```
Constraints
-----------
- 1 <= s.length <= 300
- 1 <= wordDict.length <= 1000
- 1 <= wordDict[i].length <= 20
- s and wordDict[i] consist of only lowercase English letters
- All strings in wordDict are unique
"""
from typing import List
from functools import lru_cache
class Solution:
# Approach 1: Top-down memoization — O(n^2 * m) time, O(n) space
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
# dp(i): can s[i:] be segmented? Try all words starting at i
word_set = set(wordDict)
@lru_cache(maxsize=None)
def dp(i: int) -> bool:
if i == len(s):
return True
return any(s[i:j] in word_set and dp(j) for j in range(i + 1, len(s) + 1))
return dp(0)
# Approach 2: Bottom-up DP — O(n^2 * m) time, O(n) space
def wordBreak_dp(self, s: str, wordDict: List[str]) -> bool:
# dp[i] = True if s[:i] can be segmented; check all j < i where dp[j] and s[j:i] in dict
word_set = set(wordDict)
n = len(s)
dp = [False] * (n + 1)
dp[0] = True
for i in range(1, n + 1):
for j in range(i):
if dp[j] and s[j:i] in word_set:
dp[i] = True
break
return dp[n]
if __name__ == "__main__":
sol = Solution()
assert sol.wordBreak("leetcode", ["leet", "code"]) is True
assert sol.wordBreak("applepenapple", ["apple", "pen"]) is True
assert sol.wordBreak("catsandog", ["cats", "dog", "sand", "and", "cat"]) is False
assert sol.wordBreak_dp("leetcode", ["leet", "code"]) is True
assert sol.wordBreak_dp("catsandog", ["cats", "dog", "sand", "and", "cat"]) is False
print("All tests passed.") 1D Dynamic Programming
dp[i] = min coins to make amount i (unbounded knapsack)
Coin Change DP. Press "Start" to initialize the table.