Dynamic Programming Patterns
Solve each small question once, write the answer down, and build the big answer out of the small ones. DP is recursion with a memory, and it comes in a handful of recurring shapes you can learn to recognize.
1. What is this pattern for?
1+1+1+1+1+1+1+1 on a whiteboard and ask a friend for the total. They count: "8". Now write another +1 at the end. They answer "9" instantly. They didn't recount, because they remembered the 8. That's dynamic programming: remember answers to sub-questions so you never solve the same thing twice.DP applies when a problem has two properties:
- Optimal substructure: the best answer for the whole is built from best answers to smaller versions. ("Best loot from houses 0..i" uses "best loot from houses 0..i−1 and 0..i−2".)
- Overlapping subproblems: a naive recursion asks the same smaller question many times. Fibonacci's recursion computes
fib(2)again and again.
It's for questions that ask for the number of ways, the minimum/maximum cost or value, the longest/shortest something, or simply is it possible, where you'd otherwise have to try exponentially many combinations.
2. How it works (bisected)
The four-stage ladder (climb it for every problem)
- Recursion (brute force). Write
f(state)in terms of smaller states. It's correct but exponential, because it re-solves the same states. - Memoization (top-down). Add a cache: before computing
f(state), look it up; after computing it, store it. Each state is now solved once. Time = (#states) × (work per state). - Tabulation (bottom-up). Turn the cache into an array
dpand fill it in an order where everything a cell needs is already filled. No recursion, so no stack-depth limit. - Space optimization. Look at which cells the transition reads. If row
ionly reads rowi−1(or the last two values), keep just those.O(n)→O(1),O(m·n)→O(n).
Recipe: how to define the state & transition
- Name the state in plain English. Complete the sentence "
dp[i](ordp[i][j]) = the best / number of ways / is-possible for …the first i items… / …the prefix ending at i… / …the substring i..j… / …with capacity j…". If you can't say it in one sentence, the state is wrong. - Find the last decision. Ask "What's the last choice made in an optimal answer for this state?" (Rob house i or not? Use coin c last? Do the last characters match? Which balloon bursts last?) Each option points to a smaller state. That's your transition.
- Base cases. The smallest states you can answer without thinking: empty prefix, amount 0, empty string, first row/column.
- Fill order. Make sure every cell's dependencies are filled first: left-to-right for prefixes, by increasing length for intervals, backwards over capacity for 0/1 knapsack.
- Where's the answer?
dp[n],dp[m][n],dp[0][n-1], ormax(dp)when the state is "ending at i". - Squeeze the space once it works.
The DP families at a glance
| Family | State | Transition (the last decision) | Classic |
|---|---|---|---|
| 1-D linear | dp[i] = best for first i items | max(dp[i-1], dp[i-2] + nums[i]) | Climbing Stairs, House Robber |
| 0/1 knapsack | dp[c] = achievable with capacity c | dp[c] |= dp[c - w], looping c backwards | Partition Equal Subset Sum, Target Sum |
| Unbounded knapsack | dp[a] = best for amount a | min(dp[a - c] + 1), looping a forwards | Coin Change, Coin Change II |
| Two strings | dp[i][j] = answer for prefixes a[:i], b[:j] | match → diagonal; else combine up / left / diagonal | LCS, Edit Distance |
| LIS | dp[i] = longest ending at i | 1 + max(dp[j]) for j < i, nums[j] < nums[i] | LIS, Russian Doll Envelopes |
| Interval | dp[l][r] = best for the range l..r | try every split / last element k in (l, r) | Burst Balloons, Matrix Chain |
| Grid | dp[r][c] = best to reach (r, c) | from top and from left | Unique Paths, Min Path Sum |
| State machine | dp[i][state] | arrows between states (hold / sold / rest) | Stock with Cooldown / Fee |
3. Seeing a problem: how to spot Dynamic Programming
Trigger words
- "number of ways", "how many distinct…"
- "minimum cost / steps / coins", "maximum profit / value / score"
- "longest / shortest subsequence", "edit distance", "common subsequence"
- "can you reach / partition / form…?" (a yes/no answer over many choices)
- "you may not take two adjacent…", "cooldown", "at most k transactions"
Shape of the input
- An array, a string (or two), a grid, or an amount/capacity, with n up to ~10³–10⁴ (so
O(n²)orO(n·target)fits). - Each step is a choice (take/skip, which coin, which edit), and choices interact.
- Brute-force smell: your backtracking solution keeps arriving at the same (index, remaining) state through different paths. Cache that state and you have DP.
4. When to use it, and when not to
| Use DP when… | Prefer something else when… |
|---|---|
| You want a count or an optimum over exponentially many choices | You must list every configuration: Backtracking, since the output itself is exponential |
| Subproblems overlap (the same state is reached many ways) | Subproblems are independent (e.g. merge sort halves): plain divide & conquer, no cache needed |
| A locally best choice can be wrong (coins [1,3,4], amount 6: greedy picks 4+1+1, DP finds 3+3) | A greedy choice is provably safe (interval scheduling by end time, see Overlapping Intervals) |
| Grid paths moving only right/down (no cycles) | Moves go in all 4 directions and you want the fewest steps: BFS / Matrix Traversal |
| Contiguous subarray optimum with a "carry or restart" choice (Kadane) | Contiguous window with a monotone condition: Sliding Window; range sums: Prefix Sum |
LIS in O(n²) is fine | n is 10⁵: the O(n log n) "tails" trick uses Binary Search |
Cost formula: time = number of states × work per transition; space = number of states (often reducible by one dimension). House Robber: n states × O(1) = O(n). Coin Change: amount × coins. Edit Distance: m·n × O(1). Burst Balloons: n² states × n splits = O(n³).
5. The Python templates (memorize these)
# ---- The ladder on Fibonacci ------------------------------------------
# fib(0) = 0, fib(1) = 1, and every later number is the sum of the two before it.
def fib_rec(n): # 1) plain recursion: O(2^n) time
# base cases: fib(0) = 0 and fib(1) = 1
if n < 2:
return n
# each call makes TWO more calls, so the same values are recomputed over and over
return fib_rec(n - 1) + fib_rec(n - 2)
def fib_memo(n, memo=None): # 2) memoization (top-down): O(n) time
# memo is a dict that remembers answers we already computed: memo[k] = fib(k).
# We create it on the first call (memo is None) and pass the SAME dict down every call.
if memo is None:
memo = {}
if n < 2:
return n
# already solved? just look it up instead of recomputing
if n in memo:
return memo[n]
# solve once, store the answer, then return it
answer = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
memo[n] = answer
return answer
def fib_tab(n): # 3) tabulation (bottom-up): O(n) time, O(n) space
if n < 2:
return n
# dp[i] will hold fib(i); [0] * (n + 1) makes a list of n + 1 zeros
dp = [0] * (n + 1)
dp[1] = 1
# fill the table from small to large, so dp[i - 1] and dp[i - 2] are always ready
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
def fib_opt(n): # 4) space-optimized: O(1) space
# the table only ever reads the last two cells, so keep just two variables
older = 0 # fib(i) (starts as fib(0))
newer = 1 # fib(i + 1) (starts as fib(1))
for step in range(n):
# next_value must be computed BEFORE we overwrite older,
# because it needs the old value of older
next_value = older + newer
older = newer
newer = next_value
return older
# ---- 0/1 knapsack: each item at most once -> capacity loop BACKWARDS ----
def knapsack_01(weights, values, capacity):
# dp[cap] = best value that fits in a bag of size cap; [0] * k = k zeros (take nothing)
dp = [0] * (capacity + 1)
for i in range(len(weights)):
weight = weights[i]
value = values[i]
# walk cap DOWN from capacity to weight: dp[cap - weight] has not been
# updated for this item yet, so it still means "without this item"
cap = capacity
while cap >= weight:
dp[cap] = max(dp[cap], dp[cap - weight] + value)
cap = cap - 1
return dp[capacity]
# ---- Unbounded knapsack: items reusable -> loop FORWARDS -----------------
def unbounded(weights, values, capacity):
# dp[cap] = best value for a bag of size cap, each item may be used many times
dp = [0] * (capacity + 1)
for cap in range(1, capacity + 1):
for i in range(len(weights)):
weight = weights[i]
value = values[i]
if weight <= cap:
# dp[cap - weight] may ALREADY include this item: that is what allows reuse
dp[cap] = max(dp[cap], dp[cap - weight] + value)
return dp[capacity]
# ---- Two strings: dp[i][j] over prefixes a[:i], b[:j] ------------------
def two_strings(a, b):
len_a = len(a)
len_b = len(b)
# build a (len_a + 1) x (len_b + 1) table of zeros, one row at a time;
# row 0 / column 0 stand for the empty prefix
dp = []
for i in range(len_a + 1):
dp.append([0] * (len_b + 1))
for i in range(1, len_a + 1):
for j in range(1, len_b + 1):
# dp index i matches string index i - 1 (the extra row shifts everything by one)
if a[i - 1] == b[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1 # (LCS version) extend the diagonal
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) # drop a character from one side
return dp[len_a][len_b]
# ---- Interval DP: fill by increasing length ----------------------------
def interval(size, score):
# score is a function passed in: score(left, last, right) = value of picking "last" inside (left, right)
# build a size x size table of zeros: dp[left][right] = best answer strictly between left and right
dp = []
for i in range(size):
dp.append([0] * size)
# short intervals first, so the smaller pieces are ready when a longer one needs them
for length in range(2, size):
for left in range(size - length):
right = left + length
best = float("-inf") # float("-inf") is smaller than every number
for last in range(left + 1, right):
candidate = dp[left][last] + score(left, last, right) + dp[last][right]
best = max(best, candidate)
dp[left][right] = best
return dp[0][size - 1]
# ---- State machine: one variable per state, update all at once ---------
def stock_cooldown(prices):
# float("-inf") = "impossible": you can't hold or have sold anything before day 0
hold = float("-inf")
sold = float("-inf")
rest = 0
for i in range(len(prices)):
price = prices[i]
# every new value must use YESTERDAY's values, so compute all three into
# temporary variables first, then copy them over
new_hold = max(hold, rest - price)
new_sold = hold + price
new_rest = max(rest, sold)
hold = new_hold
sold = new_sold
rest = new_rest
return max(sold, rest)
What the code is doing, line by line:
- The ladder, stage 1 (
fib_rec): the recurrence written directly. It is correct but slow:fib_rec(5)callsfib_rec(3)twice,fib_rec(2)three times, and so on, so the work roughly doubles with every step ofn. - Stage 2 (
fib_memo): the same recursion plus a dictionarymemo. Before computingfib(n)we checkif n in memo; after computing it we storememo[n] = answer. Each value is now computed once, so the time drops toO(n). Thememo=Nonedefault plusif memo is None: memo = {}gives every top-level call a fresh dictionary, and passingmemointo the recursive calls makes them all share it. - Stage 3 (
fib_tab): no recursion at all. A listdpis filled from index 2 upwards, and each cell only reads two cells that were filled earlier. - Stage 4 (
fib_opt): since only the last two cells are read, two variables replace the whole list.next_valueis a temporary: if we wroteolder = newerfirst, the oldolderwould be lost before we could add it. The dry run below shows stages 3 and 4 side by side:olderafternloop steps equalsdp[n]. - 0/1 knapsack:
dp[cap]is the best value for a bag of sizecap. For each item, awhileloop walkscapfromcapacitydown toweight. Going downwards meansdp[cap - weight]was not touched yet for this item, so the item can be counted at most once. - Unbounded knapsack: the same update, but
capgoes upwards. Nowdp[cap - weight]may already contain this item, which is exactly what lets an item be reused. - Two strings: the table is built row by row with
append(one row of zeros per prefix ofa). Celldp[i][j]compares the last charactersa[i - 1]andb[j - 1]: on a match it extends the diagonal, otherwise it keeps the better of dropping one character from either string. - Interval DP:
dp[left][right]is the best answer for the open interval betweenleftandright. The outer loop goes overlengthfrom short to long; the inner loop tries every split pointlastand keeps the maximum inbest(which starts at minus infinity so the first candidate always wins). - State machine: one variable per state (
hold,sold,rest). All three new values are computed intonew_hold,new_sold,new_restbefore any variable is overwritten, so each one is based on yesterday's numbers only.
Dry run on the example:
| n | dp[n] in fib_tab | older after n loop steps | newer after n loop steps |
|---|---|---|---|
| 0 | 0 | 0 | 1 |
| 1 | 1 | 1 | 1 |
| 2 | 1 | 1 | 2 |
| 3 | 2 | 2 | 3 |
| 4 | 3 | 3 | 5 |
| 5 | 5 | 5 | 8 |
| 6 | 8 | 8 | 13 |
| 7 | 13 | 13 | 21 |
6. How to remember it forever
How this hook works: what “Last decision, written down” means
What the slogan means, phrase by phrase
- “Last decision”
- To solve the problem for size
n, ask "what was the very last choice?" (the last stair step, the last coin, rob this house or not). Each option leaves a smaller version of the same problem, and that gives the transition. - “written down”
- Store each smaller answer the first time you compute it (a memo dict or a
dptable), so no sub-question is ever solved twice.
Why this picture fits the pattern: Dynamic programming is recursion plus memory. Plain recursion re-solves the same sub-questions exponentially often (see the fib(4) tree on the Start here page). The last decision gives you the recursion and writing it down removes the repeats.
The story → the code
| In the story | In the code |
|---|---|
| Name the state in one sentence | "dp[i] = number of ways to reach stair i" |
| The last decision | the transition dp[i] = dp[i - 1] + dp[i - 2] |
| Answers that need no decision | base cases dp[1], dp[2] |
| Write it down | memo[n] = answer or the dp list |
| Fill in an order that's always ready | for i in range(3, n + 1) |
Watch it on numbers
Climbing Stairs, n = 5, taking 1 or 2 steps at a time.
- Last decision: the final move was 1 step (from stair 4) or 2 steps (from stair 3). So ways(5) = ways(4) + ways(3).
- Base: ways(1) = 1, ways(2) = 2.
- Write down: ways(3) = 3, ways(4) = 5, ways(5) = 8. Each value is computed once.
- House Robber
[2, 7, 9, 3, 1]: the last decision is "rob house i or skip it". The table is 2, 7, 11, 11, 12 (2 + 9 + 1).
The trap it saves you from
Writing code before naming the state, which gives a muddled transition. Also, filling the table in the wrong order reads a cell that's still 0. In 0/1 knapsack, a forward capacity loop lets you use one item twice: "Once? Go backwards."
It still works when…
Coin Change: the last coin used was one of the coins, so dp[a] = 1 + min(dp[a - c]). Longest Common Subsequence: the last characters either match or one is dropped. It's the same question every time, on a 2-D table.
Picture
A spreadsheet. Each cell has a formula that only points to cells above it or to its left. You fill it top-left to bottom-right, and the answer appears in the last cell.
Chant
"State, choice, base, order, answer." Then: "Recurse, remember, tabulate, trim."
Knapsack rule
"Once? Go backwards. Again and again? Go forwards." Coin Change II (count combinations): coins outside, amounts inside.
7. LeetCode problems, solved step by step
Medium 198. House Robber (1-D linear) Start here Google Meta Amazon Cisco Microsoft
Problem: Houses in a row hold nums[i] money. You can't rob two adjacent houses. Maximize the loot.
Solve it on LeetCode ↗ · Video walkthrough ↗
nums = [1,2,3,1]4nums = [2,7,9,3,1]12Constraints: 1 ≤ nums.length ≤ 100 · 0 ≤ nums[i] ≤ 400
Hint 1
At each house you either rob it (then you couldn't rob the previous one) or skip it.
Hint 2
dp[i] = max(dp[i − 1], dp[i − 2] + nums[i]). You only need the last two values.
From brute force to the pattern: Trying every set of non-adjacent houses is O(2ⁿ), and plain recursion recomputes the same sub-problems. Each prefix has one best answer, so DP fills them in order: O(n) time, O(1) space.
Spot it: "maximum" + "cannot take two adjacent" + an array. Each house is a take/skip choice, and the choices interact.
State & transition: dp[i] = best loot from houses 0..i. Last decision: rob house i (then house i−1 is off-limits → dp[i−2] + nums[i]) or skip it (dp[i−1]). The 1-D visualizer above fills this table.
Approach (step by step):
- State:
dp[i]= the most money from houses0..i. - Transition:
dp[i] = max(dp[i−1], dp[i−2] + nums[i]), i.e. skip house i or rob it. - Base: before any house the loot is 0, so both "previous" values start at 0.
- Space: only the last two values are ever read, so keep two variables
prev2andprev1. - Answer:
prev1after the last house.
from typing import List
class Solution:
def rob(self, nums: List[int]) -> int:
# prev2 = dp[i - 2] and prev1 = dp[i - 1]; before any house the loot is 0
prev2 = 0
prev1 = 0
for i in range(len(nums)):
money = nums[i]
# last decision for house i: skip it (keep prev1) or rob it (prev2 + money)
best_here = max(prev1, prev2 + money)
# slide the two-value window one house to the right
prev2 = prev1
prev1 = best_here
return prev1
What the code is doing, line by line:
prev2andprev1are the last two cells of thedptable (dp[i - 2]anddp[i - 1]). Both start at 0 because before any house you have robbed nothing.- The loop visits every house by index and reads its money into
money. best_hereisdp[i]: the better of skipping housei(the best so far staysprev1) or robbing it (then housei - 1is off-limits, so addmoneytoprev2).- Shift the window: the old
prev1becomesprev2, andbest_herebecomesprev1. The order matters:prev2 = prev1must happen beforeprev1is overwritten. - After the last house,
prev1is the best loot for the whole street.
Dry run on the example:
nums = [2, 7, 9, 3, 1] (answer 12 = 2 + 9 + 1):
| i | money | skip = prev1 | rob = prev2 + money | choice | new prev2 | new prev1 |
|---|---|---|---|---|---|---|
| 0 | 2 | 0 | 2 | rob | 0 | 2 |
| 1 | 7 | 2 | 7 | rob | 2 | 7 |
| 2 | 9 | 7 | 11 | rob | 7 | 11 |
| 3 | 3 | 11 | 10 | skip | 11 | 11 |
| 4 | 1 | 11 | 12 | rob | 11 | 12 |
Edge cases to test:
- A single house.
- Two houses → the richer one.
- Skipping two houses in a row can be best, e.g.
[2, 1, 1, 2]→ 4.
Complexity: O(n) time, O(1) space (the 2-variable version of the table).
Medium 416. Partition Equal Subset Sum (0/1 knapsack) Google Meta Amazon Microsoft Apple
Problem: Can the array be split into two subsets with equal sums?
nums = [1,5,11,5]truenums = [1,2,3,5]falseConstraints: 1 ≤ nums.length ≤ 200 · 1 ≤ nums[i] ≤ 100
Hint 1
If the total is odd, it's impossible. Otherwise: can some subset sum to total / 2?
Hint 2
0/1 knapsack: a boolean dp[s] for reachable sums, filled with s going down so each number is used once.
From brute force to the pattern: Trying all 2ⁿ subsets is exponential. The set of reachable sums is at most total / 2 + 1 values, so DP over sums is O(n · total).
Spot it: "can you partition" means picking a subset that sums to total/2. Each number is used at most once, so it's a yes/no 0/1 knapsack.
State & transition: dp[s] = "some subset of the items seen so far sums to s". For a new item x: dp[s] = dp[s] or dp[s − x]. Loop s downwards so dp[s − x] still means "without x". Looping upwards would let x be used twice.
Approach (step by step):
- If the total is odd, return False right away.
target = total // 2. State:dp[s]= can some subset of the items seen so far sum tos?- Base:
dp[0] = True(the empty subset). - For each number
x, loopsfromtargetdown tox:dp[s] = dp[s] or dp[s − x]. - Return
dp[target].
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total % 2 == 1:
return False # an odd total can't split into two equal halves
target = total // 2
# dp[subset_sum] = True if some subset of the numbers seen so far adds up to subset_sum
# [False] * k makes a list of k False values
dp = [False] * (target + 1)
dp[0] = True # the empty subset sums to 0
for i in range(len(nums)):
num = nums[i]
# walk subset_sum DOWN from target to num (BACKWARDS): dp[subset_sum - num]
# is still the value from BEFORE this number, so each number is used at most once
subset_sum = target
while subset_sum >= num:
if dp[subset_sum - num]:
dp[subset_sum] = True # reach subset_sum by adding num to a smaller subset
subset_sum = subset_sum - 1
return dp[target]
What the code is doing, line by line:
- Add up the numbers. If the total is odd, two equal halves are impossible, so return
Falseimmediately. targetis half the total. The question becomes: can some subset add up to exactlytarget?dphas one True/False cell per possible sum0..target. Onlydp[0]starts True (pick nothing).- For each number
num, awhileloop walkssubset_sumfromtargetdown tonum. Ifsubset_sum - numwas reachable, addingnummakessubset_sumreachable. - Why downwards? Cells below
subset_sumhave not been updated for thisnumyet, so they describe subsets without it. Going upwards would letnumbe added twice (for[2, 2, 3, 5], target 6, it would wrongly reach 6 as 3 + 3). - Return
dp[target].
Dry run on the example:
nums = [1, 5, 11, 5], total 22, target = 11 (T = True, F = False):
dp[subset_sum] for subset_sum = | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| start | T | F | F | F | F | F | F | F | F | F | F | F |
after num = 1 | T | T | F | F | F | F | F | F | F | F | F | F |
after num = 5 | T | T | F | F | F | T | T | F | F | F | F | F |
after num = 11 | T | T | F | F | F | T | T | F | F | F | F | T |
after num = 5 | T | T | F | F | F | T | T | F | F | F | T | T |
Edge cases to test:
- An odd total → False.
- A single element → False.
- One element equal to half the total.
Complexity: O(n · target) time, O(target) space.
Medium 322. Coin Change (unbounded knapsack) Google Meta Amazon Microsoft
Problem: Fewest coins (unlimited supply of each denomination) that make up amount, or -1 if impossible.
Solve it on LeetCode ↗ · Video walkthrough ↗
coins = [1,2,5], amount = 113coins = [2], amount = 3-1Constraints: 1 ≤ coins.length ≤ 12 · 1 ≤ coins[i] ≤ 2³¹ - 1 · 0 ≤ amount ≤ 10⁴
Hint 1
dp[a] = the fewest coins to make amount a.
Hint 2
dp[a] = 1 + min(dp[a − coin]) over coins ≤ a; build up from dp[0] = 0.
From brute force to the pattern: Greedy (biggest coin first) can fail, e.g. coins [1, 3, 4] for 6; plain recursion is exponential. Unbounded-knapsack DP over amounts is O(amount · coins).
Spot it: "fewest" + "infinite number of each coin". That's unbounded knapsack. Greedy fails: with coins [1,3,4] and amount 6, greedy gives 4+1+1, but 3+3 is better.
State & transition: dp[a] = fewest coins to make amount a. Last decision: which coin c was added last → dp[a] = 1 + min(dp[a − c]). Fill a upwards.
Approach (step by step):
- State:
dp[a]= fewest coins that make amounta. - Base:
dp[0] = 0. Every other cell starts at a sentinelINF = amount + 1(impossible). - For
a = 1..amount(forwards), for each coinc ≤ a:dp[a] = min(dp[a], dp[a − c] + 1). Coin c is the last coin added. - If
dp[amount]is stillINF, return -1. Otherwise return it.
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
INF = amount + 1 # "impossible": more coins than could ever be needed
# dp[sub_amount] = fewest coins that make sub_amount; every cell starts as impossible
dp = [INF] * (amount + 1)
dp[0] = 0 # 0 coins make amount 0
# fill amounts FORWARDS so smaller amounts are ready (and coins can be reused)
for sub_amount in range(1, amount + 1):
for i in range(len(coins)):
coin = coins[i]
if coin <= sub_amount:
# use this coin LAST: 1 coin + best way to make the rest
dp[sub_amount] = min(dp[sub_amount], dp[sub_amount - coin] + 1)
if dp[amount] == INF:
return -1 # never reached: amount can't be made
return dp[amount]
What the code is doing, line by line:
INF = amount + 1is a sentinel for "impossible": even using only 1-coins you never need more thanamountcoins.dpstarts with every amount impossible, exceptdp[0] = 0.- The outer loop walks
sub_amountfrom 1 up toamount. For each coin that fits, it asks: "if this coin is the last one I add, how many coins in total?" That isdp[sub_amount - coin] + 1, and we keep the minimum. - Because smaller amounts are finished first,
dp[sub_amount - coin]is always final when we read it, and it may itself already use the same coin (unlimited supply). - If
dp[amount]is stillINF, no combination works and we return -1; otherwise we return the count.
Dry run on the example:
coins = [1, 2, 5], amount = 11 (answer 3 = 5 + 5 + 1). "Best last coin" is the first coin that achieved the minimum:
sub_amount | dp[sub_amount] | best last coin |
|---|---|---|
| 0 | 0 | — |
| 1 | 1 | coin 1 + dp[0] (= 0) |
| 2 | 1 | coin 2 + dp[0] (= 0) |
| 3 | 2 | coin 1 + dp[2] (= 1) |
| 4 | 2 | coin 2 + dp[2] (= 1) |
| 5 | 1 | coin 5 + dp[0] (= 0) |
| 6 | 2 | coin 1 + dp[5] (= 1) |
| 7 | 2 | coin 2 + dp[5] (= 1) |
| 8 | 3 | coin 1 + dp[7] (= 2) |
| 9 | 3 | coin 2 + dp[7] (= 2) |
| 10 | 2 | coin 5 + dp[5] (= 1) |
| 11 | 3 | coin 1 + dp[10] (= 2) |
Edge cases to test:
amount = 0→ 0.- Impossible amounts → −1.
- A single coin type.
Complexity: O(amount · len(coins)) time, O(amount) space.
Medium 72. Edit Distance (two strings) Google Meta Amazon Cisco Microsoft Apple
Problem: Minimum number of single-character inserts, deletes, or replaces to turn word1 into word2.
word1 = "horse", word2 = "ros"3word1 = "intention", word2 = "execution"5Constraints: 0 ≤ word1.length, word2.length ≤ 500 · word1 and word2 consist of lowercase English letters.
Hint 1
dp[i][j] = the edits needed to turn word1[:i] into word2[:j].
Hint 2
If the last letters match, copy the diagonal; otherwise 1 + min(insert, delete, replace).
From brute force to the pattern: Recursing on the three operations without memory is exponential. There are only (m + 1)(n + 1) prefix pairs, so a DP table fills them all in O(m · n).
Spot it: two strings + "minimum operations". That's the 2-D string family, a sibling of LCS (shown in the table-fill visualizer above).
State & transition: dp[i][j] = edits to turn word1[:i] into word2[:j]. If the last characters match: dp[i−1][j−1] (free). Otherwise 1 + min(delete → dp[i−1][j], insert → dp[i][j−1], replace → dp[i−1][j−1]). Base: dp[i][0] = i, dp[0][j] = j.
Approach (step by step):
- State:
dp[i][j]= edits to turnword1[:i]intoword2[:j]. The table is(m+1) × (n+1). - Base:
dp[i][0] = i(delete everything) anddp[0][j] = j(insert everything). - Fill row by row. If
word1[i−1] == word2[j−1], copy the diagonal (no edit needed). - Otherwise take
1 + min(up = delete, left = insert, diagonal = replace). - Answer:
dp[m][n].
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
len1 = len(word1) # m in the steps above
len2 = len(word2) # n in the steps above
# build a (len1 + 1) x (len2 + 1) table of zeros, one row at a time
dp = []
for i in range(len1 + 1):
dp.append([0] * (len2 + 1))
# base cases: turning a prefix into "" (or "" into a prefix) costs its length
for i in range(len1 + 1):
dp[i][0] = i # delete everything
for j in range(len2 + 1):
dp[0][j] = j # insert everything
for i in range(1, len1 + 1):
for j in range(1, len2 + 1):
if word1[i - 1] == word2[j - 1]:
# last characters already match: no edit needed
dp[i][j] = dp[i - 1][j - 1]
else:
delete_cost = dp[i - 1][j] # delete word1[i - 1]
insert_cost = dp[i][j - 1] # insert word2[j - 1]
replace_cost = dp[i - 1][j - 1] # replace word1[i - 1] with word2[j - 1]
dp[i][j] = 1 + min(delete_cost, insert_cost, replace_cost)
return dp[len1][len2]
What the code is doing, line by line:
len1andlen2are the word lengths (calledmandnin the steps above).- The table is built with a loop that appends one row of zeros per prefix of
word1(including the empty prefix), giving(len1 + 1) × (len2 + 1)cells. - Column 0 and row 0 are the base cases: turning
word1[:i]into an empty string takesideletes; turning an empty string intoword2[:j]takesjinserts. - For every other cell we compare the last characters of the two prefixes. If they match, the cost is the same as without them (the diagonal cell).
- Otherwise we pay 1 edit plus the cheapest of three neighbors: up (
delete_cost), left (insert_cost) or diagonal (replace_cost). - The bottom-right cell
dp[len1][len2]is the answer for the full words.
Dry run on the example:
word1 = "horse", word2 = "ros". Each cell is dp[i][j]; the answer is the bottom-right 3:
word1 ↓ / word2 → | "" | 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 |
Edge cases to test:
- One word empty → the other word's length.
- Identical words → 0.
- No letters in common.
Complexity: O(m·n) time; O(m·n) space, or O(n) keeping two rows.
Medium 300. Longest Increasing Subsequence (LIS) Google Meta Amazon Microsoft
Problem: Length of the longest strictly increasing subsequence (elements don't have to be contiguous).
Solve it on LeetCode ↗ · Video walkthrough ↗
nums = [10,9,2,5,3,7,101,18]4nums = [0,1,0,3,2,3]4Constraints: 1 ≤ nums.length ≤ 2500 · -10⁴ ≤ nums[i] ≤ 10⁴
Hint 1
dp[i] = the length of the longest increasing subsequence that ends at index i.
Hint 2
dp[i] = 1 + max(dp[j]) for earlier j with nums[j] < nums[i]. (An O(n log n) version keeps the smallest tail for each length and uses binary search.)
From brute force to the pattern: Checking all 2ⁿ subsequences is exponential. The O(n²) DP reuses the answer for every earlier ending; patience sorting with binary search brings it to O(n log n).
Spot it: "longest" + "subsequence" + one array.
State & transition: dp[i] = length of the longest increasing subsequence that ends at index i. Last decision: which earlier j (with nums[j] < nums[i]) comes right before i. The answer is max(dp), because the best subsequence can end anywhere.
Approach (step by step):
- State:
dp[i]= length of the longest increasing subsequence that ends at index i. Start every cell at 1. - For each
i, look at everyj < i. Ifnums[j] < nums[i], nums[i] can extend that subsequence:dp[i] = max(dp[i], dp[j] + 1). - Answer:
max(dp), because the best subsequence can end anywhere. - Faster version: keep
tails, wheretails[k]= the smallest possible tail of an increasing subsequence of lengthk+1. Binary-search eachxinto it: append if it's larger than every tail, otherwise overwrite the first tail ≥ x. The answer islen(tails).
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int: # O(n^2) DP
# dp[i] = length of the longest increasing subsequence that ENDS at index i
# every element alone is a run of length 1, so start every cell at 1
dp = [1] * len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[j] < nums[i]:
# nums[i] can extend the run that ends at j
dp[i] = max(dp[i], dp[j] + 1)
# the best run may end anywhere, so take the largest cell (0 for an empty list)
best = 0
for i in range(len(dp)):
best = max(best, dp[i])
return best
def lengthOfLIS_fast(self, nums: List[int]) -> int: # O(n log n)
# tails[k] = smallest possible last number of an increasing subsequence of length k + 1
# (tails is always sorted, which is what makes binary search possible)
tails = []
for i in range(len(nums)):
num = nums[i]
position = self.first_tail_at_least(tails, num)
if position == len(tails):
tails.append(num) # num is bigger than every tail: the longest run grows
else:
tails[position] = num # num is a smaller tail for runs of length position + 1
return len(tails)
def first_tail_at_least(self, tails: List[int], num: int) -> int:
# binary search: return the first index whose value is >= num,
# or len(tails) if every value is smaller than num
low = 0
high = len(tails) # the answer is always inside [low, high]
while low < high:
middle = (low + high) // 2
if tails[middle] < num:
low = middle + 1 # middle is too small, the answer is to its right
else:
high = middle # middle might be the answer, keep it in range
return low # low == high: the window has shrunk to one spot
What the code is doing, line by line:
- Slow version.
dpstarts as all 1s: every number on its own is an increasing run of length 1. - For each
i, the inner loop looks at every earlier indexj. Ifnums[j] < nums[i], thennums[i]can be appended to the best run ending atj, giving lengthdp[j] + 1. - The longest run can end at any index, so a final loop finds the largest value in
dp. Startingbestat 0 also handles an empty list. - Fast version.
tails[k]remembers the smallest number that can end an increasing run of lengthk + 1. A smaller tail is always better, because more future numbers can extend it.tailsstays sorted. - For each
num, the helper findsposition= the first tail that is>= num. If there is none,numextends the longest run, so it is appended. Otherwisenumreplaces that tail: we found a run of the same length with a smaller ending. - The hand-written binary search (
first_tail_at_least): the answer is somewhere inlow..high(wherehigh = len(tails)means "after the end"). Look at themiddleelement. If it is smaller thannum, the answer must be to the right, solow = middle + 1. Otherwisemiddleitself might be the answer, sohigh = middle. Each step halves the window; whenlow == highthe loop stops andlowis the answer. (Python'sbisect.bisect_leftdoes the same thing.) - The length of
tailsis the LIS length. Careful:tailsitself is not always a real subsequence; only its length is meaningful.
Dry run on the example:
nums = [10, 9, 2, 5, 3, 7, 101, 18] (answer 4, e.g. 2, 3, 7, 18). Both versions side by side:
| i | nums[i] | dp[i] (slow version) | position (binary search) | action | tails after |
|---|---|---|---|---|---|
| 0 | 10 | 1 | 0 | append | [10] |
| 1 | 9 | 1 | 0 | overwrite tails[0] | [9] |
| 2 | 2 | 1 | 0 | overwrite tails[0] | [2] |
| 3 | 5 | 2 | 1 | append | [2, 5] |
| 4 | 3 | 2 | 1 | overwrite tails[1] | [2, 3] |
| 5 | 7 | 3 | 2 | append | [2, 3, 7] |
| 6 | 101 | 4 | 3 | append | [2, 3, 7, 101] |
| 7 | 18 | 4 | 3 | overwrite tails[3] | [2, 3, 7, 18] |
Edge cases to test:
- Strictly decreasing → 1.
- Equal values don't count as increasing.
- A single element.
Complexity: O(n²) / O(n log n) time, O(n) space.
Hard 312. Burst Balloons (interval DP) Google Meta Amazon Cisco Microsoft
Problem: Bursting balloon i earns nums[left] · nums[i] · nums[right], using its current neighbors. Missing neighbors count as 1. Maximize total coins.
nums = [3,1,5,8]167nums = [1,5]10Constraints: n == nums.length · 1 ≤ n ≤ 300 · 0 ≤ nums[i] ≤ 100
Hint 1
Think about which balloon you burst last in a range, not first.
Hint 2
dp[l][r] = the best coins from bursting everything strictly between l and r; try each k in between as the last burst.
From brute force to the pattern: Trying every burst order is O(n!). Fixing the last balloon splits the range into two independent halves, giving an interval DP in O(n³).
Spot it: the order of removals changes the neighbors, and the answer for a range depends on sub-ranges. That's interval DP.
Key insight (the trick): don't think about which balloon bursts first. That splits the problem into halves that still interact. Think about which balloon k bursts last inside the open interval (l, r). When it bursts, its neighbors are exactly l and r, and the two sides are independent: dp[l][r] = max(dp[l][k] + a[l]·a[k]·a[r] + dp[k][r]).
Approach (step by step):
- Pad the array with a 1 at each end. This encodes the "missing neighbor counts as 1" rule.
- State:
dp[l][r]= the most coins from bursting every balloon strictly between l and r. - Fill by increasing interval length, so shorter intervals are always ready first.
- Transition: try each
kin(l, r)as the last balloon burst:dp[l][k] + a[l]·a[k]·a[r] + dp[k][r]. - Answer:
dp[0][n−1].
class Solution:
def maxCoins(self, nums: List[int]) -> int:
# pad with a 1 on each side: the "missing neighbor counts as 1" rule
# (list + list glues two lists together)
balloons = [1] + nums + [1]
size = len(balloons)
# build a size x size table of zeros, one row at a time
# dp[left][right] = most coins from bursting every balloon strictly between left and right
dp = []
for i in range(size):
dp.append([0] * size)
# short intervals before long ones, so the pieces are always ready
for length in range(2, size):
for left in range(size - length):
right = left + length
# try every balloon in (left, right) as the LAST one to burst there;
# at that moment its neighbors are exactly balloons[left] and balloons[right]
for last in range(left + 1, right):
burst_last = balloons[left] * balloons[last] * balloons[right]
total = dp[left][last] + burst_last + dp[last][right]
dp[left][right] = max(dp[left][right], total)
return dp[0][size - 1]
What the code is doing, line by line:
balloonsisnumswith an extra 1 at both ends, so every real balloon always has two neighbors.- The table
dpis built row by row withappend.dp[left][right]means: the best total from bursting only the balloons strictly between positionsleftandright. Intervals with nothing inside (right = left + 1) stay 0. lengthis the distanceright - left, going from 2 (one balloon inside) up to the whole array. Shorter intervals are filled first.- For each interval, every
lastin between is tried as the final balloon burst there. Everything else inside was already burst, so its neighbors areballoons[left]andballoons[right]; it earnsburst_last. - The left part
(left, last)and the right part(last, right)are independent, and their best totals are already in the table.totaladds the three pieces; we keep the maximum. - The answer is the interval covering all real balloons:
dp[0][size - 1].
Dry run on the example:
nums = [3, 1, 5, 8] → balloons = [1, 3, 1, 5, 8, 1]. Headers show index (value); each cell is dp[left][right], · = empty or impossible interval. Answer: top-right 167.
left ↓ / right → | 0 (1) | 1 (3) | 2 (1) | 3 (5) | 4 (8) | 5 (1) |
|---|---|---|---|---|---|---|
| 0 (1) | · | · | 3 | 30 | 159 | 167 |
| 1 (3) | · | · | · | 15 | 135 | 159 |
| 2 (1) | · | · | · | · | 40 | 48 |
| 3 (5) | · | · | · | · | · | 40 |
| 4 (8) | · | · | · | · | · | · |
| 5 (1) | · | · | · | · | · | · |
Edge cases to test:
- A single balloon.
- Zeros in the array.
- Pad the array with 1 at both ends.
Complexity: O(n³) time, O(n²) space. [3,1,5,8] → 167.
Medium 64. Minimum Path Sum (grid DP) Google Amazon Microsoft
Problem: Move only right or down from the top-left to the bottom-right, minimizing the sum of the cells you pass through.
Solve it on LeetCode ↗ · Video walkthrough ↗
grid = [[1,3,1],[1,5,1],[4,2,1]]7grid = [[1,2,3],[4,5,6]]12Constraints: m == grid.length · n == grid[i].length · 1 ≤ m, n ≤ 200 · 0 ≤ grid[i][j] ≤ 200
Hint 1
You arrive at a cell from above or from the left.
Hint 2
dp[r][c] = grid[r][c] + min(dp[r − 1][c], dp[r][c − 1]); the first row and column have only one way in.
From brute force to the pattern: Trying every right/down path is exponential. Each cell's best cost depends only on its two predecessors, so a grid DP is O(r · c).
Spot it: grid + "only right or down" + "minimum". With one-way moves there are no cycles, so it's grid DP rather than BFS/Dijkstra.
State & transition: dp[r][c] = grid[r][c] + min(dp[r−1][c], dp[r][c−1]). Only the previous row is needed, so one 1-D array suffices.
Approach (step by step):
- State:
dp[r][c]= cheapest path sum to reach(r, c). Keep only one row:dpof sizenum_cols + 1. dp[0]stays∞as a left-wall sentinel. Setdp[1] = 0so the start cell costs just its own value.- For each row, go left to right:
dp[j+1] = cell + min(dp[j+1] (from above), dp[j] (from the left)). - Answer:
dp[num_cols]after the last row.
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
num_cols = len(grid[0])
# dp[j + 1] = cheapest path sum to reach column j of the current row.
# dp[0] is an extra "wall" cell that is never reachable.
# float("inf") is bigger than any number, so it means "can't come from here"
dp = [float("inf")] * (num_cols + 1)
dp[1] = 0 # so the start cell costs just grid[0][0]
for i in range(len(grid)):
row = grid[i]
for j in range(num_cols):
from_above = dp[j + 1] # still holds the PREVIOUS row's value
from_left = dp[j] # already updated for THIS row
dp[j + 1] = row[j] + min(from_above, from_left)
return dp[num_cols]
What the code is doing, line by line:
- Instead of a full 2-D table we keep one row,
dp, with one extra cell at the front.dp[j + 1]is the cheapest way to reach columnj;dp[0]is a wall that is always infinity. - Before the first row everything is unreachable except
dp[1] = 0. That makes the start cell cost exactlygrid[0][0](coming "from above" for free). - For each row (left to right),
dp[j + 1]still holds the value from the row above (from_above), anddp[j]was just updated for this row (from_left). The new value is the cell's own cost plus the cheaper of the two. - In row 0,
from_aboveis infinity (except at the start), so cells can only be reached from the left. In column 0,from_leftis the wall, so cells can only be reached from above. - After the last row,
dp[num_cols]is the cheapest path to the bottom-right corner.
Dry run on the example:
grid = [[1, 3, 1], [1, 5, 1], [4, 2, 1]] (answer 7 = 1→3→1→1→1). The single dp row after each grid row:
| row values | dp[0] (wall) | dp[1] (col 0) | dp[2] (col 1) | dp[3] (col 2) | |
|---|---|---|---|---|---|
| start | — | ∞ | 0 | ∞ | ∞ |
| after row 0 | [1, 3, 1] | ∞ | 1 | 4 | 5 |
| after row 1 | [1, 5, 1] | ∞ | 2 | 7 | 6 |
| after row 2 | [4, 2, 1] | ∞ | 6 | 8 | 7 |
Edge cases to test:
- A single row or column.
- A 1 × 1 grid.
- Large values along the edges.
Complexity: O(R·C) time, O(C) space.
Medium 309. Best Time to Buy and Sell Stock with Cooldown (state machine) Google Amazon Apple
Problem: Unlimited transactions, but after selling you must wait one day before buying again. Maximize profit.
prices = [1,2,3,0,2]3prices = [1]0Constraints: 1 ≤ prices.length ≤ 5000 · 0 ≤ prices[i] ≤ 1000
Hint 1
Track each day's state: holding a share, just sold (cooldown), or resting.
Hint 2
Write a transition for each state from the previous day's states; the answer is the best non-holding state.
From brute force to the pattern: Trying every sequence of buy/sell/rest is exponential. Three states per day with constant-time transitions is O(n) time and O(1) space.
Spot it: "after you sell, you cannot buy the next day". What you're allowed to do depends on what you are right now. Model it as a few states with arrows between them.
States: hold (own a share), sold (just sold today, so cooling down tomorrow), rest (no share, free to buy). Arrows: hold ← max(hold, rest − p), sold ← hold + p, rest ← max(rest, sold).
Approach (step by step):
- Define three states for the end of each day:
hold(own a share),sold(sold today),rest(no share and free to buy). - Start with
hold = sold = −∞(impossible before day 0) andrest = 0. - Each day, compute all three from yesterday's values:
hold = max(hold, rest − p),sold = hold + p,rest = max(rest, sold). - Buying reads
rest, neversold. That one detail enforces the cooldown. - Answer:
max(sold, rest). Ending while holding a share is never best.
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# float("-inf") = "impossible": before day 0 you can't own a share or have just sold one
hold = float("-inf") # own a share
sold = float("-inf") # sold today (cooling down tomorrow)
rest = 0 # no share and free to buy
for i in range(len(prices)):
price = prices[i]
# all three must be computed from YESTERDAY's values, so we store the
# new values in temporary variables and only copy them over at the end
new_hold = max(hold, rest - price) # keep holding, or buy today (only from rest)
new_sold = hold + price # sell the share we held yesterday
new_rest = max(rest, sold) # keep resting, or finish yesterday's cooldown
hold = new_hold
sold = new_sold
rest = new_rest
return max(sold, rest) # ending while holding a share is never best
What the code is doing, line by line:
- Three variables, one per state, each holding the best profit you can have at the end of the current day in that state.
holdandsoldstart at minus infinity because neither state is possible before the first day;reststarts at 0.- Each day computes
new_hold,new_soldandnew_restfrom yesterday's values. The temporaries are needed because, for example,new_solduses yesterday'shold; if we had already overwrittenhold, we could buy and sell on the same day, andnew_restwould see today'ssoldand skip the cooldown. - Buying reads
rest, neversold: that is the cooldown. After a sale you spend a day insoldbefore moving torest, and only then can you buy. - The answer is the better of
soldandrest.
Dry run on the example:
prices = [1, 2, 3, 0, 2] (answer 3: buy, sell, cooldown, buy, sell). Values at the end of each day:
| day | price | hold | sold | rest |
|---|---|---|---|---|
| start | — | −∞ | −∞ | 0 |
| 0 | 1 | -1 | −∞ | 0 |
| 1 | 2 | -1 | 1 | 0 |
| 2 | 3 | -1 | 2 | 1 |
| 3 | 0 | 1 | -1 | 2 |
| 4 | 2 | 1 | 3 | 2 |
Edge cases to test:
- A single day → 0.
- Falling prices → 0.
- The cooldown forcing you to skip a good buy.
Complexity: O(n) time, O(1) space.
8. Common mistakes
[2,2,3,5] is a classic failing test for 416.dp[i] means…", you'll write a transition that mixes "first i items" with "ending at i". LIS needs "ending at i" and max(dp) at the end. House Robber uses "first i" and dp[n−1].dp of size (m+1)×(n+1), cell dp[i][j] compares a[i-1] and b[j-1]. Row/column 0 is the empty prefix.amount + 1 or float('inf') for "impossible", and convert back to -1 at the end. Initializing a max with 0 is wrong when values can be negative.@lru_cache needs hashable arguments (pass indices or tuples, not lists). Deep recursion (n ≈ 10⁴) may hit the recursion limit. Switch to tabulation.9. Self-check quiz
1-D array, 0/1 knapsack (each item at most once). Which way should the capacity loop run?
dp[c − w] as "without the current item". Forwards would let the item be counted again in the same pass."Minimum deletions to make two strings equal." Which family?
dp[i][j]. In fact the answer is m + n − 2·LCS.LIS with dp[i] = "longest increasing subsequence ending at i". Where's the answer?
Burst Balloons: what makes the sub-intervals independent?
"Return the minimum number of perfect squares that sum to n."
10. Practice list (in order)
| # | Problem | Level | Data structure | Twist |
|---|---|---|---|---|
| 70 | Climbing Stairs | Easy | Warm-up: Fibonacci in disguise | |
| 509 | Fibonacci Number | Easy | The smallest DP: f(n) = f(n−1) + f(n−2) with two rolling variables | |
| 746 | Min Cost Climbing Stairs | Easy | Climbing Stairs with a price: take the cheaper of the two ways in | |
| 198 | House Robber | Medium | Take / skip, 1-D | |
| 213 | House Robber II | Medium | Circle → run 198 twice (drop first / drop last) | |
| 322 | Coin Change | Medium | Unbounded knapsack, min | |
| 518 | Coin Change II | Medium | Count combinations: coins in the outer loop | |
| 416 | Partition Equal Subset Sum | Medium | 0/1 knapsack, backwards | |
| 300 | Longest Increasing Subsequence | Medium | "Ending at i" + O(n log n) tails | |
| 1143 | Longest Common Subsequence | Medium | Two-string template | |
| 72 | Edit Distance | Medium | 3-way min | |
| 64 | Minimum Path Sum | Medium | Grid DP, 1 row of memory | |
| 309 | Best Time to Buy and Sell Stock with Cooldown | Medium | State machine | |
| 53 | Maximum Subarray | Medium | Kadane: best sum ending here = max(x, best ending before + x) | |
| 62 | Unique Paths | Medium | Grid DP: paths to a cell = paths from above + paths from the left | |
| 63 | Unique Paths II | Medium | Same grid DP, but an obstacle cell has 0 paths | |
| 120 | Triangle | Medium | Bottom-up: each cell adds the cheaper of its two children | |
| 91 | Decode Ways | Medium | Fibonacci-like: last step decodes one digit (not 0) or two digits (10–26) | |
| 97 | Interleaving String | Medium | dp[i][j]: can s1[:i] and s2[:j] interleave into s3[:i+j]? The last char came from s1 or s2 | |
| 139 | Word Break | Medium | ok[i] = some dictionary word ends at i and ok[start of that word] | |
| 152 | Maximum Product Subarray | Medium | Track both the max and min product ending here: a negative flips them | |
| 221 | Maximal Square | Medium | side[r][c] = 1 + min(top, left, top-left) for a '1' cell | |
| 343 | Integer Break | Medium | best[i] = max over the first part j of j · max(i − j, best[i − j]) | |
| 368 | Largest Divisible Subset | Medium | Sort; LIS-style DP where 'increasing' means 'divides'; keep parents to rebuild | |
| 377 | Combination Sum IV | Medium | Order matters: ways[t] = Σ ways[t − x] (target in the outer loop) | |
| 396 | Rotate Function | Medium | F(k) = F(k − 1) + sum − n · (the element that wraps to the front) | |
| 486 | Predict the Winner | Medium | diff[i][j] = best (my score − opponent's) on nums[i..j]; take an end, subtract their best | |
| 494 | Target Sum | Medium | Count ways to reach each running sum; each number adds or subtracts | |
| 516 | Longest Palindromic Subsequence | Medium | Interval DP: equal ends add 2 to the inside; otherwise drop one end | |
| 583 | Delete Operation for Two Strings | Medium | Keep the longest common subsequence; delete everything else: m + n − 2·LCS | |
| 638 | Shopping Offers | Medium | Memoised recursion on the remaining needs tuple; try each offer that doesn't overshoot | |
| 647 | Palindromic Substrings | Medium | Expand around each of the 2n − 1 centres; each successful expansion is one palindrome | |
| 673 | Number of Longest Increasing Subsequence | Medium | LIS DP with a second array: how many LIS end at each index | |
| 714 | Best Time to Buy and Sell Stock with Transaction Fee | Medium | Two states per day: holding / not holding; pay the fee when selling | |
| 740 | Delete and Earn | Medium | Total points per value, then House Robber over values (adjacent values conflict) | |
| 788 | Rotated Digits | Medium | Digit state per number from its prefix: invalid / same / changed (dp[i] from dp[i // 10]) | |
| 790 | Domino and Tromino Tiling | Medium | full[i] = full[i−1] + full[i−2] + 2·half[i−2]; half[i] = half[i−1] + full[i−1] | |
| 799 | Champagne Tower | Medium | Simulate the flow row by row: each glass keeps 1 and splits the excess evenly | |
| 877 | Stone Game | Medium | Interval DP on score difference (and Alice always wins: she can take all even or all odd piles) | |
| 918 | Maximum Sum Circular Subarray | Medium | Max of (Kadane max) and (total − Kadane min); if all negative, just the Kadane max | |
| 931 | Minimum Falling Path Sum | Medium | Row by row: each cell adds the cheapest of the three cells above it | |
| 983 | Minimum Cost For Tickets | Medium | cost[d] = cost[d−1] if not travelling; else min of 1-, 7-, 30-day pass ending today | |
| 1043 | Partition Array for Maximum Sum | Medium | best[i] = max over the last block length L ≤ k of best[i − L] + L · max(block) | |
| 1048 | Longest String Chain | Medium | Sort by length; chain[w] = 1 + max chain of w with one letter removed | |
| 1049 | Last Stone Weight II | Medium | Split stones into two groups as evenly as possible: subset-sum up to total/2 | |
| 1140 | Stone Game II | Medium | best(i, M) = suffix sum − min over X ≤ 2M of best(i + X, max(M, X)) | |
| 1277 | Count Square Submatrices with All Ones | Medium | side[r][c] = 1 + min(top, left, diagonal); the sum of all sides counts every square | |
| 1749 | Maximum Absolute Sum of Any Subarray | Medium | Kadane twice: the largest sum and the smallest sum; answer max(|both|) | |
| 1884 | Egg Drop With 2 Eggs and N Floors | Medium | Flip the question: with m moves you can cover floors[m] = floors[m − 1] + m; find the first m reaching n | |
| 2140 | Solving Questions With Brainpower | Medium | From the back: best[i] = max(skip → best[i + 1], solve → points + best[i + brainpower + 1]) | |
| 2400 | Number of Ways to Reach a Position After Exactly k Steps | Medium | Choose which steps go right: C(k, (k + d) / 2) when parity and distance allow | |
| 3573 | Best Time to Buy and Sell Stock V | Medium | Three states per transaction count: free, holding long, holding short | |
| 3592 | Inverse Coin Change | Medium | Rebuild the Coin Change II table amount by amount; amount i is a coin iff the count is one short | |
| 3660 | Jump Game IX | Medium | Answer = prefix max, unless the prefix max beats the suffix min to its right; then reuse ans[i + 1] | |
| 3738 | Longest Non-Decreasing Subarray After Replacing at Most One Element | Medium | left[i] = run ending at i, right[i] = run starting at i; try replacing each element to glue its neighbours' runs | |
| 3742 | Maximum Path Score in a Grid | Medium | Grid DP with a cost dimension: best[r][c][cost], cost capped at min(k, m + n) | |
| 3751 | Total Waviness of Numbers in Range I | Medium | Range is small (≤ 10⁵): count peaks and valleys of each number directly | |
| 3976 | Maximum Subarray Sum After Multiplier | Medium | Kadane with three states: before the operation, inside it, after it (for both × k and ÷ k) | |
| 4002 | Count Valid Sequences | Medium | All compositions C(n−1, k−1) minus the all-odd ones C((n−k)/2 + k − 1, k − 1) | |
| 279 | Perfect Squares | Medium | Unbounded knapsack: least[x] = 1 + min(least[x − s²]) over squares ≤ x | |
| 357 | Count Numbers with Unique Digits | Medium | Numbers with exactly k distinct digits: 9 · 9 · 8 · … (k factors); sum over k ≤ n | |
| 1871 | Jump Game VII | Medium | reach[i] = s[i] is '0' and some reachable index lies in [i − maxJump, i − minJump]; keep that count in a sliding window | |
| 241 | Different Ways to Add Parentheses | Medium | Split at every operator, combine all results of both sides; memoise on the substring | |
| 313 | Super Ugly Number | Medium | Ugly Number II with k pointers: next = min(ugly[ptr[i]] · primes[i]); advance every pointer that produced it | |
| 376 | Wiggle Subsequence | Medium | Two states: longest wiggle ending with an up step, and with a down step | |
| 474 | Ones and Zeroes | Medium | 0/1 knapsack with two capacities (zeros, ones); iterate capacities downwards | |
| 1014 | Best Sightseeing Pair | Medium | Score = (values[i] + i) + (values[j] − j): keep the best values[i] + i seen so far | |
| 1594 | Maximum Non Negative Product in a Matrix | Medium | Keep both the max and min product per cell: a negative cell swaps them | |
| 2770 | Maximum Number of Jumps to Reach the Last Index | Medium | dp[j] = 1 + max dp[i] over earlier i with |nums[j] − nums[i]| ≤ target | |
| 3418 | Maximum Amount of Money Robot Can Earn | Medium | Grid DP with a third index: neutralisations used so far (0, 1 or 2) | |
| 3434 | Maximum Frequency After Subarray Operation | Medium | For each value v: Kadane on +1 for v, −1 for k gives the best gain of turning v into k inside one subarray | |
| 3857 | Minimum Cost to Split into Ones | Medium | dp[x] = min over a of a·(x − a) + dp[a] + dp[x − a]; every order gives x(x − 1)/2 | |
| 464 | Can I Win | Medium | Game DP over the bitmask of used numbers: a state wins if some pick reaches the total or leaves a losing state | |
| 837 | New 21 Game | Medium | P[x] = (sum of P over the last maxPts points still drawing) / maxPts; keep that sum as a sliding window | |
| 3946 | Maximum Number of Items From Sale I | Medium | The first copy of item i is worth 1 + (items whose factor it divides); 0/1 knapsack on first copies, fill the rest with the cheapest item | |
| 688 | Knight Probability in Chessboard | Medium | dp over moves: probability of being on each cell after t moves; each move spreads 1/8 to every knight target on the board | |
| 312 | Burst Balloons | Hard | Interval DP, "last" trick | |
| 10 | Regular Expression Matching | Hard | Two-string DP with * | |
| 123 | Best Time to Buy and Sell Stock III | Hard | 4-state machine (buy1, sell1, buy2, sell2) | |
| 32 | Longest Valid Parentheses | Hard | dp[i] = longest valid substring ending at i; a ')' extends the match before it | |
| 44 | Wildcard Matching | Hard | dp[i][j]: does p[:j] match s[:i]? '*' matches empty (dp[i][j−1]) or one more char (dp[i−1][j]) | |
| 87 | Scramble String | Hard | Memoised recursion: try every split, swapped or not; prune with letter counts | |
| 132 | Palindrome Partitioning II | Hard | Expand palindromes around each centre; each palindrome s[l..r] gives cuts[r+1] ≤ cuts[l] + 1 | |
| 174 | Dungeon Game | Hard | DP backwards from the princess: need[r][c] = max(1, min(need right, need down) − dungeon[r][c]) | |
| 188 | Best Time to Buy and Sell Stock IV | Hard | State machine: buy[j] / sell[j] = best balance after the j-th buy / sell | |
| 233 | Number of Digit One | Hard | Count 1s per digit position from the high part, the current digit and the low part | |
| 403 | Frog Jump | Hard | For each stone, the set of jump sizes that can land on it; extend by k−1, k, k+1 | |
| 546 | Remove Boxes | Hard | dp(i, j, k): boxes[i..j] with k extra boxes equal to boxes[i] attached on the left | |
| 741 | Cherry Pickup | Hard | Two people walk down-right together; state (step, row1, row2); count a shared cell once | |
| 879 | Profitable Schemes | Hard | 0/1 knapsack over (members used, profit capped at minProfit) | |
| 1092 | Shortest Common Supersequence | Hard | Build the LCS table, then walk back: shared letters once, other letters from whichever side | |
| 1301 | Number of Paths with Max Score | Hard | DP of (best score, number of ways) from 'S' moving up/left/up-left; combine equal bests by adding ways | |
| 1320 | Minimum Distance to Type a Word Using Two Fingers | Hard | One finger is always on the last letter; DP over the other finger's position (26 + 'free') | |
| 1340 | Jump Game V | Hard | Memoised DFS: visits(i) = 1 + max over reachable lower indices (stop at the first taller-or-equal) | |
| 1406 | Stone Game III | Hard | lead[i] = max over taking 1–3 stones of (their sum − lead[i + taken]) | |
| 1463 | Cherry Pickup II | Hard | Both robots move down one row at a time: DP over (column 1, column 2), 9 moves per state | |
| 1510 | Stone Game IV | Hard | win[n] = some square k² ≤ n leaves a losing position | |
| 2376 | Count Special Integers | Hard | Digit DP: count shorter numbers by permutations, then walk n's digits with a used-digit set | |
| 2484 | Count Palindromic Subsequences | Hard | Fix the middle; multiply 'pairs ab before' by 'pairs ba after' for all 100 digit pairs | |
| 3225 | Maximum Score From Grid Operations | Hard | Each column's score comes from its left or right neighbour; DP over (height, is this column's credit pending to the right) | |
| 3336 | Find the Number of Subsequences With Equal GCD | Hard | DP over (gcd of seq1, gcd of seq2); each number goes to seq1, seq2 or neither | |
| 3700 | Number of ZigZag Arrays II | Hard | DP over the last value and direction, folded by symmetry into one vector; matrix power for n up to 10⁹ | |
| 3743 | Maximize Cyclic Partition Score | Hard | Range = pick one + and one − element per part (a stock-trading DP); handle the cycle by cutting just before or after the maximum | |
| 3753 | Total Waviness of Numbers in Range II | Hard | Digit DP returning (count of numbers, total waviness) with state (pos, last two digits, tight, started) | |
| 3962 | Maximum Subarray Sum After at Most K Swaps | Hard | Fix the left end; as the window grows, keep U = window ∪ best k outside values; answer = sum(U) − its smallest m | |
| 115 | Distinct Subsequences | Hard | ways[j] = ways to form t[:j]; for each char of s, update j from right to left when s[i] == t[j−1] | |
| 472 | Concatenated Words | Hard | Process words by length; Word Break DP against the shorter words seen so far | |
| 689 | Maximum Sum of 3 Non-Overlapping Subarrays | Hard | Window sums; best left window index up to i and best right window from i; try every middle window | |
| 805 | Split Array With Same Average | Hard | Same average ⇔ some subset of size k sums to total·k/n; subset-sum DP per size using bitsets | |
| 940 | Distinct Subsequences II | Hard | ends[c] = number of distinct subsequences ending with c; a new c makes it (sum of all ends) + 1 | |
| 1000 | Minimum Cost to Merge Stones | Hard | Interval DP: dp[i][j] = min cost to reduce stones[i..j] as far as possible; add the range sum when it can become one pile | |
| 1335 | Minimum Difficulty of a Job Schedule | Hard | dp[d][i] = min over the last day's start j of dp[d−1][j] + max(jobs[j..i−1]) | |
| 1478 | Allocate Mailboxes | Hard | Sort houses; one mailbox for houses i..j goes at their median; dp[k][j] = best for the first j houses with k boxes | |
| 1494 | Parallel Courses II | Hard | Bitmask DP over taken courses; each semester takes a subset (≤ k) of the currently available courses | |
| 1547 | Minimum Cost to Cut a Stick | Hard | Interval DP on the sorted cut positions (with the ends added): cost(i, j) = (P_j − P_i) + min over the first cut k | |
| 1691 | Maximum Height by Stacking Cuboids | Hard | Sort each cuboid's sides (tallest as height), sort cuboids, then LIS-style DP on 3D dominance | |
| 2262 | Total Appeal of A String | Hard | Count each character where it's the first occurrence of its letter in a substring: (i − last[c]) · (n − i) | |
| 2272 | Substring With Largest Variance | Hard | For each ordered letter pair (a, b): Kadane on +1 for a, −1 for b, requiring at least one b | |
| 2742 | Painting the Walls | Hard | Knapsack: a paid painter on wall i covers 1 + time[i] walls (the free painter works meanwhile); min cost to cover n | |
| 3130 | Find All Possible Stable Binary Arrays II | Hard | dp0/dp1[i][j] = arrays with i zeros, j ones ending in 0/1; subtract arrays whose last run would exceed limit | |
| 3995 | Minimum Cost to Convert String III | Hard | Positions are used at most once, so rules apply to disjoint original segments: dp over prefixes | |
| 2719 | Count of Integers | Hard | Digit DP f(N) = numbers ≤ N with digit sum in range; answer f(num2) − f(num1) + (num1 is good) | |
| 2472 | Maximum Number of Non-overlapping Palindrome Substrings | Hard | dp[i] = best for s[:i]; only palindromes of length k or k + 1 ending at i are worth taking | |
| 2977 | Minimum Cost to Convert String II | Hard | Floyd–Warshall between rule strings (by id), then a prefix DP that converts disjoint blocks |
Worked solutions for the practice list
Every problem in the table above that isn't already solved in section 7 is worked out here, so you never need to leave this page. Click a problem name in the table to jump to its solution.
Easy 70. Climbing Stairs Start here Google Meta Amazon Microsoft
Problem: You climb a staircase of n steps, 1 or 2 steps at a time. In how many distinct ways can you reach the top?
Solve it on LeetCode ↗ · Video walkthrough ↗
n = 22n = 33Constraints: 1 ≤ n ≤ 45
Hint 1
How can you arrive on step n?
Hint 2
From step n − 1 or step n − 2: ways(n) = ways(n − 1) + ways(n − 2), like Fibonacci.
From brute force to the pattern: Plain recursion recomputes the same steps exponentially often (O(2ⁿ)). Building up from the bottom computes each value once: O(n) time, O(1) space.
Spot it: "how many distinct ways" + each move is a small choice is 1-D DP, and it's Fibonacci in disguise.
Approach (step by step):
- State:
ways(i)= number of ways to stand on step i. - Last decision: the final move was either 1 step (from
i−1) or 2 steps (fromi−2), soways(i) = ways(i−1) + ways(i−2). - Base:
ways(0) = 1(stand still) andways(1) = 1. - Only two previous values are needed, so roll two variables up to n.
class Solution:
def climbStairs(self, n: int) -> int:
prev2 = 1 # ways(0): standing still is 1 way
prev1 = 1 # ways(1): a single 1-step
for step in range(2, n + 1):
# the last move onto this step was a 1-step (from step - 1) or a 2-step (from step - 2)
ways_here = prev1 + prev2
# slide the window up one step
prev2 = prev1
prev1 = ways_here
return prev1
What the code is doing, line by line:
prev2andprev1hold the number of ways to reach the two steps just below the current one. Both start at 1 (ways(0)andways(1)).- For every step from 2 to
n, the ways to stand on it are the ways to reach the step below (then take 1 step) plus the ways to reach two below (then take 2 steps). - Then the window slides:
prev2takes the oldprev1, andprev1takesways_here. - If
nis 1 the loop never runs andprev1 = 1is returned. Otherwiseprev1ends asways(n).
Dry run on the example:
n = 5 (answer 8). The LeetCode examples n = 2 → 2 and n = 3 → 3 appear in the first two loop rows:
step | ways_here = prev1 + prev2 | new prev2 | new prev1 |
|---|---|---|---|
| start | — | 1 | 1 |
| 2 | 2 | 1 | 2 |
| 3 | 3 | 2 | 3 |
| 4 | 5 | 3 | 5 |
| 5 | 8 | 5 | 8 |
Edge cases to test:
n = 1→ 1.n = 2→ 2.- Large
n(the recursion would be far too slow).
Complexity: O(n) time, O(1) space.
Medium 213. House Robber II Google Meta Amazon
Problem: House Robber (198), but the houses stand in a circle: the first and last houses are neighbors.
nums = [2,3,2]3nums = [1,2,3,1]4Constraints: 1 ≤ nums.length ≤ 100 · 0 ≤ nums[i] ≤ 1000
Hint 1
The houses are in a circle, so the first and last are neighbours.
Hint 2
Run House Robber twice: once without the first house and once without the last; take the max.
From brute force to the pattern: Handling the circle with complex state is error-prone. Splitting into two straight-line problems reuses House Robber: O(n).
Spot it: This is the same 1-D take/skip DP. The circle only adds one conflict: you can't rob both house 0 and house n−1.
Approach (step by step):
- If there is only one house, return its value.
- Case A: ignore the last house and run the linear robber on
nums[:-1]. - Case B: ignore the first house and run it on
nums[1:]. - Every valid plan skips the first house or the last one (or both), so the answer is
max(A, B).
from typing import List
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0] # nums[1:] and nums[:-1] would both be empty
# break the circle two ways (slices make new lists):
without_last = self.rob_line(nums[:-1]) # everything except the last house
without_first = self.rob_line(nums[1:]) # everything except the first house
return max(without_last, without_first)
def rob_line(self, houses: List[int]) -> int:
# House Robber I (houses in a straight line) in O(1) space
prev2 = 0 # best loot up to two houses back
prev1 = 0 # best loot up to the previous house
for i in range(len(houses)):
money = houses[i]
best_here = max(prev1, prev2 + money) # skip this house, or rob it
prev2 = prev1
prev1 = best_here
return prev1
What the code is doing, line by line:
- With a single house there is no conflict, so return its value. (Both slices below would be empty and give 0.)
- In a circle the first and last houses are neighbors, so no plan robs both. Every valid plan therefore fits inside
nums[:-1](all but the last house) or insidenums[1:](all but the first). - Each of those is an ordinary straight street, solved by the helper method
rob_line, which is exactly the House Robber I solution from problem 198. rob_linekeepsprev2andprev1, picks the better of skipping or robbing each house, and slides the window.- The answer is the larger of the two straight-line results.
Dry run on the example:
nums = [1, 2, 3, 1]: case A uses [1, 2, 3], case B uses [2, 3, 1]. Answer max(4, 3) = 4.
| case | money | new prev2 | new prev1 |
|---|---|---|---|
A: nums[:-1] | 1 | 0 | 1 |
A: nums[:-1] | 2 | 1 | 2 |
A: nums[:-1] | 3 | 2 | 4 |
B: nums[1:] | 2 | 0 | 2 |
B: nums[1:] | 3 | 2 | 3 |
B: nums[1:] | 1 | 3 | 3 |
Edge cases to test:
- A single house (the two ranges would be empty).
- Two houses.
- The best answer uses the first house.
Complexity: O(n) time, O(n) for the slices (or O(1) with index ranges).
Medium 518. Coin Change II Google Amazon Microsoft
Problem: Count the number of combinations of coins (unlimited supply) that make up amount. Order doesn't matter, so 1+2 and 2+1 are the same combination.
amount = 5, coins = [1,2,5]4amount = 3, coins = [2]0Constraints: 1 ≤ coins.length ≤ 300 · 1 ≤ coins[i] ≤ 5000 · All the values of coins are unique. · 0 ≤ amount ≤ 5000
Hint 1
Count combinations, not orderings: [1, 2] and [2, 1] are the same.
Hint 2
Loop over coins in the outer loop and amounts in the inner loop: dp[a] += dp[a − coin].
From brute force to the pattern: Recursion over coin choices is exponential, and swapping the two loops counts orderings by mistake. The coins-outer DP counts each combination once in O(amount · coins).
Spot it: "number of combinations" + unlimited coins is unbounded knapsack, counting version. Loop order decides whether you count combinations or permutations.
Approach (step by step):
- State:
dp[a]= number of ways to makeausing the coins processed so far. - Base:
dp[0] = 1(one way to make 0: take nothing). - Coins in the outer loop, amounts in the inner loop going forwards:
dp[a] += dp[a − c]. - Coins outside means each combination is built in a fixed coin order, so it's counted once. Forwards means each coin may be reused.
from typing import List
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
# dp[sub_amount] = number of ways to make sub_amount with the coins processed so far
dp = [0] * (amount + 1) # [0] * k = k zeros
dp[0] = 1 # one way to make 0: take no coins
# coins in the OUTER loop -> each combination is built in a fixed coin order,
# so 1+2 and 2+1 are counted only once
for i in range(len(coins)):
coin = coins[i]
# amounts FORWARDS -> dp[sub_amount - coin] may already use this coin, so it can be reused
for sub_amount in range(coin, amount + 1):
# add the ways that end with (another) copy of this coin
dp[sub_amount] = dp[sub_amount] + dp[sub_amount - coin]
return dp[amount]
What the code is doing, line by line:
dpcounts ways per amount.dp[0] = 1: there is exactly one way to make 0 (use nothing), and that is what lets the first coin get counted.- The outer loop goes over coins. After finishing coin number
i,dpcounts combinations that use only the firsti + 1coins. Because coins are added in a fixed order, each combination is counted once. - The inner loop goes forwards over amounts from
cointoamount.dp[sub_amount - coin]may already include this coin, so taking it again is allowed (unlimited supply). - Each update adds the ways that make
sub_amount - coinand then finish with this coin. - Swapping the two loops (amounts outside) would count orders, i.e. permutations, which is a different problem (LeetCode 377).
Dry run on the example:
amount = 5, coins = [1, 2, 5] (answer 4). The dp array after each coin:
dp[sub_amount] for sub_amount = | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| start | 1 | 0 | 0 | 0 | 0 | 0 |
after coin = 1 | 1 | 1 | 1 | 1 | 1 | 1 |
after coin = 2 | 1 | 1 | 2 | 2 | 3 | 3 |
after coin = 5 | 1 | 1 | 2 | 2 | 3 | 4 |
Edge cases to test:
amount = 0→ 1 (the empty combination).- No way to make the amount → 0.
- A coin larger than the amount.
Complexity: O(amount · len(coins)) time, O(amount) space.
Medium 1143. Longest Common Subsequence Google Meta Amazon Cisco Microsoft
Problem: Return the length of the longest subsequence that appears in both strings (characters in order, not necessarily contiguous).
Solve it on LeetCode ↗ · Video walkthrough ↗
text1 = "abcde", text2 = "ace"3text1 = "abc", text2 = "abc"3Constraints: 1 ≤ text1.length, text2.length ≤ 1000 · text1 and text2 consist of only lowercase English characters.
Hint 1
dp[i][j] = the LCS length of text1[:i] and text2[:j].
Hint 2
Matching last letters → 1 + dp[i − 1][j − 1]; otherwise max(dp[i − 1][j], dp[i][j − 1]).
From brute force to the pattern: Comparing all subsequences of both strings is O(2^(m + n)). There are only m · n prefix pairs, so the DP table is O(m · n).
Spot it: Two strings + "longest common subsequence" is the base two-string DP. The table-filling visualizer in section 2 runs exactly this algorithm.
Approach (step by step):
- State:
dp[i][j]= LCS length ofa[:i]andb[:j]. The table is(m+1) × (n+1), and row 0 / column 0 (empty prefixes) are 0. - If
a[i−1] == b[j−1], both last characters join the LCS:dp[i][j] = dp[i−1][j−1] + 1. - Otherwise drop one of the two last characters:
dp[i][j] = max(dp[i−1][j], dp[i][j−1]). - Fill row by row. The answer is
dp[m][n].
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
len1 = len(text1) # m in the steps above
len2 = len(text2) # n in the steps above
# build a (len1 + 1) x (len2 + 1) table of zeros, one row at a time;
# the extra row/column stands for the empty prefix
dp = []
for i in range(len1 + 1):
dp.append([0] * (len2 + 1))
for i in range(1, len1 + 1):
for j in range(1, len2 + 1):
# dp index i matches string index i - 1
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1 # both characters join the LCS
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) # drop a character from one side
return dp[len1][len2]
What the code is doing, line by line:
len1andlen2are the lengths (mandnin the steps).- The table gets one row of zeros per prefix of
text1, including the empty prefix. Row 0 and column 0 stay 0: nothing is common with an empty string. - Cell
dp[i][j]describestext1[:i]andtext2[:j], so their last characters aretext1[i - 1]andtext2[j - 1]. - If they are equal, both join the common subsequence: take the diagonal and add 1.
- If not, at least one of them is not in the LCS, so take the better of dropping the last character of
text1(cell above) or oftext2(cell to the left). - The answer is in the bottom-right cell.
Dry run on the example:
text1 = "abcde", text2 = "ace" (answer 3, "ace"). Each cell is dp[i][j]:
text1 ↓ / text2 → | "" | a | c | e |
|---|---|---|---|---|
"" | 0 | 0 | 0 | 0 |
a | 0 | 1 | 1 | 1 |
b | 0 | 1 | 1 | 1 |
c | 0 | 1 | 2 | 2 |
d | 0 | 1 | 2 | 2 |
e | 0 | 1 | 2 | 3 |
Edge cases to test:
- No common letters → 0.
- Identical strings → their length.
- One string empty.
Complexity: O(m·n) time; O(m·n) space, or O(n) with two rows.
Hard 10. Regular Expression Matching Google Meta Amazon Microsoft
Problem: Implement matching with '.' (any single character) and '*' (zero or more of the preceding element). The pattern must match the entire string.
s = "aa", p = "a"falses = "aa", p = "a*"trueConstraints: 1 ≤ s.length ≤ 20 · 1 ≤ p.length ≤ 20 · s contains only lowercase English letters. · p contains only lowercase English letters, '.', and '*'. · It is guaranteed for each appearance of the character '*', there will be a previous valid character to match.
Hint 1
dp[i][j] = whether s[i:] matches p[j:].
Hint 2
For x*, either skip the pair (zero copies) or, if the current letter matches, consume one letter of s and keep the pattern.
From brute force to the pattern: Naive recursion on * branches exponentially. Memoising on (i, j) limits it to m · n states: O(m · n).
Spot it: Two strings, prefix vs prefix, yes/no is two-string DP. The '*' adds a "use it zero times / use it once more" choice.
Approach (step by step):
- State:
dp[i][j]= doess[:i]matchp[:j]? Base:dp[0][0] = True. - Empty string vs pattern:
dp[0][j] = dp[0][j−2]whenp[j−1] == '*'. Patterns likea*b*can match nothing. - If
p[j−1]is a letter or'.':dp[i][j] = dp[i−1][j−1]and the characters match. - If
p[j−1] == '*': either use "x*" zero times (dp[i][j−2]), or, ifs[i−1]matches x, eat one more character with it (dp[i−1][j]). - Answer:
dp[m][n].
class Solution:
def isMatch(self, s: str, p: str) -> bool:
len_s = len(s)
len_p = len(p)
# build a (len_s + 1) x (len_p + 1) table of False, one row at a time
# dp[i][j] = does s[:i] (first i characters) match p[:j] (first j pattern characters)?
dp = []
for i in range(len_s + 1):
dp.append([False] * (len_p + 1))
dp[0][0] = True # empty matches empty
# empty string vs pattern: only "x*" pairs can match nothing
for j in range(2, len_p + 1):
if p[j - 1] == "*":
dp[0][j] = dp[0][j - 2] # "x*" can vanish
for i in range(1, len_s + 1):
for j in range(1, len_p + 1):
if p[j - 1] == "*":
# option 1: use "x*" zero times -> skip both pattern characters
zero_times = dp[i][j - 2]
# option 2: "x*" eats s[i - 1] (if x matches it) and stays available
one_more = False
if dp[i - 1][j] and self.chars_match(s[i - 1], p[j - 2]):
one_more = True
dp[i][j] = zero_times or one_more
else:
# plain letter or ".": the last characters must match, and so must the rest
dp[i][j] = dp[i - 1][j - 1] and self.chars_match(s[i - 1], p[j - 1])
return dp[len_s][len_p]
def chars_match(self, text_char: str, pattern_char: str) -> bool:
# "." matches any single character; anything else must be equal
if pattern_char == ".":
return True
return text_char == pattern_char
What the code is doing, line by line:
- The table has one row per prefix of
sand one column per prefix ofp, all starting asFalse, built row by row withappend.dp[0][0] = Truebecause an empty pattern matches an empty string. - Row 0 (empty string): a pattern can only match nothing if it is made of
x*pairs. Whenp[j - 1]is*, copy the answer from two columns back (drop the pair). - For a
*atp[j - 1], the element it repeats isp[j - 2].zero_timesdrops the pair entirely.one_moresays: ifs[i - 1]matches that element, let the star swallow it and look atdp[i - 1][j](same pattern, string one character shorter), because the star can still be used again. - For a plain letter or
., the last characters must match and the shorter prefixes (dp[i - 1][j - 1]) must match too. - The helper method
chars_matchhandles the.wildcard so the main loop stays readable. dp[len_s][len_p]says whether the whole string matches the whole pattern.
Dry run on the example:
s = "aab", p = "c*a*b" (answer True: c* vanishes, a* eats "aa", b matches). T = True, F = False:
s ↓ / p → | "" | c | * | a | * | b |
|---|---|---|---|---|---|---|
"" | T | F | T | F | T | F |
a | F | F | F | T | T | F |
a | F | F | F | F | T | F |
b | F | F | F | F | F | T |
Edge cases to test:
.*matches anything, including an empty string.a*matching zeroas.- The pattern is longer than the string but still matches, e.g.
s = "",p = "a*".
Complexity: O(m·n) time and space.
Hard 123. Best Time to Buy and Sell Stock III Google Meta Amazon
Problem: Make at most two transactions (buy then sell, never holding two shares at once). Maximize profit.
prices = [3,3,5,0,0,3,1,4]6prices = [1,2,3,4,5]4Constraints: 1 ≤ prices.length ≤ 10⁵ · 0 ≤ prices[i] ≤ 10⁵
Hint 1
At most two transactions: track the best profit after the first buy, first sell, second buy and second sell.
Hint 2
Update the four values each day in order; each uses the previous one.
From brute force to the pattern: Splitting the days at every point and solving two single-transaction problems is O(n²) (or O(n) with prefix/suffix arrays). Four rolling states do it in O(n) time and O(1) space.
Spot it: This is a state machine: what you may do depends on how many buys and sells you've already made. Four states in a chain: buy1 → sell1 → buy2 → sell2.
Approach (step by step):
buy1= best balance after the first buy,sell1= after the first sell,buy2= after the second buy,sell2= after the second sell.- Start with
buy1 = buy2 = −∞(not possible yet) andsell1 = sell2 = 0. - For each price p:
buy1 = max(buy1, −p),sell1 = max(sell1, buy1 + p),buy2 = max(buy2, sell1 − p),sell2 = max(sell2, buy2 + p). - Updating in this order with same-day values is safe. Buying and selling on the same day gains 0, so it never creates fake profit.
- Answer:
sell2. It also covers doing only one transaction, or none.
from typing import List
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# float("-inf") = "impossible": you can't be holding a share before any price is seen
buy1 = float("-inf") # best balance after the first buy
buy2 = float("-inf") # best balance after the second buy
sell1 = 0 # best balance after the first sell (or doing nothing)
sell2 = 0 # best balance after the second sell
for i in range(len(prices)):
price = prices[i]
buy1 = max(buy1, -price) # first buy: pay price from a balance of 0
sell1 = max(sell1, buy1 + price) # first sell
buy2 = max(buy2, sell1 - price) # second buy reuses the profit of the first sale
sell2 = max(sell2, buy2 + price) # second sell
return sell2
What the code is doing, line by line:
- Four variables track the best money balance after each stage of the chain buy1 → sell1 → buy2 → sell2. The buys start at minus infinity (not possible yet); the sells start at 0.
- For each day's
price, each state either keeps its old value (do nothing today) or moves forward from the previous state: buying subtractsprice, selling adds it. - Here we update in order using values from the same day, with no temporaries. That is safe because buying and selling on the same day changes the balance by 0, so it can never invent profit.
sell2is the answer. It also covers making only one transaction (buy2 and sell2 on the same day add 0) or none at all.
Dry run on the example:
prices = [3, 3, 5, 0, 0, 3, 1, 4] (answer 6: buy at 0 sell at 3, buy at 1 sell at 4). Values at the end of each day:
| day | price | buy1 | sell1 | buy2 | sell2 |
|---|---|---|---|---|---|
| start | — | −∞ | 0 | −∞ | 0 |
| 0 | 3 | -3 | 0 | -3 | 0 |
| 1 | 3 | -3 | 0 | -3 | 0 |
| 2 | 5 | -3 | 2 | -3 | 2 |
| 3 | 0 | 0 | 2 | 2 | 2 |
| 4 | 0 | 0 | 2 | 2 | 2 |
| 5 | 3 | 0 | 3 | 2 | 5 |
| 6 | 1 | 0 | 3 | 2 | 5 |
| 7 | 4 | 0 | 4 | 2 | 6 |
Edge cases to test:
- Prices only fall → 0.
- One transaction is best.
- Very few days.
Complexity: O(n) time, O(1) space. It generalizes to k transactions with 2k states (LeetCode 188).
Easy 509. Fibonacci Number Start here Google Meta Amazon Microsoft
Problem: F(0) = 0, F(1) = 1, and F(n) = F(n − 1) + F(n − 2) for n > 1. Return F(n).
Solve it on LeetCode ↗ · Video walkthrough ↗
n = 21n = 32Constraints: 0 ≤ n ≤ 30
Hint 1
F(n) = F(n − 1) + F(n − 2).
Hint 2
Build up from F(0) and F(1), keeping only the last two values.
From brute force to the pattern: Direct recursion recomputes the same values exponentially often (O(2ⁿ)). Bottom-up DP is O(n) time and O(1) space.
Spot it: A recurrence where each value depends on smaller ones is DP's home ground. Plain recursion recomputes the same values exponentially often; building bottom-up computes each value once.
Approach:
- Handle
n < 2directly: the answer isn. - Keep only the last two values:
prev2 = F(i − 2),prev1 = F(i − 1). - For
i = 2..n:current = prev1 + prev2, then slide the two variables forward. - Return
prev1, which is nowF(n).
class Solution:
def fib(self, n: int) -> int:
if n < 2:
return n
prev2 = 0 # F(i - 2)
prev1 = 1 # F(i - 1)
for i in range(2, n + 1):
current = prev1 + prev2
# slide the window of the last two values forward
prev2 = prev1
prev1 = current
return prev1
What the code is doing, line by line:
if n < 2: return n:F(0) = 0andF(1) = 1are the base cases.prev2 = 0,prev1 = 1: we only ever need the last two values, so no array is needed (O(1)space).current = prev1 + prev2: the recurrence.prev2 = prev1,prev1 = current: shift both forward. The order matters: overwriteprev2first, or its new value would be lost.return prev1: after the loop,prev1holdsF(n).
Dry run on the example:
n = 5. Returns 5.
| i | prev2 | prev1 | current = F(i) |
|---|---|---|---|
| 2 | 0 | 1 | 1 |
| 3 | 1 | 1 | 2 |
| 4 | 1 | 2 | 3 |
| 5 | 2 | 3 | 5 |
Edge cases to test:
n = 0→ 0.n = 1→ 1.n = 30(too slow for plain recursion).
Complexity: O(n) time, O(1) space. (Naive recursion is O(2ⁿ); memoised recursion is O(n) time and space.)
Easy 746. Min Cost Climbing Stairs Google Meta Amazon
Problem: cost[i] is the price of stepping on stair i. After paying you may climb 1 or 2 stairs. You may start on stair 0 or 1. Return the minimum cost to reach the top, just past the last stair.
Solve it on LeetCode ↗ · Video walkthrough ↗
cost = [10,15,20]15cost = [1,100,1,1,1,100,1,1,100,1]6Constraints: 2 ≤ cost.length ≤ 1000 · 0 ≤ cost[i] ≤ 999
Hint 1
dp[i] = the cheapest cost to stand on step i.
Hint 2
dp[i] = min(dp[i − 1] + cost[i − 1], dp[i − 2] + cost[i − 2]); the top is step n.
From brute force to the pattern: Trying every sequence of 1- and 2-steps is exponential. Each step's best cost depends only on the two below it: O(n).
Spot it: The cheapest way to reach a stair depends only on the cheapest ways to reach the two stairs below it, so this is 1-D DP. dp[i] = minimum cost to stand on stair i (not yet paid).
Approach:
dp[0] = dp[1] = 0: starting on either of the first two stairs is free.- For
i = 2..n(wherenis the top): you arrive fromi − 1(payingcost[i − 1]) or fromi − 2(payingcost[i − 2]). dp[i] = min(dp[i − 1] + cost[i − 1], dp[i − 2] + cost[i − 2]).- Return
dp[n].
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost) # stair n is the top (past the last stair)
dp = [0] * (n + 1) # dp[i] = cheapest way to stand on stair i
# dp[0] = dp[1] = 0: you may start on stair 0 or 1 for free
for i in range(2, n + 1):
from_one_below = dp[i - 1] + cost[i - 1]
from_two_below = dp[i - 2] + cost[i - 2]
dp[i] = min(from_one_below, from_two_below)
return dp[n]
What the code is doing, line by line:
dp = [0] * (n + 1): one entry per stair plus the top. Entries 0 and 1 stay 0.from_one_below = dp[i - 1] + cost[i - 1]: stand oni − 1as cheaply as possible, pay to leave it, take one step.from_two_below: the same, from two stairs down.dp[i] = min(...): the cheaper way in.return dp[n]: the top. Onlydp[i − 1]anddp[i − 2]are ever read, so two variables would also do.
Dry run on the example:
cost = [10, 15, 20] (LeetCode Example 1). Returns 15 (start on stair 1, pay 15, jump to the top).
| i | from i−1 | from i−2 | dp[i] |
|---|---|---|---|
| 2 | 0 + 15 = 15 | 0 + 10 = 10 | 10 |
| 3 (top) | 10 + 20 = 30 | 0 + 15 = 15 | 15 |
Edge cases to test:
- Two steps.
- Starting on step 1 is cheaper.
- Expensive steps in the middle.
Complexity: O(n) time, O(n) space (O(1) with two variables).
Medium 53. Maximum Subarray Google Meta Amazon Cisco Microsoft
Problem: Return the largest sum of any non-empty contiguous subarray of nums.
Solve it on LeetCode ↗ · Video walkthrough ↗
nums = [-2,1,-3,4,-1,2,1,-5,4]6nums = [1]1Constraints: 1 ≤ nums.length ≤ 10⁵ · -10⁴ ≤ nums[i] ≤ 10⁴
Hint 1
best_ending_here = the best sum of a subarray that ends at the current index.
Hint 2
Either extend the previous best subarray or start fresh: max(x, best_ending_here + x).
From brute force to the pattern: Summing every subarray is O(n²). Kadane's DP keeps only the best sum ending at each index: O(n) time, O(1) space.
Spot it: Define dp[i] = the best sum of a subarray that ends at i. It either extends the best subarray ending at i − 1 or starts fresh at i. That's Kadane's algorithm, a 1-D DP that needs only one variable.
Approach:
current= best sum of a subarray ending at the current index;best= the overall answer.- For each
x:current = max(x, current + x). If the running sum went negative, it only dragsxdown, so start over atx. best = max(best, current).
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
current = nums[0] # best sum of a subarray ending at the current index
best = nums[0] # best sum seen anywhere
for i in range(1, len(nums)):
x = nums[i]
# extend the previous subarray, or start a new one at x
current = max(x, current + x)
best = max(best, current)
return best
What the code is doing, line by line:
current = nums[0],best = nums[0]: the subarray must be non-empty, so start from the first element (not 0, which would be wrong for an all-negative array).current = max(x, current + x): ifcurrentis negative,current + x < x, so dropping the past is better.best = max(best, current): the best subarray ends somewhere, so take the maximum over all ending positions.
Dry run on the example:
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] (LeetCode Example 1). Returns 6 (subarray [4, -1, 2, 1]).
| x | current + x | current | best |
|---|---|---|---|
| -2 | — (start) | -2 | -2 |
| 1 | -1 | 1 (restart) | 1 |
| -3 | -2 | -2 | 1 |
| 4 | 2 | 4 (restart) | 4 |
| -1 | 3 | 3 | 4 |
| 2 | 5 | 5 | 5 |
| 1 | 6 | 6 | 6 |
| -5 | 1 | 1 | 6 |
| 4 | 5 | 5 | 6 |
Edge cases to test:
- All negative → the largest single element (don't start the answer at 0).
- A single element.
- Zeros in the array.
Complexity: O(n) time, O(1) space.
Medium 62. Unique Paths Google Meta Amazon Microsoft
Problem: A robot starts at the top-left of an m × n grid and moves only right or down. How many distinct paths reach the bottom-right?
Solve it on LeetCode ↗ · Video walkthrough ↗
m = 3, n = 728m = 3, n = 23Constraints: 1 ≤ m, n ≤ 100
Hint 1
You arrive at a cell from above or from the left.
Hint 2
dp[r][c] = dp[r − 1][c] + dp[r][c − 1], with 1s along the first row and column.
From brute force to the pattern: Enumerating every right/down path is exponential. Counting with a grid DP is O(m · n); the closed form is C(m + n − 2, m − 1).
Spot it: The last move into a cell comes from above or from the left, and those two sets of paths never overlap. So dp[r][c] = dp[r − 1][c] + dp[r][c − 1]: grid DP.
Approach:
- Every cell in the first row and first column has exactly 1 path (straight right or straight down).
- Fill the rest row by row:
dp[r][c] = dp[r − 1][c] + dp[r][c − 1]. - Return
dp[m − 1][n − 1].
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# first row and first column: only one way to reach each cell
dp = [[1] * n for _ in range(m)]
for r in range(1, m):
for c in range(1, n):
# arrive from above or from the left
dp[r][c] = dp[r - 1][c] + dp[r][c - 1]
return dp[m - 1][n - 1]
What the code is doing, line by line:
dp = [[1] * n for _ in range(m)]: a fresh row for eachr(writing[[1] * n] * mwould make every row the same list). The 1s in row 0 and column 0 are already correct.for r in range(1, m): for c in range(1, n):row by row, sodp[r − 1][c]anddp[r][c − 1]are always ready.dp[r][c] = dp[r - 1][c] + dp[r][c - 1]: add the two disjoint ways in.return dp[m - 1][n - 1]: the bottom-right corner.
Dry run on the example:
m = 3, n = 7 (LeetCode Example 1). Returns 28. The finished table:
| row | c=0 | c=1 | c=2 | c=3 | c=4 | c=5 | c=6 |
|---|---|---|---|---|---|---|---|
| 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 2 | 1 | 3 | 6 | 10 | 15 | 21 | 28 |
Edge cases to test:
m = 1orn = 1→ 1.- A square grid.
- Large grids (watch overflow in other languages).
Complexity: O(m · n) time and space (one row is enough for O(n) space).
Medium 63. Unique Paths II Google Amazon Microsoft
Problem: Same robot, but obstacleGrid[r][c] == 1 marks a blocked cell. Count the paths from the top-left to the bottom-right that avoid obstacles.
Solve it on LeetCode ↗ · Video walkthrough ↗
obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]2obstacleGrid = [[0,1],[0,0]]1Constraints: m == obstacleGrid.length · n == obstacleGrid[i].length · 1 ≤ m, n ≤ 100 · obstacleGrid[i][j] is 0 or 1.
Hint 1
Same as Unique Paths, but an obstacle cell has 0 paths.
Hint 2
Watch the first row and column: an obstacle cuts off every cell after it.
From brute force to the pattern: Enumerating paths is exponential. The grid DP stays O(r · c), with obstacles simply set to 0.
Spot it: Unique Paths with one change: no path can pass through an obstacle, so its count is 0. The first row and column are no longer all 1s either, because a block cuts off everything after it.
Approach:
dp[r][c]= number of paths to(r, c). If the start is blocked, the answer is 0.- Set
dp[0][0] = 1, then visit cells row by row. - Obstacle →
dp[r][c] = 0. Otherwise add the count from above (ifr > 0) and from the left (ifc > 0). - Return the bottom-right count.
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
rows = len(obstacleGrid)
cols = len(obstacleGrid[0])
dp = [[0] * cols for _ in range(rows)]
if obstacleGrid[0][0] == 1:
return 0
dp[0][0] = 1
for r in range(rows):
for c in range(cols):
if obstacleGrid[r][c] == 1:
dp[r][c] = 0 # nothing can pass through a rock
continue
if r > 0:
dp[r][c] = dp[r][c] + dp[r - 1][c] # from above
if c > 0:
dp[r][c] = dp[r][c] + dp[r][c - 1] # from the left
return dp[rows - 1][cols - 1]
What the code is doing, line by line:
dp = [[0] * cols ...]: start every count at 0.if obstacleGrid[0][0] == 1: return 0: a blocked start means no paths at all.dp[0][0] = 1: one way to be at the start.if obstacleGrid[r][c] == 1: dp[r][c] = 0; continue: an obstacle contributes nothing to its neighbours.if r > 0/if c > 0: add the counts from above and from the left when those cells exist. This also handles the first row and column without special cases (and(0, 0)keeps its 1).
Dry run on the example:
obstacleGrid = [[0, 0, 0], [0, 1, 0], [0, 0, 0]] (LeetCode Example 1). Returns 2. The finished table (X = obstacle):
| row | c=0 | c=1 | c=2 |
|---|---|---|---|
| 0 | 1 | 1 | 1 |
| 1 | 1 | X (0) | 1 |
| 2 | 1 | 1 | 2 |
Edge cases to test:
- The start or end cell is blocked → 0.
- An obstacle in the first row or column.
- A single cell.
Complexity: O(rows · cols) time and space.
Medium 120. Triangle Google Meta Amazon Microsoft
Problem: Return the minimum path sum from the top of triangle to the bottom. From index i on one row you may move to index i or i + 1 on the row below.
Solve it on LeetCode ↗ · Video walkthrough ↗
triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]11triangle = [[-10]]-10Constraints: 1 ≤ triangle.length ≤ 200 · triangle[0].length == 1 · triangle[i].length == triangle[i - 1].length + 1 · -10⁴ ≤ triangle[i][j] ≤ 10⁴
Hint 1
Working bottom-up, each cell needs only the two cells beneath it.
Hint 2
best[i] = row[i] + min(best[i], best[i + 1]), from the second-to-last row up to the top.
From brute force to the pattern: Trying every top-to-bottom path is O(2ⁿ). Paths share sub-paths, so bottom-up DP computes each cell once: O(n²) time, O(n) space.
Spot it: Choices that branch and re-merge (two parents share a child) → overlapping sub-problems → DP. Working from the bottom up, each cell needs only the two cells beneath it, and the answer ends up at the top.
Approach:
- Copy the bottom row into
best: from a bottom cell, the cheapest path is the cell itself. - For each row above, going up:
best[i] = row[i] + min(best[i], best[i + 1]). - After the top row,
best[0]is the answer.
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
# best[i] = cheapest path from (current row, i) down to the bottom
best = list(triangle[-1])
for r in range(len(triangle) - 2, -1, -1):
row = triangle[r]
for i in range(len(row)):
# step to the cheaper of the two cells below
best[i] = row[i] + min(best[i], best[i + 1])
return best[0]
What the code is doing, line by line:
best = list(triangle[-1]): a copy of the bottom row, so the input is left unchanged.for r in range(len(triangle) - 2, -1, -1):from the second-to-last row up to row 0.best[i] = row[i] + min(best[i], best[i + 1]):best[i]andbest[i + 1]still describe the row below, the two cells reachable from(r, i). Going left to right is safe becausebest[i + 1]hasn't been overwritten yet when we read it.return best[0]: the top row has one cell.
Dry run on the example:
triangle = [[2], [3, 4], [6, 5, 7], [4, 1, 8, 3]] (LeetCode Example 1). Returns 11 (2 + 3 + 5 + 1).
| row processed | best after |
|---|---|
bottom [4, 1, 8, 3] (copied) | [4, 1, 8, 3] |
[6, 5, 7] | [7, 6, 10] (+ unused tail) |
[3, 4] | [9, 10] (+ unused tail) |
[2] | [11] → 11 |
Edge cases to test:
- A single row.
- Negative numbers.
- The greedy choice (always the smaller child) fails, e.g.
[[1], [2, 3], [9, 9, 0]].
Complexity: O(n²) time for n rows (every number once), O(n) extra space.
Medium 91. Decode Ways Google Meta Microsoft
Problem: Letters map to 1..26. Count the ways to decode a digit string.
s = "12"2s = "226"3Constraints: 1 ≤ s.length ≤ 100 · s contains only digits and may contain leading zero(s).
Hint 1
How can the last letter of a decoding be formed?
Hint 2
dp[i] = dp[i − 1] (if valid) + dp[i − 2] (if valid).
From brute force to the pattern: Recursing on both choices at every digit is exponential; the DP is linear.
Spot it: Each decoding of s[:i] ends with a 1-digit or 2-digit code → 1D DP, like climbing stairs with validity checks.
Why not another pattern? Recursion without memo branches twice per digit: exponential.
Approach:
prev2 = 1(empty prefix),prev1 = ways for s[:1].- For each
i: ways = (s[i−1] != '0'? prev1 : 0) + (10 ≤ int(s[i−2:i]) ≤ 26? prev2 : 0). - Shift the window.
class Solution:
def numDecodings(self, s: str) -> int:
prev2 = 1 # ways to decode the empty prefix
prev1 = 1 if s[0] != "0" else 0 # ways for s[:1]
for i in range(2, len(s) + 1):
cur = 0
if s[i - 1] != "0":
cur += prev1 # last letter uses one digit
if 10 <= int(s[i - 2:i]) <= 26:
cur += prev2 # last letter uses two digits
prev2, prev1 = prev1, cur
return prev1
What the code is doing, line by line:
- A single digit decodes only if it isn't 0.
- A pair decodes only in
10..26(a leading 0 like06isn't allowed). - Only the last two counts are needed, so space is
O(1).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Leading
0→ 0. "10","20"(0 must pair)."30"→ 0.
Complexity: O(n) time, O(1) space.
Medium 97. Interleaving String Google Amazon
Problem: Is s3 formed by interleaving s1 and s2 (keeping each one's order)?
s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"trues1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"falseConstraints: 0 ≤ s1.length, s2.length ≤ 100 · 0 ≤ s3.length ≤ 200 · s1, s2, and s3 consist of lowercase English letters.
Hint 1
Greedy fails when both strings could supply the next character.
Hint 2
Define dp[i][j] for using i chars of s1 and j chars of s2.
From brute force to the pattern: Trying both sources at each tie is exponential; the table has (m + 1)(n + 1) states.
Spot it: Using i characters of s1 and j of s2 fixes the position i + j in s3 → 2D DP. The last character of s3[:i+j] came from s1 or from s2.
Why not another pattern? Greedy (take from s1 when it matches) fails when both strings match the next character.
Approach:
- If lengths don't add up, false.
dp[0][0] = True.dp[i][j] = (dp[i−1][j] and s1[i−1] == s3[i+j−1]) or (dp[i][j−1] and s2[j−1] == s3[i+j−1]).
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
m, n = len(s1), len(s2)
if m + n != len(s3):
return False
dp = [[False] * (n + 1) for _ in range(m + 1)] # dp[i][j]: s1[:i] + s2[:j] form s3[:i+j]
dp[0][0] = True
for i in range(m + 1):
for j in range(n + 1):
if i > 0 and dp[i - 1][j] and s1[i - 1] == s3[i + j - 1]:
dp[i][j] = True # last character taken from s1
if j > 0 and dp[i][j - 1] and s2[j - 1] == s3[i + j - 1]:
dp[i][j] = True # last character taken from s2
return dp[m][n]
What the code is doing, line by line:
- The state
(i, j)determines exactly which prefix ofs3has been built. - Both sources are checked, which handles ties where
s1ands2offer the same character.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Lengths don't add up → false.
- Empty strings.
- Repeated characters in both strings.
Complexity: O(m · n) time and space (a single row gives O(n) space).
Medium 139. Word Break Google Meta Amazon Microsoft Netflix
Problem: Can s be split into a sequence of dictionary words (words reusable)?
s = "leetcode", wordDict = ["leet","code"]trues = "applepenapple", wordDict = ["apple","pen"]trueConstraints: 1 ≤ s.length ≤ 300 · 1 ≤ wordDict.length ≤ 1000 · 1 ≤ wordDict[i].length ≤ 20 · s and wordDict[i] consist of only lowercase English letters. · All the strings of wordDict are unique.
Hint 1
Can the prefix s[:i] be segmented? What does that depend on?
Hint 2
ok[i] is true if some word ends at i and ok[start] is true.
From brute force to the pattern: Trying every split recursively is exponential (e.g. aaaa…ab); the prefix DP is polynomial.
Spot it: Whether a prefix can be segmented depends only on shorter prefixes → 1D DP: ok[i] is true if some word ends at i and the prefix before it is ok.
Why not another pattern? Backtracking on every split point re-solves the same suffixes exponentially often.
Approach:
ok[0] = True.- For each end
i, for each word lengthLin the dictionary: ifok[i − L]ands[i − L:i]is a word,ok[i] = True.
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
words = set(wordDict)
lengths = set(map(len, words))
ok = [False] * (len(s) + 1) # ok[i]: s[:i] can be segmented
ok[0] = True
for i in range(1, len(s) + 1):
for L in lengths:
if L <= i and ok[i - L] and s[i - L:i] in words:
ok[i] = True # a word ends here after a good prefix
break
return ok[len(s)]
What the code is doing, line by line:
- Only word lengths that actually occur need checking.
ok[i − L]says the part before the word is segmentable.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Words reused several times.
- No possible split.
- Overlapping candidate words (
cats/cat).
Complexity: O(n · L · w) time where L is the number of distinct word lengths and w the word length (slicing); O(n) space.
Medium 152. Maximum Product Subarray Google Meta Amazon
Problem: Return the largest product of a non-empty contiguous subarray.
nums = [2,3,-2,4]6nums = [-2,0,-1]0Constraints: 1 ≤ nums.length ≤ 2 * 10⁴ · -10 ≤ nums[i] ≤ 10 · The product of any subarray of nums is guaranteed to fit in a 32-bit integer.
Hint 1
Kadane's algorithm doesn't work directly. Why?
Hint 2
Keep both the maximum and minimum product of a subarray ending at each index.
From brute force to the pattern: All subarrays: O(n²). Tracking max and min products: O(n).
Spot it: Like Kadane's algorithm, but a negative number turns the smallest product into the largest. So keep two DP values per position: max and min product of a subarray ending here.
Why not another pattern? Kadane with only a running maximum fails on [-2, 3, -4] (answer 24).
Approach:
hi = lo = best = nums[0].- For each
x: candidatesx,hi · x,lo · x;hi = max,lo = minof them. best = max(best, hi).
class Solution:
def maxProduct(self, nums: List[int]) -> int:
hi = lo = best = nums[0] # max / min product of a subarray ending here
for x in nums[1:]:
options = (x, hi * x, lo * x) # start fresh, or extend either extreme
hi = max(options)
lo = min(options) # a later negative can turn this into the max
best = max(best, hi)
return best
What the code is doing, line by line:
- Starting fresh at
xhandles zeros and bad prefixes. - Multiplying by a negative swaps the roles of the largest and smallest products, so both are kept.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Zeros split the array.
- A single negative number.
- An even number of negatives.
Complexity: O(n) time, O(1) space.
Medium 221. Maximal Square Google
Problem: In a binary matrix, return the area of the largest square containing only 1s.
matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]4matrix = [["0","1"],["1","0"]]1Constraints: m == matrix.length · n == matrix[i].length · 1 ≤ m, n ≤ 300 · matrix[i][j] is '0' or '1'.
Hint 1
Think about squares by their bottom-right corner.
Hint 2
How does the square ending at a cell relate to the squares ending at its three neighbours?
From brute force to the pattern: Checking every square from every corner is O(m · n · min(m,n)²). The DP is O(mn).
Spot it: The largest square with bottom-right corner (r, c) is limited by the squares ending at its top, left and top-left neighbours → 2D DP: 1 + min(of the three).
Why not another pattern? Growing a square from every cell is O((mn)²).
Approach:
side[r][c] = 0for a'0'.- Otherwise
side[r][c] = 1 + min(side[r−1][c], side[r][c−1], side[r−1][c−1]). - Answer:
max(side)².
class Solution:
def maximalSquare(self, matrix: List[List[str]]) -> int:
m, n = len(matrix), len(matrix[0])
side = [[0] * (n + 1) for _ in range(m + 1)] # side[r+1][c+1]: biggest square ending at (r, c)
best = 0
for r in range(m):
for c in range(n):
if matrix[r][c] == "1":
side[r + 1][c + 1] = 1 + min(side[r][c + 1], side[r + 1][c], side[r][c]) # limited by all three
best = max(best, side[r + 1][c + 1])
return best * best
What the code is doing, line by line:
- A square of side
sending at(r, c)needs squares of sides − 1ending above, left and diagonally. - The padding row/column of zeros avoids boundary checks.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No 1s → 0.
- All 1s →
min(m,n)². - One row.
Complexity: O(m · n) time and space.
Medium 343. Integer Break Google Meta Microsoft
Problem: Split n ≥ 2 into at least two positive integers to maximise their product.
n = 21n = 1036Constraints: 2 ≤ n ≤ 58
Hint 1
Pick the first part j. What's the best you can do with the rest?
Hint 2
The rest can stay whole or be split further.
From brute force to the pattern: Enumerating partitions is exponential; the DP is O(n²).
Spot it: The best product for i uses a first part j and then either keeps i − j whole or splits it further (its own best) → 1D DP like unbounded knapsack.
Why not another pattern? Trying every partition of n is exponential (partition numbers grow fast).
Approach:
best[1] = 1.- For
ifrom 2 ton:best[i] = max(j · max(i − j, best[i − j]))overjin1..i − 1.
class Solution:
def integerBreak(self, n: int) -> int:
best = [0] * (n + 1) # best[i]: max product splitting i into at least two parts
best[1] = 1
for i in range(2, n + 1):
for j in range(1, i):
best[i] = max(best[i], j * max(i - j, best[i - j])) # keep the rest whole or split it
return best[n]
What the code is doing, line by line:
max(i − j, best[i − j])allows the rest to stay whole, sincebestitself forces at least two parts.- The math shortcut is to use as many 3s as possible, but the DP explains why without proof.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 2→ 1.n = 3→ 2.n = 4→ 4.
Complexity: O(n²) time, O(n) space.
Medium 368. Largest Divisible Subset Google Amazon Microsoft
Problem: Return a largest subset where every pair (a, b) satisfies a % b == 0 or b % a == 0.
nums = [1,2,3][1,2]nums = [1,2,4,8][1,2,4,8]Constraints: 1 ≤ nums.length ≤ 1000 · 1 ≤ nums[i] ≤ 2 * 10⁹ · All the integers in nums are unique.
Hint 1
Sort the numbers. What structure must a divisible subset have?
Hint 2
It's a chain where each element divides the next: use an LIS-style DP.
From brute force to the pattern: Trying all subsets is O(2ⁿ · n²). The chain DP is O(n²).
Spot it: After sorting, a divisible subset is a chain where each element divides the next (divisibility is transitive). That's Longest Increasing Subsequence with nums[i] % nums[j] == 0 as the link.
Why not another pattern? Checking all subsets is O(2ⁿ).
Approach:
- Sort.
size[i]= longest chain ending ati;parent[i]= previous element. - For each
j < iwithnums[i] % nums[j] == 0: extend ifsize[j] + 1is bigger. - Rebuild from the index with the largest
size.
class Solution:
def largestDivisibleSubset(self, nums: List[int]) -> List[int]:
nums = sorted(nums)
n = len(nums)
size = [1] * n # longest divisible chain ending at i
parent = [-1] * n
for i in range(n):
for j in range(i):
if nums[i] % nums[j] == 0 and size[j] + 1 > size[i]:
size[i] = size[j] + 1 # nums[j] divides nums[i]: extend its chain
parent[i] = j
i = max(range(n), key=lambda t: size[t])
subset = []
while i != -1:
subset.append(nums[i]) # walk the chain backwards
i = parent[i]
return subset[::-1]
What the code is doing, line by line:
- If
a | bandb | cthena | c, so checking only consecutive chain elements is enough. - This is the
O(n²)LIS DP with a different link condition. - Parent pointers rebuild one optimal chain.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single element.
- Several answers of the same size (any is accepted).
- 1 divides everything.
Complexity: O(n²) time, O(n) space.
Medium 377. Combination Sum IV Google Amazon
Problem: Count ordered sequences of numbers from nums (reusable) that add up to target.
nums = [1,2,3], target = 47nums = [9], target = 30Constraints: 1 ≤ nums.length ≤ 200 · 1 ≤ nums[i] ≤ 1000 · All the elements of nums are unique. · 1 ≤ target ≤ 1000
Hint 1
The last number of a sequence summing to t can be any x in nums.
Hint 2
Compare with Coin Change II: why does the loop order matter?
From brute force to the pattern: Listing every sequence is exponential. The DP is O(target · n).
Spot it: Sequences, not sets: different orders count separately. The last number of the sequence is any x, leaving t − x → 1D DP over the total with the target in the outer loop.
Why not another pattern? Coin Change II's loop order (coins outside) counts combinations, not sequences.
Approach:
ways[0] = 1.- For
tfrom 1 totarget:ways[t] = Σ ways[t − x]overx ≤ t.
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
ways = [0] * (target + 1)
ways[0] = 1 # the empty sequence
for t in range(1, target + 1): # totals outside: every order is counted
for x in nums:
if x <= t:
ways[t] += ways[t - x] # sequences ending with x
return ways[target]
What the code is doing, line by line:
- Classifying sequences by their last number gives each sequence exactly once.
- Putting the total in the outer loop allows any number at any position, which is why orders count separately.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No way to reach target → 0.
targetsmaller than all numbers.- One number.
Complexity: O(target · n) time, O(target) space.
Medium 396. Rotate Function Google Meta Amazon
Problem: F(k) = Σ i · arrₖ[i] where arrₖ is nums rotated right by k. Return the maximum F(k).
nums = [4,3,2,6]26nums = [100]0Constraints: n == nums.length · 1 ≤ n ≤ 10⁵ · -100 ≤ nums[i] ≤ 100
Hint 1
Write out F(1) − F(0). What changes?
Hint 2
Every element gains one coefficient step except the one that wraps around.
From brute force to the pattern: Computing every F(k) directly is O(n²); the recurrence is O(n).
Spot it: Computing each F(k) from scratch is O(n²). Each rotation adds 1 to every coefficient (adding sum) except the last element, which drops from n − 1 to 0 (subtracting n · that element). A recurrence from one state to the next.
Why not another pattern? Direct evaluation of all n rotations is O(n²).
Approach:
F = Σ i · nums[i],total = sum(nums).- For
k = 1..n − 1:F += total − n · nums[n − k]. - Track the maximum.
class Solution:
def maxRotateFunction(self, nums: List[int]) -> int:
n = len(nums)
total = sum(nums)
f = sum(i * x for i, x in enumerate(nums)) # F(0)
best = f
for k in range(1, n):
f += total - n * nums[n - k] # everyone shifts right; the last element wraps to index 0
best = max(best, f)
return best
What the code is doing, line by line:
- Rotating right moves every element one index up (adding its value once more), so
Fgrows bytotal. - The element at the end wraps to index 0: it had coefficient
n − 1, now 0, a loss ofn · valueafter thetotalwas added. - In rotation
kthe wrapping element isnums[n − k].
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- One element → 0.
- Negative numbers.
- All equal values.
Complexity: O(n) time, O(1) space.
Medium 486. Predict the Winner Google Meta Amazon Cisco Microsoft
Problem: Players alternately take a number from either end. Does player 1 win (ties count as a win) with optimal play?
nums = [1,5,2]falsenums = [1,5,233,7]trueConstraints: 1 ≤ nums.length ≤ 20 · 0 ≤ nums[i] ≤ 10⁷
Hint 1
Greedy taking the larger end fails. Think in terms of score difference.
Hint 2
Define dp[i][j] = the best difference the current player can get on nums[i..j].
From brute force to the pattern: Exploring the full game tree is O(2ⁿ); the interval DP is O(n²).
Spot it: A two-player game on an interval → interval DP with the score difference: the player to move on nums[i..j] takes one end and then faces the opponent's best difference on what's left.
Why not another pattern? Greedy (take the bigger end) loses on cases like [1, 5, 2] vs [1, 5, 233, 7].
Approach:
diff[i][i] = nums[i].diff[i][j] = max(nums[i] − diff[i+1][j], nums[j] − diff[i][j−1]).- Player 1 wins if
diff[0][n−1] ≥ 0.
class Solution:
def predictTheWinner(self, nums: List[int]) -> bool:
n = len(nums)
diff = [[0] * n for _ in range(n)] # diff[i][j]: mover's score minus opponent's on nums[i..j]
for i in range(n - 1, -1, -1):
diff[i][i] = nums[i]
for j in range(i + 1, n):
take_left = nums[i] - diff[i + 1][j] # opponent then plays optimally on the rest
take_right = nums[j] - diff[i][j - 1]
diff[i][j] = max(take_left, take_right)
return diff[0][n - 1] >= 0
What the code is doing, line by line:
- Using the difference removes the need to track whose turn it is: the opponent's best difference is subtracted.
- Rows go from the bottom up so
diff[i + 1][j]is ready beforediff[i][j].
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- One number → true.
- A tie → true.
- An even count (player 1 can always at least tie).
Complexity: O(n²) time and space.
Medium 494. Target Sum Google Amazon Microsoft
Problem: Put + or − before each number; count the expressions that evaluate to target.
nums = [1,1,1,1,1], target = 35nums = [1], target = 11Constraints: 1 ≤ nums.length ≤ 20 · 0 ≤ nums[i] ≤ 1000 · 0 ≤ sum(nums[i]) ≤ 1000 · -1000 ≤ target ≤ 1000
Hint 1
What do you need to remember after choosing signs for the first few numbers?
Hint 2
Keep a map from running sum to the number of ways to reach it.
From brute force to the pattern: Enumerating all 2ⁿ sign patterns is exponential; merging equal sums makes it O(n · S).
Spot it: After processing some numbers, only the running sum matters → DP over sums (a hash map from sum to number of ways). Equivalently a 0/1 knapsack: choose the positive set P with sum(P) = (total + target) / 2.
Why not another pattern? Trying all 2ⁿ sign choices is too slow for n = 20 in many languages and doesn't scale.
Approach:
ways = {0: 1}.- For each number
x: new map withs + xands − xfor everys, adding counts. - Return
ways[target].
from collections import defaultdict
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
ways = {0: 1} # running sum -> number of sign choices giving it
for x in nums:
nxt = defaultdict(int)
for s, count in ways.items():
nxt[s + x] += count # put + before x
nxt[s - x] += count # put - before x
ways = nxt
return ways.get(target, 0)
What the code is doing, line by line:
- Different sign choices that reach the same sum are merged, so the state space is bounded by the range of sums.
- Zeros double the count (both signs give the same sum), which the map handles naturally.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Zeros (both signs count).
- Unreachable target → 0.
- Negative target.
Complexity: O(n · S) time and space, where S is the range of possible sums (≤ 2·sum + 1).
Medium 516. Longest Palindromic Subsequence Google Meta Amazon Microsoft
Problem: Return the length of the longest palindromic subsequence of s.
s = "bbbab"4s = "cbbd"2Constraints: 1 ≤ s.length ≤ 1000 · s consists only of lowercase English letters.
Hint 1
Think about the first and last characters of the range.
Hint 2
If they match they can both be in the palindrome; if not, drop one.
From brute force to the pattern: All subsequences: O(2ⁿ). Interval DP: O(n²).
Spot it: A palindrome is decided by its outer pair, then its inside → interval DP on s[i..j]. Equal ends both join; different ends mean one of them isn't used.
Why not another pattern? Checking all 2ⁿ subsequences is impossible; it also equals LCS(s, reversed s) but the interval form is direct.
Approach:
L[i][i] = 1.s[i] == s[j]:L[i][j] = L[i+1][j−1] + 2; elsemax(L[i+1][j], L[i][j−1]).
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
n = len(s)
L = [[0] * n for _ in range(n)] # L[i][j]: longest palindromic subsequence in s[i..j]
for i in range(n - 1, -1, -1):
L[i][i] = 1
for j in range(i + 1, n):
if s[i] == s[j]:
L[i][j] = L[i + 1][j - 1] + 2 # both ends wrap the inside
else:
L[i][j] = max(L[i + 1][j], L[i][j - 1]) # one end can't be used
return L[0][n - 1]
What the code is doing, line by line:
- Rows from bottom to top make
L[i + 1][…]ready first. - When
j = i + 1and the characters match,L[i + 1][j − 1]is the empty range (0).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All same letters → n.
- All distinct → 1.
- Length 1.
Complexity: O(n²) time and space.
Medium 583. Delete Operation for Two Strings Google
Problem: Return the minimum number of deletions (in either string) to make word1 and word2 equal.
word1 = "sea", word2 = "eat"2word1 = "leetcode", word2 = "etco"4Constraints: 1 ≤ word1.length, word2.length ≤ 500 · word1 and word2 consist of only lowercase English letters.
Hint 1
After the deletions, what is the remaining string in relation to both words?
Hint 2
Keep the longest common subsequence.
From brute force to the pattern: Trying all subsets to delete is exponential; LCS is O(mn).
Spot it: What remains in both strings is a common subsequence; to delete least, keep the longest one → LCS DP.
Why not another pattern? Edit distance with replacements gives a different (smaller) number; only deletions are allowed here.
Approach:
- LCS table:
L[i][j] = L[i−1][j−1] + 1if chars match elsemax(L[i−1][j], L[i][j−1]). - Answer:
m + n − 2 · L[m][n].
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
L = [[0] * (n + 1) for _ in range(m + 1)] # L[i][j]: LCS of word1[:i], word2[:j]
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
L[i][j] = L[i - 1][j - 1] + 1 # keep this common letter
else:
L[i][j] = max(L[i - 1][j], L[i][j - 1])
return m + n - 2 * L[m][n] # delete everything outside the LCS
What the code is doing, line by line:
- The final common string must be a subsequence of both words.
- Keeping the LCS minimises the letters deleted from each side.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No common letters →
m + n. - Equal words → 0.
- One empty word.
Complexity: O(m · n) time and space.
Medium 638. Shopping Offers Google
Problem: Buy exactly the needed quantities of up to 6 items using single prices and bundle offers. Return the minimum cost.
price = [2,5], special = [[3,0,5],[1,2,10]], needs = [3,2]14price = [2,3,4], special = [[1,1,0,4],[2,2,1,9]], needs = [1,2,1]11Constraints: n == price.length == needs.length · 1 ≤ n ≤ 6 · 0 ≤ price[i], needs[i] ≤ 10 · 1 ≤ special.length ≤ 100 · special[i].length == n + 1 · 0 ≤ special[i][j] ≤ 50 · The input is generated that at least one of special[i][j] is non-zero for 0 ≤ j ≤ n - 1.
Hint 1
The needs are small. What should the state of a recursion be?
Hint 2
From a state, either buy the rest at single prices or apply one offer and recurse; memoise.
From brute force to the pattern: Trying every sequence of offers without memo repeats states; the memo bounds the work by the number of need vectors.
Spot it: The state is the vector of remaining needs (small: ≤ 6 items, ≤ 10 each) → memoised DP over tuples. From a state, either pay full price for everything or apply an offer that fits and recurse.
Why not another pattern? Greedy (best discount first) fails because offers interact.
Approach:
cost(needs): start with buying everything at single prices.- For each offer that doesn't exceed any need:
offer price + cost(needs − offer). - Memoise on the
needstuple.
from functools import lru_cache
class Solution:
def shoppingOffers(self, price: List[int], special: List[List[int]], needs: List[int]) -> int:
n = len(price)
@lru_cache(maxsize=None)
def cost(needs):
best = sum(p * q for p, q in zip(price, needs)) # no offers: buy singly
for offer in special:
rest = tuple(needs[i] - offer[i] for i in range(n))
if min(rest) >= 0: # the offer doesn't give more than needed
best = min(best, offer[n] + cost(rest))
return best
return cost(tuple(needs))
What the code is doing, line by line:
- Each state is a tuple, so it can be a cache key.
- Offers that would overshoot any item are skipped (buying extra isn't allowed).
- The number of states is at most
11⁶, and much less in practice.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- An offer more expensive than buying singly.
- No offer fits.
- Needs all zero → 0.
Complexity: O(S · offers · n) time, where S is the number of reachable needs vectors.
Medium 647. Palindromic Substrings Google Meta Amazon Cisco Microsoft
Problem: Count the substrings of s that are palindromes.
s = "abc"3s = "aaa"6Constraints: 1 ≤ s.length ≤ 1000 · s consists of lowercase English letters.
Hint 1
A palindrome is determined by its centre and radius.
Hint 2
Expand around each of the 2n − 1 centres.
From brute force to the pattern: Testing every substring is O(n³); centre expansion is O(n²).
Spot it: A palindrome grows from its centre: s[l..r] is one if s[l+1..r−1] is and s[l] == s[r]. Expanding around every centre applies that DP relation without a table.
Why not another pattern? Checking each of the O(n²) substrings separately is O(n³).
Approach:
- For each centre (a character, or a gap between two):
l, rstart there. - While
s[l] == s[r]: count one, expand outwards.
class Solution:
def countSubstrings(self, s: str) -> int:
n = len(s)
count = 0
for center in range(2 * n - 1): # n letters and n - 1 gaps
l = center // 2
r = l + center % 2
while l >= 0 and r < n and s[l] == s[r]:
count += 1 # s[l..r] is a palindrome
l -= 1
r += 1
return count
What the code is doing, line by line:
- Even
centervalues are letters (odd-length palindromes); odd values are gaps (even length). - Every expansion step is a new palindrome with the same centre.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All equal letters → n(n + 1)/2.
- All distinct → n.
- Single character.
Complexity: O(n²) time, O(1) space.
Medium 673. Number of Longest Increasing Subsequence Google Amazon
Problem: Return how many longest strictly increasing subsequences there are.
nums = [1,3,5,4,7]2nums = [2,2,2,2,2]5Constraints: 1 ≤ nums.length ≤ 2000 · -10⁶ ≤ nums[i] ≤ 10⁶ · The answer is guaranteed to fit inside a 32-bit integer.
Hint 1
Start from the O(n²) LIS DP.
Hint 2
Alongside the length, keep how many subsequences reach that length.
From brute force to the pattern: Listing every increasing subsequence is exponential; the counting DP is O(n²).
Spot it: The O(n²) LIS DP extended with counts: length[i] and count[i] for subsequences ending at i. A longer extension resets the count; an equally long one adds to it.
Why not another pattern? The patience-sorting O(n log n) LIS gives the length but not the count without extra structure.
Approach:
length[i] = count[i] = 1.- For
j < iwithnums[j] < nums[i]: iflength[j] + 1 > length[i], take it and copycount[j]; if equal, addcount[j]. - Sum
count[i]over indices with the maximum length.
class Solution:
def findNumberOfLIS(self, nums: List[int]) -> int:
n = len(nums)
length = [1] * n # longest increasing subsequence ending at i
count = [1] * n # how many of that length end at i
for i in range(n):
for j in range(i):
if nums[j] < nums[i]:
if length[j] + 1 > length[i]:
length[i] = length[j] + 1 # a longer one: restart the count
count[i] = count[j]
elif length[j] + 1 == length[i]:
count[i] += count[j] # another way to the same length
top = max(length)
return sum(c for l, c in zip(length, count) if l == top)
What the code is doing, line by line:
count[i]counts distinct index sequences, matching the problem's definition.- The final answer adds the counts over every possible ending index.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All equal → n (each element alone).
- Strictly increasing → 1.
- Decreasing → n.
Complexity: O(n²) time, O(n) space.
Medium 714. Best Time to Buy and Sell Stock with Transaction Fee Google Apple
Problem: Unlimited transactions, each costing fee. Return the maximum profit.
prices = [1,3,2,8,4,9], fee = 28prices = [1,3,7,5,10,3], fee = 36Constraints: 1 ≤ prices.length ≤ 5 * 10⁴ · 1 ≤ prices[i] < 5 * 10⁴ · 0 ≤ fee < 5 * 10⁴
Hint 1
What two situations can you be in at the end of each day?
Hint 2
Track the best balance holding and not holding a share.
From brute force to the pattern: Trying all buy/sell day choices is exponential; two states per day is O(n).
Spot it: Each day you either hold a share or don't → two-state DP. Selling moves hold → cash and pays the fee.
Why not another pattern? Adding every price rise (unlimited transactions without fee) pays the fee on each small rise.
Approach:
cash = 0,hold = −prices[0].- Each day:
cash = max(cash, hold + p − fee),hold = max(hold, cash − p).
class Solution:
def maxProfit(self, prices: List[int], fee: int) -> int:
cash = 0 # best balance with no share
hold = -prices[0] # best balance holding a share
for p in prices[1:]:
cash = max(cash, hold + p - fee) # sell today (pay the fee)
hold = max(hold, cash - p) # buy today
return cash
What the code is doing, line by line:
- Using the updated
cashforholdon the same day only allows sell-then-buy on one day, which never helps, so it's safe. - The fee makes merging nearby rises into one transaction better, which the DP finds automatically.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Fee larger than any rise → 0.
- Falling prices → 0.
- One day.
Complexity: O(n) time, O(1) space.
Medium 740. Delete and Earn Google Amazon Microsoft
Problem: Taking a number x earns x but deletes every x − 1 and x + 1. Return the maximum points.
nums = [3,4,2]6nums = [2,2,3,3,3,4]9Constraints: 1 ≤ nums.length ≤ 2 * 10⁴ · 1 ≤ nums[i] ≤ 10⁴
Hint 1
If you take one x, should you take all of them?
Hint 2
Group by value. Which known problem has "can't pick neighbours"?
From brute force to the pattern: Trying every subset of values is exponential; the House Robber DP is linear in the value range.
Spot it: If you take one x, take them all. Sum points per value: points[x] = x · count(x). Taking x forbids x − 1 and x + 1, which is exactly House Robber on the value line.
Why not another pattern? Greedy (take the biggest total first) fails when two neighbours together beat the middle value.
Approach:
points[v] = v · count(v)forvup tomax(nums).- House Robber:
take, skiprunning values;take = skip + points[v],skip = max(previous take, previous skip).
from collections import Counter
class Solution:
def deleteAndEarn(self, nums: List[int]) -> int:
count = Counter(nums)
take = skip = 0 # best total when value v-1 is taken / not taken
for v in range(1, max(nums) + 1):
gain = v * count[v] # take every copy of v
take, skip = skip + gain, max(take, skip) # taking v needs v-1 skipped
return max(take, skip)
What the code is doing, line by line:
- Taking one copy of
xalready deletes allx ± 1, so the other copies ofxare free to take too. - Values form a line where neighbours conflict: House Robber.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Values with gaps (no conflict).
- All the same value.
- One number.
Complexity: O(n + max) time, O(n) space for the counter.
Medium 788. Rotated Digits Google Amazon
Problem: A number is good if rotating each digit 180° gives a valid, different number. Count good numbers in 1..n.
n = 104n = 10Constraints: 1 ≤ n ≤ 10⁴
Hint 1
Classify digits into: rotate to themselves, rotate to another digit, invalid.
Hint 2
A number's class depends on its prefix's class and its last digit.
From brute force to the pattern: Examining every digit of every number is O(n log n); prefix reuse makes it O(n) (digit DP gives O(log n)).
Spot it: Digits 0, 1, 8 rotate to themselves; 2, 5, 6, 9 change; 3, 4, 7 break the number. A number's status follows from i // 10's status and its last digit → DP over numbers (a simple digit DP).
Why not another pattern? Checking each number's digits separately is O(n log n); reusing the prefix status is O(n).
Approach:
state[i]: 0 = invalid, 1 = valid and unchanged, 2 = valid and changed.state[0] = 1.state[i]= invalid if the last digit is 3/4/7 or the prefix is invalid; changed if either is changed; else unchanged.- Count
i ≤ nwithstate[i] == 2.
class Solution:
def rotatedDigits(self, n: int) -> int:
kind = {0: 1, 1: 1, 8: 1, 2: 2, 5: 2, 6: 2, 9: 2, 3: 0, 4: 0, 7: 0} # 0 bad, 1 same, 2 changes
state = [0] * (n + 1)
state[0] = 1
good = 0
for i in range(1, n + 1):
prefix, last = state[i // 10], kind[i % 10]
if prefix == 0 or last == 0:
state[i] = 0 # contains 3, 4 or 7
else:
state[i] = max(prefix, last) # changed if any digit changes
if state[i] == 2:
good += 1
return good
What the code is doing, line by line:
i // 10isiwithout its last digit, already computed.- Using 0 < 1 < 2 for bad < same < changed makes
maxthe right combination once both parts are valid.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n < 2→ 0.- Numbers made only of 0/1/8 (valid but unchanged).
- Any 3/4/7 digit.
Complexity: O(n) time and space.
Medium 790. Domino and Tromino Tiling Google Amazon
Problem: Count tilings of a 2 × n board with dominoes and L-trominoes (mod 10⁹ + 7).
n = 35n = 11Constraints: 1 ≤ n ≤ 1000
Hint 1
Besides a flat edge, what other edge shapes can a partial tiling have?
Hint 2
Track full coverage and "one cell sticking out"; write transitions between them.
From brute force to the pattern: Enumerating tilings is exponential; two states per column make it linear.
Spot it: Tile column by column. Besides a flat edge, a tromino can leave one cell sticking out, so track two states: full[i] (first i columns covered) and half[i] (first i columns plus one cell of column i + 1; top and bottom are mirror images, so count one and double it) → DP with profile states.
Why not another pattern? Counting with dominoes only (Fibonacci) misses tromino pairs that span several columns.
Approach:
full[0] = full[1] = 1,half[0] = 0,half[1] = 1.full[i] = full[i−1](vertical domino)+ full[i−2](two horizontal dominoes)+ 2 · half[i−2](a tromino fills the gap and columni).half[i] = full[i−1](a tromino covers columniand sticks out)+ half[i−1](a horizontal domino fills the gap and sticks out).
class Solution:
def numTilings(self, n: int) -> int:
MOD = 10**9 + 7
full = [0] * (n + 1) # columns 1..i fully covered
half = [0] * (n + 1) # columns 1..i covered plus one cell of column i+1 (one orientation)
full[0] = 1
full[1] = 1
half[1] = 1 # a tromino covering column 1 and one cell of column 2
for i in range(2, n + 1):
full[i] = (full[i - 1] + full[i - 2] + 2 * half[i - 2]) % MOD # domino, two dominoes, or a closing tromino
half[i] = (full[i - 1] + half[i - 1]) % MOD # open with a tromino, or extend with a domino
return full[n]
What the code is doing, line by line:
- The jagged edge has two mirror images (top or bottom cell sticking out);
halfcounts one and the factor 2 covers both. - From a jagged state ending at column
i − 2with one cell in columni − 1, a tromino covers the other cell ofi − 1and all of columni. - A jagged state is created by a tromino on a flat edge, or carried forward by a horizontal domino in the empty row.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 1→ 1.n = 2→ 2.n = 3→ 5 (includes tromino pairs).
Complexity: O(n) time (O(1) space if only the last values are kept).
Medium 799. Champagne Tower Google
Problem: Pour poured cups on the top glass of a pyramid; overflow splits equally to the two glasses below. How full is glass (query_row, query_glass)?
poured = 1, query_row = 1, query_glass = 10.00000poured = 2, query_row = 1, query_glass = 10.50000Constraints: 0 ≤ poured ≤ 10⁹ · 0 ≤ query_glass ≤ query_row < 100
Hint 1
Track how much liquid flows into each glass, not just how full it is.
Hint 2
Each glass passes (flow − 1) / 2 to each of its two children.
From brute force to the pattern: Pouring one cup at a time and simulating is slower and unnecessary. Row DP is O(rows²).
Spot it: Flow only goes down, so each row depends on the row above → row-by-row DP of how much liquid passes through each glass.
Why not another pattern? Simulating cup by cup over time is unnecessary; only the total flow through each glass matters.
Approach:
row = [poured].- For each next row: every glass with
x > 1sends(x − 1) / 2to each child. - Return
min(1, row[query_glass]).
class Solution:
def champagneTower(self, poured: int, query_row: int, query_glass: int) -> float:
row = [float(poured)] # total liquid that reaches each glass of the current row
for r in range(query_row):
nxt = [0.0] * (r + 2)
for i, x in enumerate(row):
extra = max(0.0, x - 1) / 2 # the glass keeps 1 cup; the rest splits
nxt[i] += extra
nxt[i + 1] += extra
row = nxt
return min(1.0, row[query_glass])
What the code is doing, line by line:
- The value stored is the total that flows into a glass, which may exceed 1.
- Only the excess over 1 is shared, half to each child.
- The answer is capped at 1, a full glass.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
poured = 0.- A glass at the edge of a row (one parent).
- Huge
poured(cap at 1).
Complexity: O(query_row²) time, O(query_row) space.
Medium 877. Stone Game Google Meta Microsoft
Problem: Even number of piles, odd total; players take a pile from either end. Does Alice (first) win with optimal play?
piles = [5,3,4,5]truepiles = [3,7,2,3]trueConstraints: 2 ≤ piles.length ≤ 500 · piles.length is even. · 1 ≤ piles[i] ≤ 500 · sum(piles[i]) is odd.
Hint 1
Think of the score difference between the player to move and the other.
Hint 2
Bonus: with an even number of piles, can Alice force taking all even-indexed piles?
From brute force to the pattern: The full game tree is O(2ⁿ); interval DP is O(n²).
Spot it: Same game as Predict the Winner → interval DP on the score difference. (There's also a proof: Alice can choose to take all even-indexed or all odd-indexed piles, and one of those sums is larger, so she always wins.)
Why not another pattern? Greedy (take the larger end) isn't optimal; return True is correct but the DP shows why.
Approach:
diff[i][j]= best (mover − other) on pilesi..j.diff[i][j] = max(piles[i] − diff[i+1][j], piles[j] − diff[i][j−1]).- Return
diff[0][n−1] > 0.
class Solution:
def stoneGame(self, piles: List[int]) -> bool:
n = len(piles)
diff = [[0] * n for _ in range(n)] # mover's lead on piles[i..j]
for i in range(n - 1, -1, -1):
diff[i][i] = piles[i]
for j in range(i + 1, n):
diff[i][j] = max(piles[i] - diff[i + 1][j], piles[j] - diff[i][j - 1]) # take an end
return diff[0][n - 1] > 0
What the code is doing, line by line:
- Each choice gives the opponent the remaining interval, where they get their own best lead.
- The total is odd, so there are no ties; a positive lead means Alice wins.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Two piles.
- Large middle pile.
- Alternating big and small piles.
Complexity: O(n²) time and space.
Medium 918. Maximum Sum Circular Subarray Google Meta Amazon Microsoft
Problem: Return the maximum sum of a non-empty subarray of a circular array.
nums = [1,-2,3,-2]3nums = [5,-3,5]10Constraints: n == nums.length · 1 ≤ n ≤ 3 * 10⁴ · -3 * 10⁴ ≤ nums[i] ≤ 3 * 10⁴
Hint 1
A subarray that wraps around is the complement of a subarray in the middle.
Hint 2
Run Kadane for both the maximum and the minimum subarray.
From brute force to the pattern: Kadane from every starting point is O(n²); the complement trick is O(n).
Spot it: A wrapping subarray is the whole array minus a middle subarray, so its best sum is total − (minimum subarray). Run Kadane's DP for both the max and the min.
Why not another pattern? Trying every start point with Kadane is O(n²).
Approach:
- Kadane for the maximum subarray and for the minimum subarray, in one pass.
- If the max is negative (all numbers negative), return it.
- Otherwise return
max(max_sub, total − min_sub).
class Solution:
def maxSubarraySumCircular(self, nums: List[int]) -> int:
cur_max = cur_min = 0
best_max = float("-inf")
best_min = float("inf")
for x in nums:
cur_max = max(cur_max + x, x) # best subarray ending here
cur_min = min(cur_min + x, x) # worst subarray ending here
best_max = max(best_max, cur_max)
best_min = min(best_min, cur_min)
if best_max < 0:
return best_max # all negative: removing the min would leave nothing
return max(best_max, sum(nums) - best_min) # normal, or wrapping around
What the code is doing, line by line:
- A wrapping subarray leaves out one contiguous middle part; leaving out the smallest one gives the biggest wrap.
- If every number is negative,
total − minwould be the empty subarray (0), which isn't allowed.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All negative.
- All positive (the whole array).
- One element.
Complexity: O(n) time, O(1) space.
Medium 931. Minimum Falling Path Sum Google Amazon
Problem: A falling path takes one cell per row, moving to the same or an adjacent column. Return the minimum path sum.
matrix = [[2,1,3],[6,5,4],[7,8,9]]13matrix = [[-19,57],[-40,-5]]-59Constraints: n == matrix.length == matrix[i].length · 1 ≤ n ≤ 100 · -100 ≤ matrix[i][j] ≤ 100
Hint 1
Where can the path to a cell come from?
Hint 2
Keep the best sums of the previous row.
From brute force to the pattern: All 3ⁿ paths vs an O(n²) row DP.
Spot it: The best path to a cell comes from one of the three cells above → row-by-row DP.
Why not another pattern? Trying all paths is O(3ⁿ).
Approach:
prev= first row.- Each next row:
cur[c] = matrix[r][c] + min(prev[c − 1], prev[c], prev[c + 1])(within bounds). - Answer:
min(prev)after the last row.
class Solution:
def minFallingPathSum(self, matrix: List[List[int]]) -> int:
n = len(matrix)
prev = matrix[0][:] # best path sum ending at each cell of the previous row
for r in range(1, n):
cur = [0] * n
for c in range(n):
above = prev[max(0, c - 1):c + 2] # up-left, up, up-right
cur[c] = matrix[r][c] + min(above)
prev = cur
return min(prev)
What the code is doing, line by line:
- The slice
prev[max(0, c − 1):c + 2]handles both edges. - Only the previous row is needed.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- 1×1 matrix.
- Negative values.
- Edge columns (two parents).
Complexity: O(n²) time, O(n) space.
Medium 983. Minimum Cost For Tickets Google Amazon
Problem: Travel days are given; passes cost costs[0] (1 day), costs[1] (7 days), costs[2] (30 days). Return the minimum cost.
days = [1,4,6,7,8,20], costs = [2,7,15]11days = [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15]17Constraints: 1 ≤ days.length ≤ 365 · 1 ≤ days[i] ≤ 365 · days is in strictly increasing order. · costs.length == 3 · 1 ≤ costs[i] ≤ 1000
Hint 1
Think about the pass that covers the last travel day.
Hint 2
dp[d] = minimum cost to cover all travel days up to day d.
From brute force to the pattern: Trying every combination of passes is exponential; the day DP is O(365).
Spot it: The cheapest cover for days 1..d ends with a pass that covers day d → 1D DP over calendar days.
Why not another pattern? Greedy (buy a 7-day pass when many trips are close) misjudges overlaps; the DP checks all three choices.
Approach:
cost[0] = 0.- Day
dnot a travel day:cost[d] = cost[d − 1]. - Otherwise:
min(cost[d−1] + c1, cost[max(0, d−7)] + c7, cost[max(0, d−30)] + c30).
class Solution:
def mincostTickets(self, days: List[int], costs: List[int]) -> int:
travel = set(days)
last = days[-1]
cost = [0] * (last + 1) # cost[d]: cheapest way to cover travel days up to d
for d in range(1, last + 1):
if d not in travel:
cost[d] = cost[d - 1] # nothing to cover today
continue
cost[d] = min(cost[d - 1] + costs[0], # 1-day pass for today
cost[max(0, d - 7)] + costs[1], # 7-day pass ending today
cost[max(0, d - 30)] + costs[2]) # 30-day pass ending today
return cost[last]
What the code is doing, line by line:
- A pass bought to cover day
dcan be assumed to end on dayd, covering the days before it. - Non-travel days cost nothing extra.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A 7-day pass cheaper than a 1-day pass.
- One travel day.
- Travel every day for a month.
Complexity: O(last day) time and space (≤ 365).
Medium 1043. Partition Array for Maximum Sum Google
Problem: Split the array into blocks of length ≤ k; each block becomes its maximum. Return the largest possible sum.
arr = [1,15,7,9,2,5,10], k = 384arr = [1,4,1,5,7,3,6,1,9,9,3], k = 483Constraints: 1 ≤ arr.length ≤ 500 · 0 ≤ arr[i] ≤ 10⁹ · 1 ≤ k ≤ arr.length
Hint 1
Consider the last block of the partition.
Hint 2
dp[i] = best for the first i elements; try every last-block length.
From brute force to the pattern: All partitions: O(2ⁿ). DP: O(nk).
Spot it: The last block of the prefix arr[:i] has length 1..k → 1D DP over prefixes, tracking the block max while extending it leftwards.
Why not another pattern? Greedy blocks around big values can be wrong; all splits are O(2ⁿ).
Approach:
best[0] = 0.- For
i = 1..n: forL = 1..min(k, i):top = max(top, arr[i − L]);best[i] = max(best[i], best[i − L] + top · L).
class Solution:
def maxSumAfterPartitioning(self, arr: List[int], k: int) -> int:
n = len(arr)
best = [0] * (n + 1) # best[i]: answer for arr[:i]
for i in range(1, n + 1):
top = 0
for L in range(1, min(k, i) + 1): # the last block is arr[i-L:i]
top = max(top, arr[i - L])
best[i] = max(best[i], best[i - L] + top * L)
return best[n]
What the code is doing, line by line:
- Growing the last block one element to the left updates its maximum in
O(1). - Every block of the last partition is considered, so the best is found.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
k = 1→ the sum.k = n→n · max.- One element.
Complexity: O(n · k) time, O(n) space.
Medium 1048. Longest String Chain Google
Problem: A word is a predecessor of another if inserting one letter makes it. Return the longest chain length.
words = ["a","b","ba","bca","bda","bdca"]4words = ["xbc","pcxbcf","xb","cxbc","pcxbc"]5Constraints: 1 ≤ words.length ≤ 1000 · 1 ≤ words[i].length ≤ 16 · words[i] only consists of lowercase English letters.
Hint 1
Process words from shortest to longest.
Hint 2
Generate predecessors by deleting one letter and look them up.
From brute force to the pattern: Comparing every pair of words is O(n² · L); generating deletions is O(n · L²).
Spot it: Chains grow by one letter, so process words by length. A word's best chain is 1 + the best among the words obtained by deleting one letter → DP with a hash map.
Why not another pattern? Checking every pair of words for the predecessor relation is O(n² · L).
Approach:
- Sort words by length.
- For each word, try deleting each position;
chain[word] = max(chain.get(shorter, 0) + 1). - Return the max chain.
class Solution:
def longestStrChain(self, words: List[str]) -> int:
chain = {} # word -> longest chain ending with it
for w in sorted(words, key=len): # shorter words first
best = 1
for i in range(len(w)):
shorter = w[:i] + w[i + 1:] # delete one letter
if shorter in chain:
best = max(best, chain[shorter] + 1)
chain[w] = best
return max(chain.values())
What the code is doing, line by line:
- Deleting letters from the longer word generates its only possible predecessors (
Lof them). - Sorting by length guarantees predecessors are already processed.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Duplicate words.
- No chains → 1.
- Several chains of the same length.
Complexity: O(n · L²) time (L deletions, each O(L)), O(n · L) space.
Medium 1049. Last Stone Weight II Google
Problem: Smashing stones x ≤ y leaves y − x. Return the smallest possible final weight.
stones = [2,7,4,1,8,1]1stones = [31,26,33,21,40]5Constraints: 1 ≤ stones.length ≤ 30 · 1 ≤ stones[i] ≤ 100
Hint 1
What final values are possible? Think of assigning a + or − sign to each stone.
Hint 2
Find a subset whose sum is as close as possible to half the total.
From brute force to the pattern: All sign assignments: O(2ⁿ). Subset sums: O(n · total).
Spot it: Every final result is |sum(A) − sum(B)| for some split into two groups (signs + and −). Minimise it by finding the subset sum closest to total / 2 → subset-sum DP.
Why not another pattern? Simulating smashes greedily (largest two) is the easier problem 1046 and isn't optimal here.
Approach:
sums = {0}; for each stone, adds + stonefor every reachables ≤ total / 2.- Answer:
total − 2 · max(sums).
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
total = sum(stones)
sums = {0} # subset sums reachable so far (only up to half the total)
for x in stones:
sums |= {s + x for s in sums if s + x <= total // 2}
return total - 2 * max(sums) # the two groups as even as possible
What the code is doing, line by line:
- Each smash subtracts one group from another, so the end weight is a signed sum of the stones.
- The best split has one group as close to
total / 2as possible from below.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- One stone → its weight.
- Two equal stones → 0.
- Total is odd (answer ≥ 1).
Complexity: O(n · total) time, O(total) space.
Medium 1140. Stone Game II Google Amazon Microsoft
Problem: Players take the first X piles (1 ≤ X ≤ 2M), then M = max(M, X). Return the most stones Alice gets with optimal play.
piles = [2,7,9,4,4]10piles = [1,2,3,4,5,100]104Constraints: 1 ≤ piles.length ≤ 100 · 1 ≤ piles[i] ≤ 10⁴
Hint 1
The state is the starting pile and the current M.
Hint 2
The mover gets the remaining total minus what the opponent can secure afterwards.
From brute force to the pattern: The game tree is exponential; memoising (i, M) makes it O(n³).
Spot it: A game with state (start index, M) → memoised game DP. Whatever the mover doesn't get from the remaining suffix, the opponent gets; so the mover maximises suffix[i] − opponent's best.
Why not another pattern? Greedy (take as many as allowed) gives the opponent a bigger M.
Approach:
suffix[i]= stones from pileito the end.best(i, M): ifi + 2M ≥ n, take everything:suffix[i].- Else
suffix[i] − min(best(i + X, max(M, X)))forX = 1..2M.
from functools import lru_cache
class Solution:
def stoneGameII(self, piles: List[int]) -> int:
n = len(piles)
suffix = [0] * (n + 1)
for i in range(n - 1, -1, -1):
suffix[i] = suffix[i + 1] + piles[i] # stones left from i on
@lru_cache(maxsize=None)
def best(i, M): # most stones the player to move gets from piles[i:]
if i + 2 * M >= n:
return suffix[i] # can take everything
return suffix[i] - min(best(i + X, max(M, X)) for X in range(1, 2 * M + 1)) # leave the opponent the least
return best(0, 1)
What the code is doing, line by line:
- Both players share the remaining stones, so the mover gets
suffix[i] −what the opponent gets next. - Minimising the opponent's best is the same as maximising your own.
- Taking everything when allowed is always optimal (stones are positive).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Few piles (take all at once).
- Large first pile.
n = 1.
Complexity: O(n³) time (n² states × O(n) choices), O(n²) space.
Medium 1277. Count Square Submatrices with All Ones Google Amazon
Problem: Count all square submatrices that contain only 1s.
matrix =
[
[0,1,1,1],
[1,1,1,1],
[0,1,1,1]
]15matrix =
[
[1,0,1],
[1,1,0],
[1,1,0]
]7Constraints: 1 ≤ arr.length ≤ 300 · 1 ≤ arr[0].length ≤ 300 · 0 ≤ arr[i][j] ≤ 1
Hint 1
How many squares have their bottom-right corner at a given cell?
Hint 2
Reuse the Maximal Square DP.
From brute force to the pattern: Checking every square from every corner is slow; the DP counts them in O(mn).
Spot it: Same DP as Maximal Square: side[r][c] is the largest all-ones square with bottom-right corner (r, c). Such a cell is the corner of exactly side[r][c] squares (sizes 1..side), so add them all up.
Why not another pattern? Checking every square separately is O(m · n · min(m,n)²).
Approach:
- For each
1cell (not in the first row/column):side = 1 + min(up, left, up-left). - Add
sideto the total.
class Solution:
def countSquares(self, matrix: List[List[int]]) -> int:
m, n = len(matrix), len(matrix[0])
side = [row[:] for row in matrix] # largest all-ones square ending at (r, c)
total = 0
for r in range(m):
for c in range(n):
if side[r][c] and r and c:
side[r][c] = 1 + min(side[r - 1][c], side[r][c - 1], side[r - 1][c - 1])
total += side[r][c] # squares of size 1..side end here
return total
What the code is doing, line by line:
- First row and column cells can only be 1×1 squares, so they keep their value.
- A corner with largest square
salso ends squares of every smaller size.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All zeros → 0.
- All ones.
- One row.
Complexity: O(m · n) time and space.
Medium 1749. Maximum Absolute Sum of Any Subarray Google
Problem: Return the maximum absolute value of the sum of any (possibly empty) subarray.
nums = [1,-3,2,3,-4]5nums = [2,-5,1,-4,3,-2]8Constraints: 1 ≤ nums.length ≤ 10⁵ · -10⁴ ≤ nums[i] ≤ 10⁴
Hint 1
The answer is either the maximum subarray sum or minus the minimum subarray sum.
Hint 2
Run Kadane's algorithm for both.
From brute force to the pattern: All subarrays are O(n²); two Kadane passes are O(n).
Spot it: The largest absolute sum is either the largest sum or the most negative sum → Kadane's DP for both.
Why not another pattern? All subarrays: O(n²). (Equivalently max prefix sum − min prefix sum.)
Approach:
- Track the max and min subarray sums ending here (allowing restart at 0).
- Answer:
max(best_max, −best_min).
class Solution:
def maxAbsoluteSum(self, nums: List[int]) -> int:
hi = lo = 0 # largest / smallest sum of a subarray ending here (empty allowed)
best = 0
for x in nums:
hi = max(0, hi + x)
lo = min(0, lo + x)
best = max(best, hi, -lo) # either extreme can have the biggest absolute value
return best
What the code is doing, line by line:
- Empty subarrays are allowed, so both running sums can reset to 0.
- The absolute value is maximised by one of the two extremes.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All negative.
- All positive.
- Alternating signs.
Complexity: O(n) time, O(1) space.
Medium 1884. Egg Drop With 2 Eggs and N Floors Google
Problem: With two eggs and n floors, return the minimum number of drops that always finds the critical floor.
n = 22n = 10014Constraints: 1 ≤ n ≤ 1000
Hint 1
If the first egg breaks at floor x, how many drops does the second egg need?
Hint 2
Turn it around: with m drops, how many floors can you handle?
From brute force to the pattern: The DP min over x of 1 + max(x − 1, f(n − x)) is O(n²); the flipped view is O(√n).
Spot it: The classic DP is moves(eggs, floors). Flipping it is simpler: with m moves and 2 eggs, how many floors can you handle? Drop the first egg at floor m: if it breaks, the second egg checks the m − 1 floors below one by one; if not, m − 1 moves remain for the floors above. So floors[m] = m + floors[m − 1].
Why not another pattern? Binary search fails with only two eggs: after the first egg breaks, you must scan linearly.
Approach:
covered = 0,m = 0.- While
covered < n:m += 1,covered += m. - Return
m.
class Solution:
def twoEggDrop(self, n: int) -> int:
moves = 0
covered = 0 # floors we can fully resolve with this many moves
while covered < n:
moves += 1
covered += moves # first drop at +moves: below it the 2nd egg scans moves - 1 floors
return moves
What the code is doing, line by line:
- With
mmoves, the first drop goes at floorm; a break leavesm − 1drops for them − 1floors below. - No break: the problem repeats above with
m − 1moves, which coverfloors[m − 1]more floors. - So
floors[m] = 1 + 2 + … + m = m(m + 1)/2; the answer is the firstmwith that ≥n.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 1→ 1.n = 100→ 14.nexactly triangular.
Complexity: O(√n) time, O(1) space.
Medium 2140. Solving Questions With Brainpower Google
Problem: Solving question i earns points but skips the next brainpower questions. Return the maximum points.
questions = [[3,2],[4,3],[4,4],[2,5]]5questions = [[1,1],[2,2],[3,3],[4,4],[5,5]]7Constraints: 1 ≤ questions.length ≤ 10⁵ · questions[i].length == 2 · 1 ≤ pointsi, brainpoweri ≤ 10⁵
Hint 1
Decide for question i: solve or skip. What's left in each case?
Hint 2
Compute from the last question backwards.
From brute force to the pattern: Trying every subset is exponential; the backward DP is O(n).
Spot it: A choice at i jumps to a later index → DP from the end (like House Robber with variable skips).
Why not another pattern? Greedy by points or by ratio fails; forward DP needs "pushing" to future indices, while backward DP just reads them.
Approach:
best[n] = 0(and beyondncounts as 0).best[i] = max(best[i + 1], points[i] + best[min(n, i + brainpower[i] + 1)]).
class Solution:
def mostPoints(self, questions: List[List[int]]) -> int:
n = len(questions)
best = [0] * (n + 1) # best[i]: max points from questions i..n-1
for i in range(n - 1, -1, -1):
points, skip = questions[i]
solve = points + best[min(n, i + skip + 1)] # solve i, jump past the skipped ones
best[i] = max(best[i + 1], solve) # or skip i
return best[0]
What the code is doing, line by line:
- Each question is either skipped or solved; solving fixes the next index you can use.
- Going backwards, both options look up already computed values.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Brainpower jumping past the end.
- One question.
- Skipping a high-points question to solve two others.
Complexity: O(n) time and space.
Medium 2400. Number of Ways to Reach a Position After Exactly k Steps Google
Problem: Starting at startPos, take exactly k steps of ±1. Count the step sequences ending at endPos (mod 10⁹ + 7).
startPos = 1, endPos = 2, k = 33startPos = 2, endPos = 5, k = 100Constraints: 1 ≤ startPos, endPos, k ≤ 1000
Hint 1
How many right steps and left steps must there be?
Hint 2
Then count the arrangements: a binomial coefficient.
From brute force to the pattern: A DP over (steps, position) is O(k²); all sequences are 2ᵏ. The formula is O(k).
Spot it: The DP ways[step][pos] works (O(k²)), but it collapses to a formula: with R right steps and k − R left steps, R − (k − R) = d, so R = (k + d) / 2, and any choice of which steps go right works: C(k, R).
Why not another pattern? Enumerating all 2ᵏ sequences is impossible for k = 1000.
Approach:
d = |endPos − startPos|.- If
d > kork − dis odd → 0. - Return
C(k, (k + d) / 2) mod 10⁹ + 7.
from math import comb
class Solution:
def numberOfWays(self, startPos: int, endPos: int, k: int) -> int:
d = abs(endPos - startPos)
if d > k or (k - d) % 2:
return 0 # too far, or wrong parity
right = (k + d) // 2 # steps toward the target
return comb(k, right) % (10**9 + 7) # choose which steps they are
What the code is doing, line by line:
- Every step is +1 or −1, so the net displacement fixes how many steps go each way.
- The order is free: any
right-sized subset of theksteps works. - Python's
combis exact; reduce modulo at the end.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Wrong parity → 0.
- Too far → 0.
startPos == endPos.
Complexity: O(k) time for the binomial, O(1) space.
Medium 3573. Best Time to Buy and Sell Stock V Google
Problem: At most k transactions, each a normal trade (buy then sell) or a short sale (sell then buy back). Return the maximum profit.
prices = [1,7,9,8,2], k = 214prices = [12,16,19,19,8,1,19,13,9], k = 336Constraints: 2 ≤ prices.length ≤ 10³ · 1 ≤ prices[i] ≤ 10⁹ · 1 ≤ k ≤ prices.length / 2
Hint 1
Extend the Stock IV state machine.
Hint 2
Inside a transaction you're either long or short; keep both states.
From brute force to the pattern: Choosing every set of trade days and directions is exponential; three states per count make it O(nk).
Spot it: Stock IV's state machine with one more state: after opening the j-th transaction you're either long (bought) or short (sold first) → DP with free[j], long[j], short[j] per day.
Why not another pattern? Handling normal and short trades separately misses the best mix of both.
Approach:
free[0] = 0; everything else −∞.- Each day
p(using yesterday's values): open longfree[j−1] − p, open shortfree[j−1] + p; close longlong[j] + p, close shortshort[j] − p. - Answer:
max(free).
class Solution:
def maximumProfit(self, prices: List[int], k: int) -> int:
NEG = float("-inf")
free = [0] + [NEG] * k # free[j]: j transactions finished, holding nothing
long = [NEG] * (k + 1) # long[j]: inside transaction j, bought
short = [NEG] * (k + 1) # short[j]: inside transaction j, sold first
for p in prices:
new_free, new_long, new_short = free[:], long[:], short[:]
for j in range(1, k + 1):
new_long[j] = max(long[j], free[j - 1] - p) # buy to open
new_short[j] = max(short[j], free[j - 1] + p) # sell short to open
new_free[j] = max(free[j], long[j] + p, short[j] - p) # close either kind
free, long, short = new_free, new_long, new_short
return max(free)
What the code is doing, line by line:
- Using yesterday's values stops a transaction from closing and the next one opening on the same day.
- A short trade earns
sell price − buy-back price, mirroring the long trade. max(free)allows using fewer thanktransactions.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Falling prices (short selling profits).
k = 1.- A peak then a valley (long then short).
Complexity: O(n · k) time, O(k) space.
Medium 3592. Inverse Coin Change Google
Problem: numWays[i] is the number of ways to make amount i (1-indexed) with unknown coins ≤ n. Recover the coins or return [].
numWays = [0,1,0,2,0,3,0,4,0,5][2,4,6]numWays = [1,2,3,4,15][]Constraints: 1 ≤ numWays.length ≤ 100 · 0 ≤ numWays[i] ≤ 2 * 10⁸
Hint 1
Process amounts in increasing order. Which coins can affect the count for amount i?
Hint 2
If i is a coin, how much does it add to the number of ways for amount i itself?
From brute force to the pattern: Testing every subset of 1..n as denominations is O(2ⁿ · n²). The forward rebuild is O(n²).
Spot it: Run Coin Change II (unbounded knapsack) forwards with the coins found so far. Coin i affects amounts ≥ i only, and as a coin it adds exactly one way to amount i (the coin alone). So at amount i, the computed count must equal numWays[i] (not a coin) or be one less (it is a coin).
Why not another pattern? Trying subsets of denominations is 2ⁿ.
Approach:
ways = [1, 0, …]for the coins chosen so far.- For each amount
i: ifnumWays[i] == ways[i], not a coin; if== ways[i] + 1, add coiniand updatewaysfor amounts ≥i; otherwise return[].
class Solution:
def findCoins(self, numWays: List[int]) -> List[int]:
n = len(numWays)
ways = [1] + [0] * n # ways to make each amount with the coins found so far
coins = []
for i in range(1, n + 1):
if numWays[i - 1] == ways[i]:
continue # already explained without coin i
if numWays[i - 1] != ways[i] + 1:
return [] # no set of coins can produce this count
coins.append(i) # coin i adds exactly one way to amount i
for amount in range(i, n + 1):
ways[amount] += ways[amount - i] # unbounded knapsack update
return coins
What the code is doing, line by line:
- Amount
ican only be influenced by coins ≤i; coins belowiare already decided. - Adding coin
iaddsways[0] = 1toways[i], so the count must be exactly one more. - Any other difference proves no denomination set fits.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Counts that can't be produced →
[]. - All zeros → no coins.
- Coin 1 present (every amount ≥ 1 way).
Complexity: O(n²) time, O(n) space.
Medium 3660. Jump Game IX Google Amazon
Problem: Jump right only to smaller values, left only to larger ones. For each start, return the largest value reachable.
nums = [2,1,3][2,2,3]nums = [2,3,1][3,3,3]Constraints: 1 ≤ nums.length ≤ 10⁵ · 1 ≤ nums[i] ≤ 10⁹
Hint 1
From index i, can you always reach the maximum of nums[0..i]?
Hint 2
When can you cross from the prefix to the suffix? Compare the prefix max with the suffix min.
From brute force to the pattern: BFS/DFS from each index is O(n²); prefix max + suffix min is O(n).
Spot it: From i you can reach the maximum of nums[0..i] (jump left to larger values step by step). If that prefix max is larger than some value to the right (the suffix minimum after i), you can cross over and everything reachable from i + 1 becomes reachable → DP from the right with prefix max and suffix min.
Why not another pattern? BFS from every index is O(n²).
Approach:
pre[i]= max ofnums[0..i];suf[i]= min ofnums[i..n−1].ans[n−1] = pre[n−1].- From the right:
ans[i] = ans[i + 1]ifpre[i] > suf[i + 1], elsepre[i].
class Solution:
def maxValue(self, nums: List[int]) -> List[int]:
n = len(nums)
pre = nums[:] # pre[i]: max of nums[0..i]
for i in range(1, n):
pre[i] = max(pre[i - 1], nums[i])
suf = nums[:] # suf[i]: min of nums[i..]
for i in range(n - 2, -1, -1):
suf[i] = min(suf[i + 1], nums[i])
ans = [0] * n
ans[n - 1] = pre[n - 1]
for i in range(n - 2, -1, -1):
if pre[i] > suf[i + 1]:
ans[i] = ans[i + 1] # can cross to the right part: same reach
else:
ans[i] = pre[i] # the left part is closed off
return ans
What the code is doing, line by line:
- Within the prefix, the maximum is reachable: from any index, jump left to a larger value until you reach it.
- If every value on the right is ≥ the prefix max, no jump can cross the boundary in either direction.
- Otherwise the two sides are connected, so
ireaches whateveri + 1reaches.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Increasing array (each index isolated).
- Decreasing array (all connected).
- One element.
Complexity: O(n) time and space.
Medium 3738. Longest Non-Decreasing Subarray After Replacing at Most One Element Google
Problem: Replace at most one element with any value; return the longest non-decreasing subarray possible.
nums = [1,2,3,1,2]4nums = [2,2,2,2,2]5Constraints: 1 ≤ nums.length ≤ 10⁵ · -10⁹ ≤ nums[i] ≤ 10⁹
Hint 1
Precompute the non-decreasing run ending at each index and starting at each index.
Hint 2
If you replace element i, when can the runs on both sides be joined?
From brute force to the pattern: Trying each replacement and rescanning is O(n²); precomputed runs make it O(n).
Spot it: Precompute run lengths in both directions (a simple DP). Replacing nums[i] can join the run ending at i − 1 with the run starting at i + 1 if nums[i − 1] ≤ nums[i + 1]; otherwise it extends just one of them by one.
Why not another pattern? Trying every replacement and rescanning is O(n²).
Approach:
left[i]: length of the non-decreasing run ending ati;right[i]: starting ati.- For each
i:L = left[i − 1](0 if none),R = right[i + 1](0 if none). - If both exist and
nums[i − 1] ≤ nums[i + 1]: candidateL + 1 + R; elsemax(L, R) + 1.
class Solution:
def longestSubarray(self, nums: List[int]) -> int:
n = len(nums)
left = [1] * n # non-decreasing run ending at i
for i in range(1, n):
if nums[i - 1] <= nums[i]:
left[i] = left[i - 1] + 1
right = [1] * n # non-decreasing run starting at i
for i in range(n - 2, -1, -1):
if nums[i] <= nums[i + 1]:
right[i] = right[i + 1] + 1
best = max(left) # no replacement
for i in range(n): # replace nums[i]
L = left[i - 1] if i > 0 else 0
R = right[i + 1] if i < n - 1 else 0
if L and R and nums[i - 1] <= nums[i + 1]:
best = max(best, L + 1 + R) # new value fits between: glue both runs
else:
best = max(best, max(L, R) + 1) # extend one run by the replaced cell
return best
What the code is doing, line by line:
- The replaced element can take any value, so it fits after the left run or before the right run.
- It bridges both runs only when the left run's last value ≤ the right run's first value.
- Without bridging, it still adds one cell to the longer neighbouring run.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already non-decreasing → n.
- One element → 1.
- Strictly decreasing → 2.
Complexity: O(n) time and space.
Medium 3742. Maximum Path Score in a Grid Google Microsoft
Problem: Move right/down from top-left to bottom-right. Cells 1 and 2 cost 1 and score 1 or 2. Maximise the score with total cost ≤ k, or −1.
grid = [[0, 1],[2, 0]], k = 12grid = [[0, 1],[1, 2]], k = 1-1Constraints: 1 ≤ m, n ≤ 200 · 0 ≤ k ≤ 10³ · grid[0][0] == 0 · 0 ≤ grid[i][j] ≤ 2
Hint 1
A normal grid DP keeps one number per cell. What extra information do you need with a cost limit?
Hint 2
Keep, for every cell and every cost, the best score.
From brute force to the pattern: Trying all paths is exponential; the cost-indexed DP is polynomial.
Spot it: Grid paths → 2D DP, but with a budget: add the cost spent as a third dimension, storing the best score for each cost (a knapsack on a grid). A path has at most m + n − 1 cells, so the cost dimension is small.
Why not another pattern? Maximising score alone ignores the budget; trying all paths is exponential.
Approach:
K = min(k, m + n);best[c][spent]for the current row (−∞ = impossible).- For each cell, combine the states from above and from the left, then shift by the cell's cost and add its score.
- Answer: max over
spent ≤ Kat the last cell, or −1.
class Solution:
def maxPathScore(self, grid: List[List[int]], k: int) -> int:
m, n = len(grid), len(grid[0])
K = min(k, m + n) # a path never costs more than its length
NEG = float("-inf")
prev = [[NEG] * (K + 1) for _ in range(n)] # best score per cost, previous row
for r in range(m):
cur = [[NEG] * (K + 1) for _ in range(n)]
for c in range(n):
if r == 0 and c == 0:
came = [0] + [NEG] * K # start: nothing spent
else:
up = prev[c] if r else [NEG] * (K + 1)
left = cur[c - 1] if c else [NEG] * (K + 1)
came = [max(a, b) for a, b in zip(up, left)] # best way in, per cost
cost = 1 if grid[r][c] else 0
gain = grid[r][c]
cur[c] = [NEG] * cost + [s + gain for s in came[:K + 1 - cost]] # pay this cell's cost
prev = cur
best = max(prev[n - 1])
return best if best != NEG else -1
What the code is doing, line by line:
- The extra dimension is the cost spent so far; each cell keeps the best score for every possible cost.
- A cell with cost 1 shifts the whole list by one position (costs above
Kare dropped). - Capping
Katm + nkeeps the lists short whenkis large.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No path within budget → −1.
k = 0(only zero cells allowed).- Large
k(no limit in practice).
Complexity: O(m · n · min(k, m + n)) time, O(n · K) space.
Medium 3751. Total Waviness of Numbers in Range I Google Amazon
Problem: Sum, over every number in [num1, num2], the count of inner digits that are strictly above or below both neighbours.
num1 = 120, num2 = 1303num1 = 198, num2 = 2023Constraints: 1 ≤ num1 ≤ num2 ≤ 10⁵
Hint 1
The range is at most 10⁵ numbers.
Hint 2
Check every inner digit against both neighbours.
From brute force to the pattern: This is the brute force, and it's fast enough here; Range II needs a digit DP.
Spot it: With num2 ≤ 10⁵, a direct scan is fine: for each number, compare each inner digit with its neighbours. (Range II, up to 10¹⁵, needs a digit DP; this version is the warm-up that the DP must agree with.)
Why not another pattern? Digit DP works here too, but 10⁵ numbers × 6 digits is tiny.
Approach:
- For each
xin range, take its digits. - For each inner position
i: peak ifd[i] > d[i−1]andd[i] > d[i+1]; valley if both are<. - Add the count.
class Solution:
def totalWaviness(self, num1: int, num2: int) -> int:
total = 0
for x in range(num1, num2 + 1):
d = str(x)
for i in range(1, len(d) - 1): # inner digits only
if d[i - 1] < d[i] > d[i + 1] or d[i - 1] > d[i] < d[i + 1]:
total += 1 # a peak or a valley
return total
What the code is doing, line by line:
- Digits as characters compare the same way as their values.
- Chained comparisons check both neighbours at once.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Numbers with fewer than 3 digits → 0.
- Equal neighbours (not peaks or valleys).
num1 == num2.
Complexity: O((num2 − num1) · digits) time, O(1) space.
Medium 3976. Maximum Subarray Sum After Multiplier Google
Problem: Apply exactly one operation to exactly one subarray: multiply every element by k, or divide every element by k (rounding toward zero). Return the maximum sum of a non-empty subarray afterwards.
nums = [1,-2,3,4,-5], k = 214nums = [-5,-4,-3], k = 2-1Constraints: 1 ≤ nums.length ≤ 10⁵ · -10⁵ ≤ nums[i] ≤ 10⁵ · 1 ≤ k ≤ 10⁵
Hint 1
The summed subarray consists of up to three phases: plain, operated, plain.
Hint 2
Run Kadane with one state per phase, once for multiply and once for divide.
From brute force to the pattern: Every operated segment × Kadane is O(n³); the 3-state Kadane is O(n).
Spot it: Maximum subarray → Kadane. The operated segment splits the summed subarray into three phases: plain, transformed, plain. A 3-state Kadane handles that, run once for each operation. A sum that avoids the operation entirely is also fine, as long as the operation can go somewhere outside it.
Why not another pattern? Trying every operated segment and recomputing Kadane is O(n³) (or O(n²) with care).
Approach:
- For
fin (×k, ÷k): statesbefore(no op yet),inside(op running),after(op finished), each = best sum ending here. inside = max(before_prev, inside_prev, 0) + f(x);after = max(inside_prev, after_prev) + x;before = max(before_prev, 0) + x.- Best = max over
insideandafter; also allow a plain subarray that leaves at least one element free (nums[1:]ornums[:-1]).
class Solution:
def maxSubarraySum(self, nums: List[int], k: int) -> int:
def divide(x):
return x // k if x >= 0 else -((-x) // k) # floor for positives, ceil for negatives
def kadane(arr):
cur = best = float("-inf")
for x in arr:
cur = max(cur, 0) + x
best = max(best, cur)
return best
NEG = float("-inf")
best = NEG
for f in (lambda x: x * k, divide):
before = inside = after = NEG # best sums ending here, per phase
for x in nums:
before, inside, after = (max(before, 0) + x, # no operation yet
max(before, inside, 0) + f(x), # inside the operated part
max(inside, after) + x) # the operated part has ended
best = max(best, inside, after)
if len(nums) > 1:
best = max(best, kadane(nums[1:]), kadane(nums[:-1])) # the operation is applied elsewhere
return best
What the code is doing, line by line:
beforeis ordinary Kadane;insideadds transformed values;afteradds plain values once the operated part has ended.- The operated segment may start fresh, after a plain prefix, or continue.
- Leaving the whole operation outside the summed subarray needs at least one element outside, hence
nums[1:]/nums[:-1].
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All negative (divide shrinks them toward 0).
k = 1(no change).- One element (the operation must hit it).
Complexity: O(n) time, O(1) extra space.
Medium 4002. Count Valid Sequences Google
Problem: Count sequences of k positive integers summing to n whose product is even (mod 10⁹ + 7).
n = 5, k = 33n = 3, k = 22Constraints: 1 ≤ n ≤ 5 * 10⁵ · 1 ≤ k ≤ n
Hint 1
When is a product of integers odd?
Hint 2
Count all sequences, then subtract those where every element is odd.
Hint 3
Substitute aᵢ = 2bᵢ + 1 to count the all-odd sequences.
From brute force to the pattern: A DP over (length, sum, parity) is O(n · k); two binomials are O(n) with precomputed factorials.
Spot it: The product is even unless every number is odd. Count all sequences (stars and bars) and subtract the all-odd ones: writing aᵢ = 2bᵢ + 1 turns them into sequences of k non-negative bᵢ summing to (n − k) / 2.
Why not another pattern? A DP over (count, sum, parity) is O(n · k) = 2.5·10¹¹ in the worst case.
Approach:
- Precompute factorials mod
pup ton. all = C(n − 1, k − 1).odd = C((n − k)/2 + k − 1, k − 1)ifn − kis even, else 0.- Return
(all − odd) mod p.
class Solution:
def countValidSequences(self, n: int, k: int) -> int:
MOD = 10**9 + 7
fact = [1] * (n + 1)
for i in range(1, n + 1):
fact[i] = fact[i - 1] * i % MOD
inv = [1] * (n + 1)
inv[n] = pow(fact[n], MOD - 2, MOD) # Fermat inverse
for i in range(n, 0, -1):
inv[i - 1] = inv[i] * i % MOD
def C(a, b):
if b < 0 or b > a:
return 0
return fact[a] * inv[b] % MOD * inv[a - b] % MOD
total = C(n - 1, k - 1) # stars and bars: k positive parts
odd = C((n - k) // 2 + k - 1, k - 1) if (n - k) % 2 == 0 else 0 # every part odd
return (total - odd) % MOD
What the code is doing, line by line:
- Sequences of
kpositive integers summing toncorrespond to choosingk − 1cut points amongn − 1gaps. - All parts odd: subtract 1 from each and halve, giving
knon-negative numbers summing to(n − k)/2:C((n − k)/2 + k − 1, k − 1). - If
n − kis odd, no all-odd sequence exists.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
k = n→ all ones (odd product) → 0.k = 1→ 1 ifnis even, else 0.n − kodd (no all-odd sequences).
Complexity: O(n) time and space for the factorial tables.
Hard 32. Longest Valid Parentheses Google Meta Amazon Microsoft
Problem: Return the length of the longest well-formed parentheses substring.
s = "(()"2s = ")()())"4Constraints: 0 ≤ s.length ≤ 3 * 10⁴ · s[i] is '(', or ')'.
Hint 1
Let dp[i] be the length of the longest valid substring that ends at index i.
Hint 2
For s[i] = ')', where must its matching ( be?
From brute force to the pattern: Checking every substring is O(n³) (or O(n²) with a running balance). The DP is one pass.
Spot it: "Longest substring ending here" → DP on the end index. A valid block ending at i must end with ). It matches either s[i − 1] = '(' directly, or the ( just before the valid block ending at i − 1; then glue on any valid block before that.
Why not another pattern? Checking every substring for validity is O(n³). (A stack of indices also works in O(n); the DP shows why.)
Approach:
dp[i]= length of the longest valid substring ending exactly ati(0 ifs[i] == '(').s[i] == ')': letj = i − dp[i − 1] − 1(the position that must hold the matching().- If
j ≥ 0ands[j] == '(':dp[i] = dp[i − 1] + 2 + dp[j − 1](glue the block beforej). - Answer:
max(dp).
class Solution:
def longestValidParentheses(self, s: str) -> int:
n = len(s)
dp = [0] * n # longest valid substring ending exactly at i
for i in range(1, n):
if s[i] == ")":
j = i - dp[i - 1] - 1 # where the matching "(" must be
if j >= 0 and s[j] == "(":
dp[i] = dp[i - 1] + 2 # wrap the inner valid block
if j > 0:
dp[i] += dp[j - 1] # and join the valid block just before it
return max(dp, default=0)
What the code is doing, line by line:
- If
s[i − 1]is(, thendp[i − 1] = 0andj = i − 1: the simple()case. - Otherwise
dp[i − 1]is a valid block(...)ending ati − 1; the(matchings[i]sits right before it. - Adding
dp[j − 1]merges neighbouring blocks like()(()).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Empty string → 0.
)(→ 0.- Adjacent blocks
()()and nested(()).
Complexity: O(n) time and space.
Hard 44. Wildcard Matching Google Amazon Microsoft
Problem: Match the whole string s against pattern p, where ? matches one character and * matches any sequence (including empty).
s = "aa", p = "a"falses = "aa", p = "*"trueConstraints: 0 ≤ s.length, p.length ≤ 2000 · s contains only lowercase English letters. · p contains only lowercase English letters, '?' or '*'.
Hint 1
Define dp[i][j] as whether the first i characters of s match the first j of p.
Hint 2
What are the two ways a * can be used?
From brute force to the pattern: Trying every split for each * is exponential; the table has m · n states with O(1) work each.
Spot it: Two strings, prefix vs prefix → 2D DP. The only interesting case is *: it either matches nothing (drop it from the pattern) or absorbs one more character of s (drop that character, keep the *).
Why not another pattern? Plain recursion on * choices is exponential without memoisation.
Approach:
dp[0][0] = True;dp[0][j]is true whilep[:j]is all*.p[j−1] == '*':dp[i][j] = dp[i][j−1] or dp[i−1][j].p[j−1] == '?'or equalss[i−1]:dp[i][j] = dp[i−1][j−1].
class Solution:
def isMatch(self, s: str, p: str) -> bool:
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ in range(m + 1)] # dp[i][j]: s[:i] matches p[:j]
dp[0][0] = True
for j in range(1, n + 1):
dp[0][j] = dp[0][j - 1] and p[j - 1] == "*" # only stars match the empty string
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j - 1] == "*":
dp[i][j] = dp[i][j - 1] or dp[i - 1][j] # star matches nothing, or one more char
elif p[j - 1] == "?" or p[j - 1] == s[i - 1]:
dp[i][j] = dp[i - 1][j - 1] # single character match
return dp[m][n]
What the code is doing, line by line:
dp[i][j − 1]: the*stands for the empty sequence.dp[i − 1][j]: the*swallowss[i − 1]and may keep going.- Literal characters and
?consume one character from both strings.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Pattern of only
*→ true. - Empty
swith a non-star pattern → false. - Consecutive stars.
Complexity: O(m · n) time and space (one row is enough for O(n) space).
Hard 87. Scramble String Google
Problem: Can s2 be obtained from s1 by recursively splitting it into two parts and optionally swapping them?
s1 = "great", s2 = "rgeat"trues1 = "abcde", s2 = "caebd"falseConstraints: s1.length == s2.length · 1 ≤ s1.length ≤ 30 · s1 and s2 consist of lowercase English letters.
Hint 1
Think recursively: where was the first split?
Hint 2
For a split length k, the halves are either in the same order or swapped in s2.
Hint 3
Memoise on the substring pair.
From brute force to the pattern: Plain recursion over all splits is exponential; memoisation makes it polynomial.
Spot it: The definition is recursive on substrings → memoised recursion (DP over substrings). For each split length k, either the halves match in place, or swapped: s1's first k matches s2's last k.
Why not another pattern? Without memoisation the same substring pairs are recomputed exponentially many times.
Approach:
scramble(a, b): equal → true; different letter counts → false.- For each split
kin1..len − 1: no swapscramble(a[:k], b[:k]) and scramble(a[k:], b[k:]). - Swap:
scramble(a[:k], b[−k:]) and scramble(a[k:], b[:−k]). - Cache results by
(a, b).
from collections import Counter
from functools import lru_cache
class Solution:
def isScramble(self, s1: str, s2: str) -> bool:
@lru_cache(maxsize=None)
def scramble(a, b):
if a == b:
return True
if Counter(a) != Counter(b):
return False # different letters can never match
for k in range(1, len(a)):
if scramble(a[:k], b[:k]) and scramble(a[k:], b[k:]):
return True # split at k, halves kept in place
if scramble(a[:k], b[-k:]) and scramble(a[k:], b[:-k]):
return True # split at k, halves swapped
return False
return scramble(s1, s2)
What the code is doing, line by line:
- The top-level split of
s1produces two parts; ins2they appear in the same order or swapped. - Letter counts must match for any pair of substrings to be scrambles, which prunes most branches.
- Memoisation bounds the work by the number of substring pairs.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Equal strings → true.
- Different letters → false.
- Length 1.
Complexity: O(n⁴) time in the worst case (n³ substring pairs × n splits), O(n³) space.
Hard 132. Palindrome Partitioning II Google Amazon Microsoft
Problem: Return the minimum number of cuts to split s so every piece is a palindrome.
s = "aab"1s = "a"0Constraints: 1 ≤ s.length ≤ 2000 · s consists of lowercase English letters only.
Hint 1
Let dp[i] be the fewest pieces for the prefix s[:i].
Hint 2
Which palindromes end at position i? Expand around centres to find them all.
From brute force to the pattern: Listing all palindrome partitions is exponential. Prefix DP over all palindromes is O(n²).
Spot it: cuts[i] = fewest pieces for s[:i] → 1D DP over prefixes, where each transition is a palindrome ending at i. Expanding around every centre lists all palindromes in O(n²) and updates the DP directly.
Why not another pattern? Backtracking over all partitions (Palindrome Partitioning I) is exponential.
Approach:
pieces[i]= min palindromes fors[:i];pieces[0] = 0, others =i(all single letters).- For each centre (odd and even), expand while
s[l] == s[r]:pieces[r + 1] = min(pieces[r + 1], pieces[l] + 1). - Answer:
pieces[n] − 1cuts.
class Solution:
def minCut(self, s: str) -> int:
n = len(s)
pieces = list(range(n + 1)) # pieces[i]: fewest palindromes covering s[:i]
for center in range(n):
for l, r in ((center, center), (center, center + 1)): # odd and even palindromes
while l >= 0 and r < n and s[l] == s[r]:
pieces[r + 1] = min(pieces[r + 1], pieces[l] + 1) # s[l..r] is a palindrome
l -= 1
r += 1
return pieces[n] - 1 # cuts = pieces - 1
What the code is doing, line by line:
- Every palindrome
s[l..r]lets a partition ofs[:l]be extended to one ofs[:r + 1]with one more piece. - Expanding around centres visits each palindrome once, without building an
n × ntable. - Updates to
pieces[r + 1]always usepieces[l]withl ≤ center. Those were final before this centre (every palindrome ending beforecentercomes from an earlier centre), so the order is safe.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already a palindrome → 0.
- All distinct letters →
n − 1. - Even-length palindromes.
Complexity: O(n²) time, O(n) space.
Hard 174. Dungeon Game Google Amazon
Problem: Moving right or down from top-left to bottom-right, health must stay ≥ 1. Return the minimum starting health.
dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]7dungeon = [[0]]1Constraints: m == dungeon.length · n == dungeon[i].length · 1 ≤ m, n ≤ 200 · -1000 ≤ dungeon[i][j] ≤ 1000
Hint 1
Forward DP needs two quantities. Try going backwards from the princess.
Hint 2
What is the minimum health needed when entering each cell?
From brute force to the pattern: Trying every path is exponential; the backwards DP is O(mn).
Spot it: Going forward you'd need both the health so far and the minimum along the way. Going backwards there's one quantity: the health needed on entering a cell. It depends on the cells after it → 2D DP from the bottom-right.
Why not another pattern? Forward DP on "maximum health" fails: a path with more health may have dipped lower on the way.
Approach:
need[r][c]= minimum health when entering(r, c)to survive to the end.needbeyond the last cell (right of or below it) = 1.need[r][c] = max(1, min(need[r+1][c], need[r][c+1]) − dungeon[r][c]).
class Solution:
def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:
m, n = len(dungeon), len(dungeon[0])
INF = float("inf")
need = [[INF] * (n + 1) for _ in range(m + 1)] # health needed when entering (r, c)
need[m][n - 1] = need[m - 1][n] = 1 # after the princess you just need to be alive
for r in range(m - 1, -1, -1):
for c in range(n - 1, -1, -1):
after = min(need[r + 1][c], need[r][c + 1]) # the cheaper way onward
need[r][c] = max(1, after - dungeon[r][c]) # never below 1
return need[0][0]
What the code is doing, line by line:
- Working backwards, each cell knows exactly what the rest of the path demands.
- A healing cell lowers the requirement but never below 1, since health must stay positive inside the cell too.
- The
INFborder keeps paths inside the grid.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single positive cell → 1.
- All damage cells.
- A big potion late in the path.
Complexity: O(m · n) time and space.
Hard 188. Best Time to Buy and Sell Stock IV Google Amazon Microsoft
Problem: With at most k transactions (buy then sell, one at a time), return the maximum profit.
k = 2, prices = [2,4,1]2k = 2, prices = [3,2,6,5,0,3]7Constraints: 1 ≤ k ≤ 100 · 1 ≤ prices.length ≤ 1000 · 0 ≤ prices[i] ≤ 1000
Hint 1
What states can you be in after each day?
Hint 2
Track the best balance after the j-th buy and after the j-th sell.
From brute force to the pattern: Trying every choice of k buy/sell days is exponential. The state machine is O(nk).
Spot it: Each day you're in one of 2k + 1 states: after the j-th buy or after the j-th sell. Keep the best balance for each state → DP state machine, updated day by day.
Why not another pattern? Greedy (take the k biggest rises) fails when rises overlap or must be merged.
Approach:
buy[j] = −∞,sell[j] = 0forj = 1..k.- Each price
p:buy[j] = max(buy[j], sell[j−1] − p);sell[j] = max(sell[j], buy[j] + p). - Answer
sell[k].
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
buy = [float("-inf")] * (k + 1) # buy[j]: best balance holding the j-th share
sell = [0] * (k + 1) # sell[j]: best balance after j complete transactions
for p in prices:
for j in range(1, k + 1):
buy[j] = max(buy[j], sell[j - 1] - p) # buy today after j-1 sales
sell[j] = max(sell[j], buy[j] + p) # sell today
return sell[k]
What the code is doing, line by line:
buy[j]is the best money left after buying for thej-th time;sell[j]after selling thej-th time.- Updating
buy[j]thensell[j]on the same day is harmless (buying and selling on one day gives 0 profit). - Unused transactions don't hurt:
sell[k] ≥ sell[j]for fewer transactions.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
k≥ n/2 (effectively unlimited).- Falling prices → 0.
- One day.
Complexity: O(n · k) time, O(k) space.
Hard 233. Number of Digit One Google Microsoft
Problem: Count the total number of digit 1 appearing in all integers from 0 to n.
n = 136n = 00Constraints: 0 ≤ n ≤ 10⁹
Hint 1
Count the 1s in each digit position separately.
Hint 2
For the tens position, how often does a 1 appear in 0..n? Split n around that digit.
From brute force to the pattern: Counting digits of every number is O(n log n); per-position counting is O(log n).
Spot it: A digit counting problem (the simplest form of digit DP). For each position with place value p, split n into high, cur digit and low. The 1s at this position come in full blocks of p for every value of high, plus a partial block decided by cur.
Why not another pattern? Counting 1s in each number from 1 to n is O(n log n) with n up to 10⁹.
Approach:
- For
p = 1, 10, 100, …:high = n // (10p),cur = (n // p) % 10,low = n % p. cur == 0:high · p;cur == 1:high · p + low + 1;cur ≥ 2:(high + 1) · p.
class Solution:
def countDigitOne(self, n: int) -> int:
total = 0
p = 1 # place value: 1, 10, 100, ...
while p <= n:
high = n // (p * 10)
cur = (n // p) % 10
low = n % p
if cur == 0:
total += high * p # full blocks only
elif cur == 1:
total += high * p + low + 1 # plus the partial block up to n
else:
total += (high + 1) * p # the current block is complete
p = p * 10
return total
What the code is doing, line by line:
- For a fixed position, each value of the higher digits gives
pnumbers with a 1 there (thelowpart runs through0..p−1). - When the current digit is 1, only
low + 1numbers of the last block are ≤ n. - When it's 2 or more, the block with a 1 here is entirely ≤ n.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 0→ 0.nwith a 1 in a position (partial block).n= 10ᵏ.
Complexity: O(log n) time, O(1) space.
Hard 403. Frog Jump Google Amazon
Problem: Stones are at increasing positions; the first jump is 1 unit, and after a jump of k the next is k − 1, k or k + 1. Can the frog reach the last stone?
stones = [0,1,3,5,6,8,12,17]truestones = [0,1,2,3,4,8,9,11]falseConstraints: 2 ≤ stones.length ≤ 2000 · 0 ≤ stones[i] ≤ 2³¹ - 1 · stones[0] == 0 · stones is sorted in a strictly increasing order.
Hint 1
What do you need to know when standing on a stone? (position and last jump)
Hint 2
For every stone, store all jump sizes that can land on it.
From brute force to the pattern: DFS on every jump sequence is exponential; the state set has O(n²) entries.
Spot it: The state is (stone, last jump size). Store, for each stone, the set of jump sizes that reach it, and push forward in position order → DP over states with a hash map.
Why not another pattern? DFS over all jump sequences without memo re-explores the same (stone, k) states many times.
Approach:
jumps[pos] = set()for every stone;jumps[0] = {0}.- For each stone in order, for each
kin its set, forstepink − 1, k, k + 1(> 0): ifpos + stepis a stone, addstepto its set. - Return whether the last stone's set is non-empty.
class Solution:
def canCross(self, stones: List[int]) -> bool:
jumps = {pos: set() for pos in stones} # stone -> jump sizes that can land on it
jumps[stones[0]].add(0)
for pos in stones:
for k in jumps[pos]:
for step in (k - 1, k, k + 1):
if step > 0 and pos + step in jumps:
jumps[pos + step].add(step) # reachable with this last jump
return bool(jumps[stones[-1]])
What the code is doing, line by line:
- Starting with a "jump of 0" at the first stone makes the first real jump
1(0 + 1). - Stones are processed left to right, and jumps only go right, so each set is complete before it's used.
- Each stone has at most
O(n)jump sizes, so there areO(n²)states.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- The second stone isn't at position 1 → false.
- Large gaps.
- Two stones.
Complexity: O(n²) time and space.
Hard 546. Remove Boxes Google
Problem: Removing k adjacent equal boxes scores k². Return the maximum total score to remove all boxes.
boxes = [1,3,2,2,2,3,4,3,1]23boxes = [1,1,1]9Constraints: 1 ≤ boxes.length ≤ 100 · 1 ≤ boxes[i] ≤ 100
Hint 1
Why doesn't dp[i][j] work? Think about equal boxes on both sides of a removed block.
Hint 2
Add a parameter: how many boxes equal to boxes[i] are attached on its left.
From brute force to the pattern: Trying every removal order is factorial. The 3D DP has O(n³) states.
Spot it: Plain interval DP fails because removing the middle can join equal boxes from both sides. Add a third dimension: k = how many boxes equal to boxes[i] are already glued to its left → interval DP with extra state.
Why not another pattern? dp[i][j] alone can't express that boxes outside the interval will join one inside it later.
Approach:
dp(i, j, k): first absorb a run of equal boxes at the start intok.- Option 1: remove
boxes[i]with itskcompanions now:(k + 1)² + dp(i + 1, j, 0). - Option 2: for each
mini + 1..jwithboxes[m] == boxes[i]: cleari + 1..m − 1first, thenboxes[i]joinsboxes[m]:dp(i + 1, m − 1, 0) + dp(m, j, k + 1).
from functools import lru_cache
class Solution:
def removeBoxes(self, boxes: List[int]) -> int:
@lru_cache(maxsize=None)
def dp(i, j, k): # boxes[i..j], with k boxes equal to boxes[i] glued on its left
if i > j:
return 0
while i < j and boxes[i + 1] == boxes[i]:
i += 1 # absorb an equal run into k
k += 1
best = (k + 1) ** 2 + dp(i + 1, j, 0) # remove boxes[i] and its group now
for m in range(i + 1, j + 1):
if boxes[m] == boxes[i]:
best = max(best, dp(i + 1, m - 1, 0) + dp(m, j, k + 1)) # clear the middle, then join m
return best
return dp(0, len(boxes) - 1, 0)
What the code is doing, line by line:
kremembers the boxes that will be removed together withboxes[i], wherever they came from.- Removing the middle segment first lets
boxes[i](and itskcompanions) merge with an equal box atm. - Absorbing an equal run up front reduces the number of states visited.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All equal →
n². - All distinct →
n. - One box → 1.
Complexity: O(n⁴) time, O(n³) space (n ≤ 100).
Hard 741. Cherry Pickup Google Amazon
Problem: Go from (0, 0) to (n−1, n−1) moving right/down, then back moving left/up, collecting cherries (each picked once). Thorns block. Return the maximum cherries, or 0 if no path.
grid = [[0,1,-1],[1,0,-1],[1,1,1]]5grid = [[1,1,-1],[1,-1,1],[-1,1,1]]0Constraints: n == grid.length · n == grid[i].length · 1 ≤ n ≤ 50 · grid[i][j] is -1, 0, or 1. · grid[0][0] != -1 · grid[n - 1][n - 1] != -1
Hint 1
Turn the return trip into a second forward trip.
Hint 2
Move both walkers one step at a time. What's the state? (They're always on the same anti-diagonal.)
Hint 3
Don't double-count a cell both walkers stand on.
From brute force to the pattern: Optimising the first path then the second separately is wrong; trying all path pairs is exponential. The lockstep DP is O(n³).
Spot it: The return trip is just another forward trip. Walk two people at once: after t steps both are on the diagonal r + c = t, so the state is (t, r1, r2) → 3D DP. If they're on the same cell, count it once.
Why not another pattern? Taking the best path greedily and then the best path again on what's left is not optimal.
Approach:
dp[r1][r2]for the current stept(columns aret − r1,t − r2); startdp[0][0] = grid[0][0].- For each step, each pair of rows: take the best of the 4 previous moves (each person came from up or left).
- Add cherries at both cells (once if the same); thorns make the state −∞.
- Answer
max(0, dp[n−1][n−1])at the last step.
class Solution:
def cherryPickup(self, grid: List[List[int]]) -> int:
n = len(grid)
NEG = float("-inf")
dp = [[NEG] * n for _ in range(n)] # dp[r1][r2] at the current step
dp[0][0] = grid[0][0]
for t in range(1, 2 * n - 1): # both walkers have taken t steps
nxt = [[NEG] * n for _ in range(n)]
for r1 in range(max(0, t - n + 1), min(n, t + 1)):
c1 = t - r1
if grid[r1][c1] == -1:
continue
for r2 in range(r1, min(n, t + 1)): # r2 >= r1 by symmetry
c2 = t - r2
if grid[r2][c2] == -1:
continue
prev = max(dp[r1][r2], dp[r1 - 1][r2] if r1 else NEG,
dp[r1][r2 - 1] if r2 else NEG,
dp[r1 - 1][r2 - 1] if r1 and r2 else NEG) # each came from up or left
if prev == NEG:
continue
gain = grid[r1][c1] + (grid[r2][c2] if r2 != r1 else 0) # same cell counts once
nxt[r1][r2] = prev + gain
dp = nxt
return max(0, dp[n - 1][n - 1])
What the code is doing, line by line:
- A round trip equals two forward paths, which can be walked in lockstep.
- Walking in lockstep means both walkers are on the same anti-diagonal, so a shared cell is detected by
r1 == r2. - Keeping
r2 ≥ r1halves the states (the walkers are interchangeable);dp[r1][r2 − 1]withr2 − 1 < r1is still −∞, so it's harmless. - Unreachable states stay −∞; if the end is unreachable the answer is 0.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No path to the end → 0.
- Both walkers share most of the route.
- 1×1 grid.
Complexity: O(n³) time, O(n²) space.
Hard 879. Profitable Schemes Google
Problem: Each crime needs group[i] members and earns profit[i]. Count subsets using ≤ n members with total profit ≥ minProfit (mod 10⁹ + 7).
n = 5, minProfit = 3, group = [2,2], profit = [2,3]2n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8]7Constraints: 1 ≤ n ≤ 100 · 0 ≤ minProfit ≤ 100 · 1 ≤ group.length ≤ 100 · 1 ≤ group[i] ≤ 100 · profit.length == group.length · 0 ≤ profit[i] ≤ 100
Hint 1
It's a knapsack: members are the capacity.
Hint 2
You also need profit as a dimension. Do you need exact profit beyond minProfit?
From brute force to the pattern: Trying all subsets is O(2^crimes). The capped knapsack is polynomial.
Spot it: Choosing a subset under a size limit → 0/1 knapsack. The second dimension is profit, but any profit beyond minProfit is equally good, so cap it at minProfit to keep the table small.
Why not another pattern? Enumerating all 2¹⁰⁰ subsets is impossible; tracking exact profits makes the table needlessly large.
Approach:
ways[m][p]= subsets usingmmembers with capped profitp;ways[0][0] = 1.- For each crime, iterate
mdownwards (0/1):ways[m + g][min(P, p + gain)] += ways[m][p]. - Answer:
Σ ways[m][minProfit]overm ≤ n.
class Solution:
def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
MOD = 10**9 + 7
ways = [[0] * (minProfit + 1) for _ in range(n + 1)] # ways[members][capped profit]
ways[0][0] = 1
for g, gain in zip(group, profit):
for m in range(n - g, -1, -1): # downwards: each crime used at most once
for p in range(minProfit, -1, -1):
if ways[m][p]:
q = min(minProfit, p + gain) # more than enough profit is just "enough"
ways[m + g][q] = (ways[m + g][q] + ways[m][p]) % MOD
return sum(ways[m][minProfit] for m in range(n + 1)) % MOD
What the code is doing, line by line:
- Iterating members downwards keeps each crime from being counted twice in one pass.
- Capping profit merges all "profitable enough" states into one column.
- The answer sums the capped column over all member counts up to
n.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
minProfit = 0(the empty scheme counts).- A crime needing more than
nmembers. - Duplicate crimes.
Complexity: O(crimes · n · minProfit) time, O(n · minProfit) space.
Hard 1092. Shortest Common Supersequence Google
Problem: Return a shortest string that has both str1 and str2 as subsequences.
str1 = "abac", str2 = "cab""cabac"str1 = "aaaaaaaa", str2 = "aaaaaaaa""aaaaaaaa"Constraints: 1 ≤ str1.length, str2.length ≤ 1000 · str1 and str2 consist of lowercase English letters.
Hint 1
How long is the shortest common supersequence in terms of the LCS?
Hint 2
Build the LCS table, then reconstruct the answer by walking back through it.
From brute force to the pattern: Trying all interleavings is exponential; LCS + reconstruction is O(mn).
Spot it: The supersequence writes the common letters once and every other letter once: length m + n − LCS. Fill the LCS table, then walk it backwards to build the string.
Why not another pattern? Merging greedily letter by letter can repeat letters that could have been shared.
Approach:
- LCS DP table
L. - From
(m, n): equal letters → output once, move diagonally. Otherwise move toward the largerLneighbour, outputting that string's letter. - Output the leftovers of either string; reverse.
class Solution:
def shortestCommonSupersequence(self, str1: str, str2: str) -> str:
m, n = len(str1), len(str2)
L = [[0] * (n + 1) for _ in range(m + 1)] # LCS lengths of prefixes
for i in range(1, m + 1):
for j in range(1, n + 1):
if str1[i - 1] == str2[j - 1]:
L[i][j] = L[i - 1][j - 1] + 1
else:
L[i][j] = max(L[i - 1][j], L[i][j - 1])
out = []
i, j = m, n
while i and j:
if str1[i - 1] == str2[j - 1]:
out.append(str1[i - 1]) # shared letter: write once
i -= 1
j -= 1
elif L[i - 1][j] >= L[i][j - 1]:
out.append(str1[i - 1]) # this letter of str1 isn't in the LCS path
i -= 1
else:
out.append(str2[j - 1])
j -= 1
out.extend(reversed(str1[:i])) # whatever is left
out.extend(reversed(str2[:j]))
return "".join(reversed(out))
What the code is doing, line by line:
- The walk back follows one LCS: matched letters are shared, the rest are copied from their own string.
- The result has length
m + n − LCS, the minimum possible. - Letters are collected backwards, so the list is reversed at the end.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- One string is a subsequence of the other.
- No common letters (concatenate).
- Multiple valid answers (any is accepted).
Complexity: O(m · n) time and space.
Hard 1301. Number of Paths with Max Score Google
Problem: From S (bottom-right) to E (top-left) moving up, left or up-left, avoiding X. Return [max sum of digits, number of paths achieving it] mod 10⁹ + 7, or [0, 0].
board = ["E23","2X2","12S"][7,1]board = ["E12","1X1","21S"][4,2]Constraints: 2 ≤ board.length == board[i].length ≤ 100
Hint 1
Store, for each cell, the best score to reach it and the number of ways to get that score.
Hint 2
When two neighbours give the same best score, add their counts.
From brute force to the pattern: Enumerating paths is exponential; the paired DP is O(n²).
Spot it: Each cell's best comes from three neighbours → grid DP storing two things: the best score and how many paths reach it.
Why not another pattern? Counting paths and maximising separately mixes up which paths are optimal.
Approach:
score[S] = 0,ways[S] = 1; process cells from bottom-right to top-left.- For each non-
Xcell, look at the cell below, to the right, and diagonal. Take the best score; sum the ways of every neighbour with that score. - Add the cell's digit (
EandScount 0).
class Solution:
def pathsWithMaxScore(self, board: List[str]) -> List[int]:
MOD = 10**9 + 7
n = len(board)
score = [[-1] * (n + 1) for _ in range(n + 1)] # -1 = unreachable
ways = [[0] * (n + 1) for _ in range(n + 1)]
score[n - 1][n - 1] = 0
ways[n - 1][n - 1] = 1
for r in range(n - 1, -1, -1):
for c in range(n - 1, -1, -1):
if board[r][c] in "XS":
continue
best = -1
count = 0
for pr, pc in ((r + 1, c), (r, c + 1), (r + 1, c + 1)): # cells we could come from
if score[pr][pc] > best:
best, count = score[pr][pc], ways[pr][pc] # better score: restart the count
elif score[pr][pc] == best and best >= 0:
count += ways[pr][pc] # another way to the same best
if best < 0:
continue # unreachable
digit = 0 if board[r][c] == "E" else int(board[r][c])
score[r][c] = best + digit
ways[r][c] = count % MOD
return [score[0][0], ways[0][0]] if score[0][0] >= 0 else [0, 0]
What the code is doing, line by line:
- The padding row and column are unreachable (−1), so the borders need no special case.
- When neighbours tie on the best score, their path counts add up.
- Unreachable cells stay at −1 and are ignored by later cells.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
Eunreachable →[0, 0].- Many tied paths (count mod 10⁹ + 7).
- Board of size 2.
Complexity: O(n²) time and space.
Hard 1320. Minimum Distance to Type a Word Using Two Fingers Google
Problem: Letters sit on a 6-wide keyboard grid. Type word with two fingers (free initial positions), minimising total Manhattan movement.
word = "CAKE"3word = "HAPPY"6Constraints: 2 ≤ word.length ≤ 300 · word consists of uppercase English letters.
Hint 1
After typing a letter, where is one of the fingers for sure?
Hint 2
So the DP only needs the position of the other finger.
From brute force to the pattern: Trying every assignment of letters to fingers is O(2ⁿ); the DP has 27 states per letter.
Spot it: After typing letter i, one finger is on word[i]; only the other finger's position is unknown → DP over (index, other finger), 27 states per step (26 letters or "not placed yet").
Why not another pattern? Tracking both fingers explicitly is 26² states per step; it works but the observation halves the dimension.
Approach:
dp[o]= min cost with the other finger ato(26 = not placed); start{26: 0}.- To type the next letter
cafterp: move the same finger (dp[o] + dist(p, c), other stayso) or the other one (dp[o] + dist(o, c), other becomesp). - A not-yet-placed finger moves for free.
class Solution:
def minimumDistance(self, word: str) -> int:
def dist(a, b): # 26 means "this finger hasn't been placed yet"
if a == 26:
return 0
return abs(a // 6 - b // 6) + abs(a % 6 - b % 6)
dp = {26: 0} # position of the other finger -> min cost
letters = [ord(ch) - ord("A") for ch in word]
for p, c in zip(letters, letters[1:]): # one finger is on p; type c next
nxt = {}
for other, cost in dp.items():
same = cost + dist(p, c) # the finger on p types c
if same < nxt.get(other, float("inf")):
nxt[other] = same
moved = cost + dist(other, c) # the other finger types c; p becomes "other"
if moved < nxt.get(p, float("inf")):
nxt[p] = moved
dp = nxt
return min(dp.values())
What the code is doing, line by line:
- The finger that just typed is always on the previous letter, so it needn't be stored.
- Each new letter is typed by either finger: two transitions per state.
- The first letter is free (any finger starts there), which the loop's start state captures.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single letter → 0.
- Repeated letters.
- Letters far apart on the keyboard.
Complexity: O(n · 27) time, O(27) space.
Hard 1340. Jump Game V Google Amazon
Problem: From i you can jump up to d steps left or right to a lower index, if every index in between is also lower than arr[i]. Return the maximum number of indices visited.
arr = [6,4,14,6,8,13,9,7,10,6,12], d = 24arr = [3,3,3,3,3], d = 31Constraints: 1 ≤ arr.length ≤ 1000 · 1 ≤ arr[i] ≤ 10⁵ · 1 ≤ d ≤ arr.length
Hint 1
Can you ever jump back to where you were?
Hint 2
The longest path from each index can be memoised.
From brute force to the pattern: DFS from each index without memo is exponential; memoised it's O(n · d).
Spot it: Jumps always go to strictly lower values, so there are no cycles: a DAG. The longest path from each index is 1 + the best among its targets → memoised DFS (or DP in increasing value order).
Why not another pattern? Plain DFS without memo recomputes the same suffixes exponentially often.
Approach:
visits(i)= 1 + maxvisits(j)over valid jumps.- Scan right from
iup todsteps; stop at the first value ≥arr[i]. Same to the left. - Answer: max over all starts.
from functools import lru_cache
class Solution:
def maxJumps(self, arr: List[int], d: int) -> int:
n = len(arr)
@lru_cache(maxsize=None)
def visits(i): # most indices visited starting at i
best = 1
for step in (1, -1):
j = i + step
while 0 <= j < n and abs(j - i) <= d and arr[j] < arr[i]:
best = max(best, 1 + visits(j))
j += step # keep going while everything in between is lower
return best
return max(visits(i) for i in range(n))
What the code is doing, line by line:
- A taller-or-equal value blocks every jump beyond it in that direction, so the scan stops there.
- Values strictly decrease along jumps, so the recursion always ends.
- Memoisation computes each
visits(i)once.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All equal values → 1.
- Strictly decreasing array.
d = 1.
Complexity: O(n · d) time, O(n) space.
Hard 1406. Stone Game III Google Amazon
Problem: Players take 1, 2 or 3 stones from the front (values may be negative). Return "Alice", "Bob" or "Tie".
stoneValue = [1,2,3,7]"Bob"stoneValue = [1,2,3,-9]"Alice"Constraints: 1 ≤ stoneValue.length ≤ 5 * 10⁴ · -1000 ≤ stoneValue[i] ≤ 1000
Hint 1
Think of the score difference between the player to move and the other.
Hint 2
dp[i] = max(sum of the next k stones − dp[i + k]).
From brute force to the pattern: The game tree has 3ⁿ branches; the DP is linear.
Spot it: A game on a suffix → DP on the score difference for the player to move, from the back.
Why not another pattern? Greedy (take the max prefix) ignores what the opponent gets next.
Approach:
lead[n] = 0.lead[i] = max(sum(values[i:i+k]) − lead[i + k])fork = 1..3.- Compare
lead[0]with 0.
class Solution:
def stoneGameIII(self, stoneValue: List[int]) -> str:
n = len(stoneValue)
lead = [0] * (n + 1) # lead[i]: mover's best score minus opponent's on stones i..
for i in range(n - 1, -1, -1):
best = float("-inf")
taken = 0
for k in range(1, 4):
if i + k > n:
break
taken += stoneValue[i + k - 1]
best = max(best, taken - lead[i + k]) # opponent moves next
lead[i] = best
if lead[0] > 0:
return "Alice"
return "Bob" if lead[0] < 0 else "Tie"
What the code is doing, line by line:
- The difference formulation lets one array describe both players.
- Negative values make taking more stones sometimes bad, so all three options are compared.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Negative values.
- A tie.
- One stone.
Complexity: O(n) time and space.
Hard 1463. Cherry Pickup II Google Amazon
Problem: Two robots start at the top corners and move down one row per step (left-down, down, right-down). Maximise the cherries collected (shared cells count once).
grid = [[3,1,1],[2,5,1],[1,5,5],[2,1,1]]24grid = [[1,0,0,0,0,0,1],[2,0,0,0,0,3,0],[2,0,9,0,0,0,0],[0,3,0,5,4,0,0],[1,0,2,3,0,0,6]]28Constraints: rows == grid.length · cols == grid[i].length · 2 ≤ rows, cols ≤ 70 · 0 ≤ grid[i][j] ≤ 100
Hint 1
Both robots are always on the same row.
Hint 2
DP over the pair of columns, row by row.
From brute force to the pattern: All pairs of paths: 9^rows. DP over column pairs: O(rows · cols²).
Spot it: The robots are always on the same row, so the state is the pair of columns → row-by-row DP over (c1, c2).
Why not another pattern? Optimising each robot separately can double-count or miss the best combined route.
Approach:
dp[c1][c2]for the current row; start with(0, cols − 1).- For the next row, each state tries all 9 combinations of moves.
- Gain:
grid[r][c1] + grid[r][c2](once ifc1 == c2).
class Solution:
def cherryPickup(self, grid: List[List[int]]) -> int:
rows, cols = len(grid), len(grid[0])
NEG = float("-inf")
dp = [[NEG] * cols for _ in range(cols)] # dp[c1][c2] on the current row
dp[0][cols - 1] = grid[0][0] + grid[0][cols - 1]
for r in range(1, rows):
nxt = [[NEG] * cols for _ in range(cols)]
for c1 in range(cols):
for c2 in range(cols):
if dp[c1][c2] == NEG:
continue
for n1 in (c1 - 1, c1, c1 + 1):
for n2 in (c2 - 1, c2, c2 + 1): # 9 move combinations
if 0 <= n1 < cols and 0 <= n2 < cols:
gain = grid[r][n1] + (grid[r][n2] if n1 != n2 else 0) # shared cell once
nxt[n1][n2] = max(nxt[n1][n2], dp[c1][c2] + gain)
dp = nxt
return max(max(row) for row in dp)
What the code is doing, line by line:
- Both robots move one row per step, so a pair of columns fully describes the state.
- Unreachable pairs stay −∞ and are skipped.
- A cell visited by both robots counts once.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Robots meeting in one cell.
- Two columns.
- Zero cherries everywhere.
Complexity: O(rows · cols² · 9) time, O(cols²) space.
Hard 1510. Stone Game IV Google
Problem: Players remove a non-zero square number of stones; the player who can't move loses. Does Alice win with n stones?
n = 1truen = 2falseConstraints: 1 ≤ n ≤ 10⁵
Hint 1
Classify every stone count as winning or losing for the player to move.
Hint 2
A position is winning if at least one move leads to a losing position.
From brute force to the pattern: The full game tree is exponential; the DP is O(n √n).
Spot it: A position is winning if some move leads to a losing position for the opponent → win/lose DP over the number of stones.
Why not another pattern? Greedy (take the largest square) isn't optimal: with 7 stones taking 4 leaves 3, a winning position for Bob.
Approach:
win[0] = False.win[i] = any(not win[i − k²])fork² ≤ i.
class Solution:
def winnerSquareGame(self, n: int) -> bool:
win = [False] * (n + 1) # win[i]: the player to move with i stones wins
for i in range(1, n + 1):
k = 1
while k * k <= i:
if not win[i - k * k]:
win[i] = True # this move leaves the opponent in a losing spot
break
k += 1
return win[n]
What the code is doing, line by line:
- With 0 stones the player to move can't move, so it's a loss.
- One move to a losing position is enough to win.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
na perfect square → true.n = 2→ false.- Large
n.
Complexity: O(n √n) time, O(n) space.
Hard 2376. Count Special Integers Google
Problem: Count integers in [1, n] whose digits are all distinct (n ≤ 2·10⁹).
n = 2019n = 55Constraints: 1 ≤ n ≤ 2 * 10⁹
Hint 1
Count numbers with fewer digits than n first.
Hint 2
For the same length, go digit by digit: pick a smaller digit (then the rest is free) or match n's digit.
From brute force to the pattern: Testing every number up to n is up to 2·10⁹ checks; the digit walk is about 100 steps.
Spot it: A digit DP classic. Numbers shorter than n are counted directly (9 · 9 · 8 · …). For numbers of the same length, walk n's digits: at each position, count smaller unused digits times the ways to fill the rest, then continue with n's digit if it's unused.
Why not another pattern? Checking every number up to 2·10⁹ is far too slow.
Approach:
perm(m, r)= ways to arrangerdigits out ofmavailable.- Shorter lengths
L:9 · perm(9, L − 1)(no leading zero). - Same length: for position
i, for each unused digitd < digits[i](not 0 at the front): addperm(10 − i − 1, len − i − 1). - If
digits[i]is already used, stop; if all positions pass, countnitself.
class Solution:
def countSpecialNumbers(self, n: int) -> int:
def perm(m, r): # ordered choices of r distinct digits from m
out = 1
for t in range(r):
out *= m - t
return out
digits = list(map(int, str(n)))
L = len(digits)
total = 0
for length in range(1, L):
total += 9 * perm(9, length - 1) # shorter numbers: first digit 1-9, rest distinct
used = set()
for i, top in enumerate(digits):
for d in range(1 if i == 0 else 0, top):
if d not in used:
total += perm(10 - i - 1, L - i - 1) # prefix smaller than n: rest is free
if top in used:
return total # n's own prefix repeats a digit: nothing more matches
used.add(top)
return total + 1 # n itself has distinct digits
What the code is doing, line by line:
- Shorter numbers are all below
n; count them by length. - Choosing a smaller digit at position
i(with the same prefix asn) frees all later positions:perm(remaining digits, remaining positions). - Following
n's digit keeps the number tight; a repeated digit ends the walk.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n< 10.nitself has a repeated digit.n= 10ᵏ.
Complexity: O(10 · log n) time, O(1) space.
Hard 2484. Count Palindromic Subsequences Google Microsoft
Problem: Count palindromic subsequences of length 5 in a digit string (mod 10⁹ + 7).
s = "103301"2s = "0000000"21Constraints: 1 ≤ s.length ≤ 10⁴ · s consists of digits.
Hint 1
A length-5 palindrome looks like a b c b a. Fix the middle c.
Hint 2
Count subsequences of two digits ending before the middle and starting after it.
From brute force to the pattern: Choosing all index 5-tuples is O(n⁵). Prefix/suffix pair counts make it O(100n).
Spot it: A length-5 palindrome is a b c b a. Fix the middle index; it pairs the count of subsequences ab to its left with the count of ba to its right. Precompute pair counts with prefix/suffix DP over the 10 × 10 digit pairs.
Why not another pattern? Choosing 5 indices is O(n⁵); fixing the outer pairs naively is O(n²) per middle.
Approach:
- Suffix pass: for each position,
after[i][a][b]= number ofabsubsequences ins[i:](as a running table). - Prefix pass with running
before[a][b]and digit counts. - At each middle
m: addΣ before[a][b] · after[m + 1][b][a].
class Solution:
def countPalindromes(self, s: str) -> int:
MOD = 10**9 + 7
n = len(s)
digits = [int(ch) for ch in s]
after = [None] * (n + 1) # after[i][b][a]: subsequences "b a" inside s[i:]
pairs = [[0] * 10 for _ in range(10)]
count = [0] * 10
after[n] = [row[:] for row in pairs]
for i in range(n - 1, -1, -1):
d = digits[i]
for a in range(10):
pairs[d][a] += count[a] # d followed by any later a
count[d] += 1
after[i] = [row[:] for row in pairs]
before = [[0] * 10 for _ in range(10)] # subsequences "a b" seen so far
count = [0] * 10
total = 0
for m in range(n):
right = after[m + 1]
for a in range(10):
for b in range(10):
total += before[a][b] * right[b][a] # a b | middle | b a
d = digits[m]
for a in range(10):
before[a][d] += count[a] # any earlier a followed by d
count[d] += 1
return total % MOD
What the code is doing, line by line:
- A palindrome of length 5 is determined by its outer pair
(a, b)on the left mirrored as(b, a)on the right. pairs[x][y]counts subsequencesx yand is updated inO(10)per character using single-digit counts.- For each middle, the left pairs and right pairs are independent, so their counts multiply.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Fewer than 5 characters → 0.
- All the same digit →
C(n, 5). - No repeated pairs → 0.
Complexity: O(100 · n) time and space.
Hard 3225. Maximum Score From Grid Operations Google Amazon
Problem: Choose a black height for each column (cells from the top down). A white cell scores its value if a horizontal neighbour is black. Return the maximum score.
grid = [[0,0,0,0,0],[0,0,3,0,0],[0,1,0,0,0],[5,0,0,3,0],[0,0,0,0,2]]11grid = [[10,9,0,0,15],[7,1,0,8,0],[5,20,0,11,0],[0,0,0,1,2],[8,12,1,10,3]]94Constraints: 1 ≤ n == grid.length ≤ 100 · n == grid[i].length · 0 ≤ grid[i][j] ≤ 10⁹
Hint 1
Which white cells of column j can score? It depends on the heights of columns j − 1 and j + 1.
Hint 2
The score of column j is the max of what its left neighbour gives and what its right neighbour gives. Can you choose one side per column?
Hint 3
DP over columns with the current height and whether the current column still waits for its right neighbour.
From brute force to the pattern: All (n + 1)ⁿ height choices are impossible; a DP over two neighbouring heights is O(n⁴). The pending flag brings it to O(n³).
Spot it: Column j with height h scores the cells from h up to max(left height, right height). A max of two options can be chosen: credit column j from its left or from its right neighbour. Then a column DP needs only the current height and whether the current column's credit is still pending (to be decided by the next column's height).
Why not another pattern? Tracking the previous two heights is O(n⁴); brute force over heights is (n + 1)ⁿ.
Key insight: score_j = max(gain from left, gain from right) where gain = colsum(neighbour height) − colsum(own height) (if positive). Maximising over the choice of side gives the same total, and each choice depends on only two adjacent columns.
Approach:
col[j][h]= sum of the tophcells of columnj.- State after column
j:(h, pending);pendingmeans columnjwill be credited from its right neighbour. - Moving to column
j + 1with heighth2: if pending, addmax(0, col[j][h2] − col[j][h]). Columnj + 1then either takes credit from the left now (max(0, col[j+1][h] − col[j+1][h2])) or becomes pending. - Answer: the best state after the last column (a pending credit there gives 0).
class Solution:
def maximumScore(self, grid: List[List[int]]) -> int:
n = len(grid)
col = [] # col[j][h]: sum of the top h cells of column j
for j in range(n):
sums = [0]
for i in range(n):
sums.append(sums[-1] + grid[i][j])
col.append(sums)
NEG = float("-inf")
done = [0] * (n + 1) # done[h]: best so far, column j credited (or credited from the left)
pending = [0] * (n + 1) # pending[h]: column j will be credited by its right neighbour
for j in range(n - 1):
new_done = [NEG] * (n + 1)
new_pending = [NEG] * (n + 1)
for h in range(n + 1):
for h2 in range(n + 1): # height of column j + 1
base = max(done[h], pending[h] + max(0, col[j][h2] - col[j][h])) # settle column j
left_credit = max(0, col[j + 1][h] - col[j + 1][h2]) # column j+1 scored by column j
if base + left_credit > new_done[h2]:
new_done[h2] = base + left_credit
if base > new_pending[h2]:
new_pending[h2] = base # column j+1 waits for its right neighbour
done, pending = new_done, new_pending
return max(max(done), max(pending))
What the code is doing, line by line:
- Black cells are a top prefix of each column, so a column's score is a difference of prefix sums.
- Because values are non-negative,
colsum(max(a, b)) = max(colsum(a), colsum(b)), so the score is the better of the two one-sided gains. pendingdefers columnj's gain until columnj + 1's height is known.- The last column's pending credit has no right neighbour, which is the same as gaining 0.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 1→ 0 (no neighbours).- All zeros → 0.
- Tall black columns next to valuable white cells.
Complexity: O(n³) time, O(n²) space for the prefix sums.
Hard 3336. Find the Number of Subsequences With Equal GCD Google Meta Amazon Microsoft
Problem: Count pairs of disjoint non-empty subsequences with equal GCD (mod 10⁹ + 7). Values and length ≤ 200.
nums = [1,2,3,4]10nums = [10,20,30]2Constraints: 1 ≤ nums.length ≤ 200 · 1 ≤ nums[i] ≤ 200
Hint 1
Each element is in seq1, in seq2, or in neither.
Hint 2
After processing a prefix, what do you need to know? Just the two GCDs so far.
From brute force to the pattern: All 3ⁿ assignments are impossible; the GCD-pair DP is O(n · 200²).
Spot it: Each element has three choices: skip, put in seq1, put in seq2. Only the two running GCDs matter afterwards (0 = still empty, since gcd(0, x) = x) → DP over (g1, g2), at most 201 × 201 states.
Why not another pattern? Enumerating 3ⁿ assignments is impossible for n = 200.
Approach:
ways[g1][g2]; startways[0][0] = 1.- For each
x: copy (skip) and add transitions to(gcd(g1, x), g2)and(g1, gcd(g2, x)). - Answer:
Σ ways[g][g]forg ≥ 1.
from math import gcd
class Solution:
def subsequencePairCount(self, nums: List[int]) -> int:
MOD = 10**9 + 7
top = max(nums)
ways = [[0] * (top + 1) for _ in range(top + 1)] # ways[g1][g2]; 0 means "still empty"
ways[0][0] = 1
for x in nums:
nxt = [row[:] for row in ways] # skip x
for g1 in range(top + 1):
for g2 in range(top + 1):
w = ways[g1][g2]
if w:
a = gcd(g1, x)
nxt[a][g2] = (nxt[a][g2] + w) % MOD # x joins seq1
b = gcd(g2, x)
nxt[g1][b] = (nxt[g1][b] + w) % MOD # x joins seq2
ways = nxt
return sum(ways[g][g] for g in range(1, top + 1)) % MOD
What the code is doing, line by line:
gcd(0, x) = x, so 0 works as the GCD of an empty sequence.- Every assignment of elements to seq1 / seq2 / neither is counted exactly once.
- Only
g ≥ 1on the diagonal counts, which excludes empty sequences.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All ones (many pairs).
- A single element → 0.
- Distinct primes.
Complexity: O(n · M²) time (M = max value ≤ 200), O(M²) space.
Hard 3700. Number of ZigZag Arrays II Google
Problem: Count arrays of length n (up to 10⁹) with values in [l, r] that strictly alternate up and down (mod 10⁹ + 7).
n = 3, l = 4, r = 52n = 3, l = 1, r = 310Constraints: 3 ≤ n ≤ 10⁹ · 1 ≤ l < r ≤ 75
Hint 1
Write the DP: last value and whether the last step went up or down.
Hint 2
The DP step is the same linear transformation every time. How do you apply it 10⁹ times?
Hint 3
Symmetry: an up-ending array mirrored is a down-ending array.
From brute force to the pattern: Running the DP for n steps is O(n · m²); matrix exponentiation is O(m³ log n).
Spot it: The DP up[v], down[v] (last value v, last step up/down) is the same linear step every time, so n steps are a matrix power. Reflection v → m − 1 − v turns up into down, so one vector of size m ≤ 75 is enough: up'[v] = Σ up[w] over v + w ≥ m.
Why not another pattern? Iterating the DP n times is O(n · m) with n = 10⁹.
Approach:
- Shift values to
0..m−1. After 2 elements:up[v] = v(any smaller first value). - One step:
up'[v] = Σ_{u<v} down[u] = Σ_{w > m−1−v} up[w](usingdown[u] = up[m−1−u]), a fixed matrixA. up_n = A^(n−2) · up_2; answer2 · Σ up_n(up and down by symmetry).
from operator import mul
class Solution:
def zigZagArrays(self, n: int, l: int, r: int) -> int:
MOD = 10**9 + 7
m = r - l + 1
A = [[1 if v + w >= m else 0 for w in range(m)] for v in range(m)] # up'[v] = sum of up[w], w >= m - v
def matmul(X, Y):
cols = list(zip(*Y))
return [[sum(map(mul, row, c)) % MOD for c in cols] for row in X]
def matvec(X, vec):
return [sum(map(mul, row, vec)) % MOD for row in X]
up = list(range(m)) # length 2, last step up, ending at v: any smaller first value
e = n - 2
while e:
if e & 1:
up = matvec(A, up) # apply A for this power-of-two block
A = matmul(A, A)
e >>= 1
return 2 * sum(up) % MOD # down arrays mirror up arrays
What the code is doing, line by line:
- A zigzag array alternates directions, so the state is the last value and the last direction.
- Mirroring every value (
v → m − 1 − v) swaps up and down, sodown[v] = up[m − 1 − v]always holds. - Applying
Ato the vector is one step of the DP; binary exponentiation applies itn − 2times inO(log n)matrix products. - Since matrix powers commute, applying the blocks of
Ato the vector in any order gives the same result.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 3.- Two possible values (only alternating patterns).
- Huge
n.
Complexity: O(m³ log n) time, O(m²) space (m ≤ 75).
Hard 3743. Maximize Cyclic Partition Score Google
Problem: Partition a cyclic array into at most k contiguous parts to maximise the sum of (max − min) of each part.
nums = [1,2,3,3], k = 23nums = [1,2,3,3], k = 12Constraints: 1 ≤ nums.length ≤ 1000 · 1 ≤ nums[i] ≤ 10⁹ · 1 ≤ k ≤ nums.length
Hint 1
The range of a part is the best value of (one element) − (another element) inside it.
Hint 2
So choose a + and a − element per part: a DP like stock trading with at most k transactions.
Hint 3
For the cycle: where can you cut it without losing the optimal answer? Think about the global maximum.
From brute force to the pattern: Every cut point × a linear DP is O(n² k); two rotations suffice, giving O(nk).
Spot it: Relax each part's range to "choose one element as + and one as −": that's never more than the range and equals it for the right choice. Then it's Stock V's DP (long or short transactions, at most k). For the cycle, the part holding the global maximum can always be cut so the maximum sits at the start or the end of a linear array: try both rotations.
Why not another pattern? Trying every cut point and running a linear DP is O(n² k).
Key insight: Cut the circle right before the maximum, or right after it. The part containing the maximum is split in two; whichever piece also holds that part's minimum keeps the full range, and the other piece merges into its neighbour without losing anything.
Approach:
linear(a): statesfree[j],plus[j](picked the max of partj),minus[j](picked the min first).- Each element
x: openpluswithfree[j−1] + xorminuswithfree[j−1] − x; close withplus[j] − xorminus[j] + x. - Answer:
max(linear(rotation starting at the max), linear(rotation ending at the max)).
class Solution:
def maximumScore(self, nums: List[int], k: int) -> int:
def linear(a):
NEG = float("-inf")
free = [0] + [NEG] * k # j parts closed
plus = [NEG] * (k + 1) # inside part j, its max already chosen
minus = [NEG] * (k + 1) # inside part j, its min already chosen
for x in a:
for j in range(k, 0, -1):
free[j] = max(free[j], plus[j] - x, minus[j] + x) # choose the other end: close part j
plus[j] = max(plus[j], free[j - 1] + x) # x is part j's max
minus[j] = max(minus[j], free[j - 1] - x) # x is part j's min
return max(free)
top = nums.index(max(nums))
start_at_max = nums[top:] + nums[:top]
end_at_max = nums[top + 1:] + nums[:top + 1]
return max(linear(start_at_max), linear(end_at_max))
What the code is doing, line by line:
- Any
+x − yinside one part is at most its range; choosing its max and min gives exactly the range. - Elements not chosen just belong to some neighbouring part, so the parts stay contiguous.
jgoes downwards so thatfree[j − 1]is still the previous element's value when it's used (and closing uses the oldplus/minus).- Opening and closing on the same element only gives 0, which never helps, so it's harmless.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
k = 1→ max − min.- All equal → 0.
- The best part wraps around the end.
Complexity: O(n · k) time, O(k) space.
Hard 3753. Total Waviness of Numbers in Range II Google Amazon Microsoft
Problem: Same as Range I but with num2 up to 10¹⁵: sum the peaks and valleys of every number in [num1, num2].
num1 = 120, num2 = 1303num1 = 198, num2 = 2023Constraints: 1 ≤ num1 ≤ num2 ≤ 10¹⁵
Hint 1
Count over [1, N] and subtract.
Hint 2
Build numbers digit by digit. What do you need to remember to recognise a peak? The last two digits.
Hint 3
Return both how many numbers a state completes and their total waviness.
From brute force to the pattern: Range I's scan doesn't scale to 10¹⁵; the digit DP has about 10⁴ states.
Spot it: Too many numbers to scan → digit DP: build numbers digit by digit under the bound. A peak or valley is decided when the digit after it is placed, so the state needs the last two digits. Each state returns how many numbers it completes and their total waviness, so a new peak counts once per completed number.
Why not another pattern? Scanning 10¹⁵ numbers is impossible.
Approach:
f(N)= total waviness of1..N; answerf(num2) − f(num1 − 1).go(pos, p2, p1, tight, length)→(count, waviness);length(capped at 2) says how many real digits precede.- Placing digit
dwithlength == 2:p1is a peak/valley ifp2 < p1 > dorp2 > p1 < d; addcount_of_restfor it.
from functools import lru_cache
class Solution:
def totalWaviness(self, num1: int, num2: int) -> int:
def total_up_to(N):
if N <= 0:
return 0
digits = list(map(int, str(N)))
@lru_cache(maxsize=None)
def go(pos, p2, p1, tight, length): # returns (numbers completed, their total waviness)
if pos == len(digits):
return 1, 0
count = wave = 0
top = digits[pos] if tight else 9
for d in range(top + 1):
if length == 0 and d == 0:
c, w = go(pos + 1, 0, 0, tight and d == top, 0) # still a leading zero
else:
c, w = go(pos + 1, p1, d, tight and d == top, min(length + 1, 2))
if length == 2 and (p2 < p1 > d or p2 > p1 < d):
w += c # p1 is a peak or valley in every number completed below
count += c
wave += w
return count, wave
return go(0, 0, 0, True, 0)[1]
return total_up_to(num2) - total_up_to(num1 - 1)
What the code is doing, line by line:
lengthtracks how many real digits are already placed (0, 1 or 2+), which separates leading zeros from real zeros.- When the third real digit arrives, the middle one (
p1) can be judged: a peak or valley adds 1 to each of thecnumbers that continue from here. tightkeeps the number ≤ N; memoisation on the state makes it fast (16 positions × 100 digit pairs × 2 × 3).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Leading zeros must not create fake valleys.
num1 = 1.- Equal adjacent digits.
Complexity: O(D · 100 · 10) states × transitions for D ≤ 16 digits: about 10⁵ steps.
Hard 3962. Maximum Subarray Sum After at Most K Swaps Google
Problem: With at most k swaps of any two elements, return the largest possible subarray sum.
nums = [1,-1,0,2], k = 13nums = [4,3,2,4], k = 213Constraints: 1 ≤ nums.length ≤ 1500 · -10⁵ ≤ nums[i] ≤ 10⁵ · 0 ≤ k ≤ nums.length
Hint 1
For one fixed window, which swaps are worth doing?
Hint 2
The window's best sum is the largest L values among (window ∪ best k outside values).
Hint 3
Fix the left end and extend the right end: how does that combined set change at each step?
From brute force to the pattern: Evaluating each of the O(n²) windows with sorting is O(n³ log n); the incremental set with heaps is O(n² log n).
Spot it: For a fixed window of length L, the best is to swap its smallest values for the largest values outside: equivalently, take the largest L values of (window ∪ top-k outside). Fix the left end and grow the window; that combined set only ever gains one element per step, so a heap of its smallest m values maintains the answer incrementally.
Why not another pattern? Recomputing the best swaps for each of the O(n²) windows from scratch costs O(n) each, O(n³) overall.
Key insight: Let O be the values outside the window and U = window ∪ (top min(k, |O|) of O). The answer for the window is U's sum minus its smallest min(k, |O|) values. When nums[r] moves inside, U gains exactly one value (either nums[r], or the next-best outside value if nums[r] was already among the top outside ones).
Approach:
- For each left end
l:out= all values sorted (window empty);U= topm = min(k, n)values;small= max-heap ofU's smallestm(all ofU). - Grow
r: ifnums[r]is among the topmofout, remove it and, ifoutstill has ≥kvalues, the newout[−k]joinsU; elsenums[r]joinsU. - Keep
smallholding the smallestmofU(pop one whenmshrinks). The window's answer issum(U) − sum(small).
import heapq
from bisect import bisect_left
class Solution:
def maxSum(self, nums: List[int], k: int) -> int:
n = len(nums)
ordered = sorted(nums)
best = float("-inf")
def add(v): # U gains v; keep the m smallest in the heap
nonlocal total, small_sum
total += v
if m and v < -small[0]:
small_sum += v + heapq.heapreplace(small, -v) # v replaces the largest of the small set
for l in range(n):
out = ordered[:] # values outside the window (window is empty)
m = min(k, n) # outside values that may be swapped in
total = sum(out[n - m:]) # sum of U = window + top m outside
small = [-v for v in out[n - m:]] # max-heap of U's smallest m values
heapq.heapify(small)
small_sum = total
for r in range(l, n):
x = nums[r]
was_top = len(out) - m <= bisect_left(out, x) # x is among the m largest outside
del out[bisect_left(out, x)] # x moves into the window
if was_top:
if len(out) >= k:
add(out[len(out) - k]) # the next best outside value joins U
else:
m -= 1 # fewer outside values left: one less swap possible
small_sum += heapq.heappop(small) # drop the largest of the small set
else:
add(x)
best = max(best, total - small_sum) # top L of U
return best
What the code is doing, line by line:
- Swapping a window value with an outside value only helps if the outside one is larger; the best swaps pair the window's smallest with the outside's largest.
- That equals the sum of the
Llargest values ofU(window plus the topmin(k, |O|)outside values) —Uminus its smallestmvalues. was_topuses the sortedoutlist:xis in the topmif its position is at leastlen(out) − m(ties are interchangeable).smallis a max-heap (stored negated) of themsmallest values ofU;heapreplaceswaps in a smaller value, and a pop handlesmshrinking.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
k = 0(plain maximum subarray).- All negative (take the single largest value).
k≥ window length.
Complexity: O(n² log n) time (plus O(n) list deletions), O(n) space.
Medium 279. Perfect Squares Meta Amazon Microsoft
Problem: Return the least number of perfect squares that sum to n.
n = 123n = 132Constraints: 1 ≤ n ≤ 10⁴
Hint 1
It's Coin Change where the coins are perfect squares.
Hint 2
Why does greedy fail for 12?
From brute force to the pattern: Recursion over all decompositions is exponential; the DP is O(n√n).
Spot it: Coin Change with coins 1, 4, 9, … → DP: the last square used is any s² ≤ x, leaving x − s².
Why not another pattern? Greedy (largest square first) fails: 12 = 9 + 1 + 1 + 1 (4 squares) vs 4 + 4 + 4 (3).
Approach:
least[0] = 0.- For
xin1..n:least[x] = 1 + min(least[x − s²])overs² ≤ x. - Return
least[n].
class Solution:
def numSquares(self, n: int) -> int:
squares = [s * s for s in range(1, int(n ** 0.5) + 1)]
least = [0] + [n] * n # least[x]: fewest squares summing to x
for x in range(1, n + 1):
for sq in squares:
if sq > x:
break
least[x] = min(least[x], least[x - sq] + 1) # use sq as the last square
return least[n]
What the code is doing, line by line:
- Each
xtries every square as the last piece, like coin change. nitself is an upper bound (all 1s).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
nitself a square → 1.n = 12→ 3.n = 7→ 4.
Complexity: O(n √n) time, O(n) space. (Lagrange's four-square theorem gives an O(√n) math solution.)
Medium 357. Count Numbers with Unique Digits Meta Microsoft
Problem: Count numbers 0 ≤ x < 10ⁿ whose digits are all distinct.
n = 291n = 01Constraints: 0 ≤ n ≤ 8
Hint 1
Count numbers of each length separately.
Hint 2
How many choices for the first digit? For the second?
From brute force to the pattern: Testing every number below 10ⁿ is O(10ⁿ · n); the product formula is O(n).
Spot it: Count by length (a simple digit DP / combinatorics): a k-digit number has 9 choices for its first digit (no 0), then 9, 8, 7, … for the rest. Add the counts for k = 1..n, plus 1 for zero.
Why not another pattern? Checking every number below 10⁸ is too slow.
Approach:
total = 1(the number 0),count = 9(one-digit numbers 1–9).- For
k = 1..n: addcount; thencount *= 10 − kfor the next length.
class Solution:
def countNumbersWithUniqueDigits(self, n: int) -> int:
total = 1 # the number 0
count = 9 # 1-digit numbers 1..9
for k in range(1, n + 1):
total += count # numbers with exactly k distinct digits
count *= 10 - k # next digit: any of the 10 - k unused digits
return total
What the code is doing, line by line:
- For length
k: 9 options for the first digit, then9, 8, …for the next ones. - Beyond 10 digits the count becomes 0 automatically (factor
10 − 10).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 0→ 1.n = 1→ 10.n = 2→ 91.
Complexity: O(n) time, O(1) space.
Medium 1871. Jump Game VII Meta Amazon
Problem: From index i you may jump to j with i + minJump ≤ j ≤ i + maxJump and s[j] == '0'. Can you reach the last index?
s = "011010", minJump = 2, maxJump = 3trues = "01101110", minJump = 2, maxJump = 3falseConstraints: 2 ≤ s.length ≤ 10⁵ · s[i] is either '0' or '1'. · s[0] == '0' · 1 ≤ minJump ≤ maxJump < s.length
Hint 1
Write the DP: reach[i] depends on a range of earlier indices.
Hint 2
Keep the number of reachable indices in that range as a sliding window.
From brute force to the pattern: BFS or DP scanning the range per index is O(n · range); the window count is O(n).
Spot it: reach[i] depends on whether any earlier index in a fixed-width range is reachable → DP, where the "any in range" test is a sliding-window count of reachable indices, updated in O(1) per step.
Why not another pattern? Checking the whole range for each index is O(n · (maxJump − minJump)).
Approach:
reach[0] = True;window= number of reachable indices in[i − maxJump, i − minJump].- Moving to
i: addreach[i − minJump](entering), removereach[i − maxJump − 1](leaving). reach[i] = s[i] == '0' and window > 0.
class Solution:
def canReach(self, s: str, minJump: int, maxJump: int) -> bool:
n = len(s)
reach = [False] * n
reach[0] = True
window = 0 # reachable indices in [i - maxJump, i - minJump]
for i in range(1, n):
if i - minJump >= 0 and reach[i - minJump]:
window += 1 # this index can now jump to i
if i - maxJump - 1 >= 0 and reach[i - maxJump - 1]:
window -= 1 # too far back to reach i
reach[i] = s[i] == "0" and window > 0
return reach[n - 1]
What the code is doing, line by line:
- Exactly one index enters and one leaves the range per step, so the count updates in
O(1). - An index is reachable if it's
'0'and at least one reachable index can jump to it.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Last character
'1'→ false. minJump == maxJump.- Long runs of
'1'.
Complexity: O(n) time and space.
Hard 115. Distinct Subsequences Meta Amazon Microsoft
Problem: Count the distinct subsequences of s that equal t.
s = "rabbbit", t = "rabbit"3s = "babgbag", t = "bag"5Constraints: 1 ≤ s.length, t.length ≤ 1000 · s and t consist of English letters.
Hint 1
Define dp over prefixes of s and t.
Hint 2
For each character of s, you either use it for the current character of t or skip it.
From brute force to the pattern: All subsequences: O(2ⁿ). DP: O(nm).
Spot it: Two strings, prefixes → 2D DP: dp[i][j] = ways to form t[:j] from s[:i]. Each character of s is either skipped or, if it matches t[j − 1], used. Iterating j downward lets one row do it.
Why not another pattern? Enumerating subsequences of s is O(2ⁿ).
Approach:
ways = [1, 0, …, 0](one way to form the empty prefix).- For each
chins: forjfromlen(t)down to 1: ift[j − 1] == ch,ways[j] += ways[j − 1]. - Return
ways[len(t)].
class Solution:
def numDistinct(self, s: str, t: str) -> int:
ways = [1] + [0] * len(t) # ways[j]: subsequences so far equal to t[:j]
for ch in s:
for j in range(len(t), 0, -1): # right to left: use each ch at most once
if t[j - 1] == ch:
ways[j] += ways[j - 1] # use ch as the j-th character of t
return ways[len(t)]
What the code is doing, line by line:
- Skipping
chkeepsways[j]unchanged; using it adds the ways to formt[:j − 1]before it. - Going right to left reads
ways[j − 1]from before this character, like a 0/1 knapsack.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
tlonger thans→ 0.- Repeated letters (rabbbit).
tempty → 1.
Complexity: O(len(s) · len(t)) time, O(len(t)) space.
Medium 241. Different Ways to Add Parentheses Amazon
Problem: Return all results of fully parenthesising an expression of numbers and + − * (any order).
expression = "2-1-1"[0,2]expression = "2*3-4*5"[-34,-14,-10,-10,10]Constraints: 1 ≤ expression.length ≤ 20 · expression consists of digits and the operator '+', '-', and '*'. · All the integer values in the input expression are in the range [0, 99]. · The integer values in the input expression do not have a leading '-' or '+' denoting the sign.
Hint 1
Which operator is evaluated last?
Hint 2
Recursively compute all results of the left and right sides and combine them.
From brute force to the pattern: Building strings is wasteful; recursion with memo shares sub-results.
Spot it: The last operator applied splits the expression in two → divide and conquer, with memoisation because the same sub-expressions repeat.
Why not another pattern? Enumerating parenthesisations as strings and evaluating each repeats sub-expressions (Catalan many).
Approach:
ways(expr): if it's a number,[int(expr)].- For each operator at
i: combine everyainways(left)with everybinways(right). - Memoise by substring.
from functools import lru_cache
class Solution:
def diffWaysToCompute(self, expression: str) -> List[int]:
@lru_cache(maxsize=None)
def ways(expr):
if expr.isdigit():
return [int(expr)] # a plain number
results = []
for i, ch in enumerate(expr):
if ch in "+-*": # this operator is applied last
for a in ways(expr[:i]):
for b in ways(expr[i + 1:]):
results.append(a + b if ch == "+" else a - b if ch == "-" else a * b)
return results
return ways(expression)
What the code is doing, line by line:
- Every parenthesisation has a last operator; splitting there covers all of them exactly once.
- Duplicate values are kept: different groupings count separately.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single number.
- Multi-digit numbers.
- Repeated results.
Complexity: Proportional to the Catalan number of operators (the output size).
Medium 313. Super Ugly Number Amazon
Problem: Return the n-th number whose prime factors all come from primes (1 counts).
n = 12, primes = [2,7,13,19]32n = 1, primes = [2,3,5]1Constraints: 1 ≤ n ≤ 10⁵ · 1 ≤ primes.length ≤ 100 · 2 ≤ primes[i] ≤ 1000 · primes[i] is guaranteed to be a prime number. · All the values of primes are unique and sorted in ascending order.
Hint 1
How is each super ugly number formed from a smaller one?
Hint 2
Keep one pointer per prime, as in Ugly Number II.
From brute force to the pattern: Checking integers one by one is too slow; k pointers produce them in order.
Spot it: Each new number is an earlier one times some prime → DP over the sorted sequence with one pointer per prime (merging k sorted streams).
Why not another pattern? Testing each integer is far too slow; a heap works but duplicates need care.
Approach:
ugly = [1],ptr = [0] * k.- Repeat:
nxt = min(ugly[ptr[i]] * p_i); append it; advance all pointers whose product equalsnxt.
class Solution:
def nthSuperUglyNumber(self, n: int, primes: List[int]) -> int:
ugly = [1]
ptr = [0] * len(primes) # ptr[i]: smallest ugly number not yet multiplied by primes[i]
while len(ugly) < n:
nxt = min(ugly[ptr[i]] * p for i, p in enumerate(primes))
ugly.append(nxt)
for i, p in enumerate(primes):
if ugly[ptr[i]] * p == nxt:
ptr[i] += 1 # advance every stream that produced it (no duplicates)
return ugly[-1]
What the code is doing, line by line:
- Every super ugly number > 1 is a smaller one times one of the primes.
- Advancing all matching pointers skips duplicates like 2·7 = 7·2.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- n = 1 → 1.
- One prime.
- Products that coincide.
Complexity: O(n · k) time, O(n + k) space.
Medium 376. Wiggle Subsequence Amazon
Problem: Return the length of the longest subsequence whose differences strictly alternate in sign.
nums = [1,7,4,9,2,5]6nums = [1,17,5,10,13,15,10,5,16,8]7Constraints: 1 ≤ nums.length ≤ 1000 · 0 ≤ nums[i] ≤ 1000
Hint 1
Track the best subsequence ending in an up step and in a down step.
Hint 2
A rise extends a sequence that ended with a fall.
From brute force to the pattern: All subsequences: exponential; LIS-style O(n²); two states: O(n).
Spot it: DP with two states per position: longest wiggle ending in a rise (up) and in a fall (down). A rise extends a fall and vice versa.
Why not another pattern? Trying all subsequences is exponential; the O(n²) LIS-style DP is unnecessary.
Approach:
up = down = 1.- Rise:
up = down + 1. Fall:down = up + 1. Equal: nothing. - Return
max(up, down).
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
up = down = 1 # longest wiggle ending with a rise / a fall
for a, b in zip(nums, nums[1:]):
if b > a:
up = down + 1 # a rise must follow a fall
elif b < a:
down = up + 1 # a fall must follow a rise
return max(up, down)
What the code is doing, line by line:
- Only the latest best
upanddownmatter, because extending with the newest element is never worse. - Equal neighbours don't change anything.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All equal → 1.
- Two elements.
- Plateaus.
Complexity: O(n) time, O(1) space.
Medium 474. Ones and Zeroes Amazon
Problem: Pick the largest subset of binary strings using at most m zeros and n ones in total.
strs = ["10","0001","111001","1","0"], m = 5, n = 34strs = ["10","0","1"], m = 1, n = 12Constraints: 1 ≤ strs.length ≤ 600 · 1 ≤ strs[i].length ≤ 100 · strs[i] consists only of digits '0' and '1'. · 1 ≤ m, n ≤ 100
Hint 1
It's a knapsack. What are the weights and the capacity?
Hint 2
Use two capacity dimensions.
From brute force to the pattern: All subsets are exponential; the knapsack is O(L · m · n).
Spot it: Each string is an item with two weights → a 2D 0/1 knapsack maximising the count.
Why not another pattern? All subsets: 2^L.
Approach:
dp[i][j]= most strings with ≤ i zeros and ≤ j ones.- For each string with
zzeros andoones: forifrommdown toz,jfromndown too:dp[i][j] = max(dp[i][j], dp[i − z][j − o] + 1).
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
dp = [[0] * (n + 1) for _ in range(m + 1)] # dp[i][j]: most strings within i zeros, j ones
for s in strs:
z, o = s.count("0"), s.count("1")
for i in range(m, z - 1, -1): # downwards: each string used at most once
for j in range(n, o - 1, -1):
dp[i][j] = max(dp[i][j], dp[i - z][j - o] + 1)
return dp[m][n]
What the code is doing, line by line:
- Iterating capacities from high to low reads the previous item's values, so each string is used once.
- Two capacity dimensions instead of one is the only change from the classic knapsack.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Strings that never fit.
- m = 0 or n = 0.
- Duplicate strings.
Complexity: O(L · m · n) time, O(m · n) space.
Medium 1014. Best Sightseeing Pair Amazon
Problem: Maximise values[i] + values[j] + i − j over i < j.
values = [8,1,5,2,6]11values = [1,2]2Constraints: 2 ≤ values.length ≤ 5 * 10⁴ · 1 ≤ values[i] ≤ 1000
Hint 1
Rewrite the score as (something about i) + (something about j).
Hint 2
Keep the best i-part seen so far.
From brute force to the pattern: All pairs O(n²) vs one pass.
Spot it: Split the score into a part for i and a part for j. Scan j left to right keeping the best values[i] + i so far → a one-variable DP.
Why not another pattern? All pairs: O(n²).
Approach:
best_i = values[0].- For
j ≥ 1: answer = max(answer, best_i + values[j] − j); thenbest_i = max(best_i, values[j] + j).
class Solution:
def maxScoreSightseeingPair(self, values: List[int]) -> int:
best_i = values[0] + 0 # best values[i] + i among earlier spots
answer = 0
for j in range(1, len(values)):
answer = max(answer, best_i + values[j] - j)
best_i = max(best_i, values[j] + j) # j can be an i for later spots
return answer
What the code is doing, line by line:
- The score separates into a term depending only on
iand one only onj. - Updating
best_iafter using it keepsi < j.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Two spots.
- Best pair far apart.
- Decreasing values.
Complexity: O(n) time, O(1) space.
Medium 1594. Maximum Non Negative Product in a Matrix Amazon Microsoft
Problem: Moving right/down from top-left to bottom-right, maximise the product of the path. Return it mod 10⁹ + 7, or −1 if it's negative.
grid = [[-1,-2,-3],[-2,-3,-3],[-3,-3,-2]]-1grid = [[1,-2,1],[1,-2,1],[3,-4,1]]8Constraints: m == grid.length · n == grid[i].length · 1 ≤ m, n ≤ 15 · -4 ≤ grid[i][j] ≤ 4
Hint 1
Why isn't the maximum product alone enough?
Hint 2
Keep the largest and smallest product for each cell.
From brute force to the pattern: All paths are exponential; max/min DP is O(mn).
Spot it: Like Maximum Product Subarray on a grid: a negative number turns the smallest product into the largest → grid DP with max and min per cell.
Why not another pattern? Tracking only the maximum fails when two negatives multiply.
Approach:
hi[r][c],lo[r][c]= largest / smallest product to reach the cell.- From the top and left neighbours: candidates
x · hi,x · lo. - Answer
hi[end] % MODif ≥ 0, else −1.
class Solution:
def maxProductPath(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
hi = [[0] * n for _ in range(m)] # largest product reaching (r, c)
lo = [[0] * n for _ in range(m)] # smallest product reaching (r, c)
for r in range(m):
for c in range(n):
x = grid[r][c]
if r == 0 and c == 0:
hi[r][c] = lo[r][c] = x
continue
prev = []
if r:
prev += [hi[r - 1][c], lo[r - 1][c]]
if c:
prev += [hi[r][c - 1], lo[r][c - 1]]
products = [x * p for p in prev]
hi[r][c], lo[r][c] = max(products), min(products) # a negative x flips them
best = hi[m - 1][n - 1]
return best % (10**9 + 7) if best >= 0 else -1
What the code is doing, line by line:
- Multiplying by a negative maps the smallest product to the largest, so both extremes are needed.
- The modulo is applied only at the end, after comparing true values.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A zero on the path (0 is non-negative).
- Odd number of negatives → −1.
- 1×1 grid.
Complexity: O(m · n) time and space.
Medium 2770. Maximum Number of Jumps to Reach the Last Index Amazon
Problem: Jump forward from i to j if |nums[j] − nums[i]| ≤ target. Return the maximum number of jumps from 0 to the last index, or −1.
nums = [1,3,6,4,1,2], target = 23nums = [1,3,6,4,1,2], target = 35Constraints: 2 ≤ nums.length == n ≤ 1000 · -10⁹ ≤ nums[i] ≤ 10⁹ · 0 ≤ target ≤ 2 * 10⁹
Hint 1
Jumps only go forward. What order should you compute answers in?
Hint 2
dp[j] = 1 + the best dp[i] that can jump to j.
From brute force to the pattern: Enumerating paths is exponential; the DP is O(n²) (n ≤ 1000).
Spot it: Most jumps along a forward-only path → DP over indices (like LIS): dp[j] = most jumps to reach j.
Why not another pattern? BFS gives the fewest jumps, not the most; DFS over all paths is exponential.
Approach:
dp[0] = 0, others −∞.- For each
j, for eachi < jwith reachabledp[i]and|nums[j] − nums[i]| ≤ target:dp[j] = max(dp[j], dp[i] + 1). - Return
dp[n − 1]or −1.
class Solution:
def maximumJumps(self, nums: List[int], target: int) -> int:
n = len(nums)
NEG = float("-inf")
dp = [NEG] * n # most jumps to reach index j
dp[0] = 0
for j in range(1, n):
for i in range(j):
if dp[i] != NEG and abs(nums[j] - nums[i]) <= target:
dp[j] = max(dp[j], dp[i] + 1) # jump i -> j
return dp[-1] if dp[-1] != NEG else -1
What the code is doing, line by line:
- Jumps only go forward, so indices are already in topological order.
- Unreachable indices stay −∞.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Last index unreachable → −1.
- target = 0 (only equal values).
- Two elements.
Complexity: O(n²) time, O(n) space.
Medium 3418. Maximum Amount of Money Robot Can Earn Amazon Microsoft
Problem: Move right/down collecting coins; negative cells are robbers, and up to 2 of them on the path can be neutralised. Maximise the total.
coins = [[0,1,-1],[1,-2,3],[2,-3,4]]8coins = [[10,10,10],[10,10,10]]40Constraints: m == coins.length · n == coins[i].length · 1 ≤ m, n ≤ 500 · -1000 ≤ coins[i][j] ≤ 1000
Hint 1
Add the number of neutralisations used to the DP state.
Hint 2
At a negative cell, either take the loss or spend one neutralisation.
From brute force to the pattern: All paths × choices is exponential; the 3-layer DP is O(mn).
Spot it: Grid paths with a small budget of special moves → 3D DP: (row, col, neutralisations used).
Why not another pattern? Choosing which robbers to neutralise after picking a path isn't possible without knowing the path; the budget must be part of the state.
Approach:
dp[r][c][k]= best total at(r, c)having usedkneutralisations.- From up/left with the same
k: add the cell value; if negative andk > 0, also fromk − 1adding 0. - Answer
max(dp[end][k]).
class Solution:
def maximumAmount(self, coins: List[List[int]]) -> int:
m, n = len(coins), len(coins[0])
NEG = float("-inf")
dp = [[[NEG] * 3 for _ in range(n)] for _ in range(m)] # dp[r][c][k]: k robbers neutralised
for r in range(m):
for c in range(n):
x = coins[r][c]
for k in range(3):
if r == 0 and c == 0:
best_before = 0 if k == 0 else NEG
prev_k1 = 0 if k == 1 else NEG
else:
best_before = max(dp[r - 1][c][k] if r else NEG, dp[r][c - 1][k] if c else NEG)
prev_k1 = max(dp[r - 1][c][k - 1] if r and k else NEG, dp[r][c - 1][k - 1] if c and k else NEG)
take = best_before + x # keep this cell as is
skip = prev_k1 if x < 0 else NEG # neutralise this robber
dp[r][c][k] = max(take, skip)
return max(dp[m - 1][n - 1])
What the code is doing, line by line:
kcounts robbers neutralised so far; neutralising this cell moves fromk − 1tokand adds 0.- The start cell can also be a robber, hence the special case.
- Neutralising is only offered on negative cells.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Robber at the start or end.
- No robbers (plain max path).
- More than two robbers.
Complexity: O(m · n · 3) time and space.
Medium 3434. Maximum Frequency After Subarray Operation Amazon
Problem: Add one value x to every element of a chosen subarray once. Return the maximum possible frequency of k.
nums = [1,2,3,4,5,6], k = 12nums = [10,2,3,4,5,5,4,3,2,2], k = 104Constraints: 1 ≤ n == nums.length ≤ 10⁵ · 1 ≤ nums[i] ≤ 50 · 1 ≤ k ≤ 50
Hint 1
Which values can become k after one +x on a subarray?
Hint 2
For a fixed value v, the gain on a subarray is (#v) − (#k): maximum subarray.
From brute force to the pattern: All subarrays and shifts: O(50 n²); Kadane per value: O(50 n).
Spot it: Choosing x = k − v turns every v in the subarray into k, but breaks the ks already there. For each v, the best subarray maximises #v − #k → Kadane's maximum subarray. Values are ≤ 50, so try each v.
Why not another pattern? All subarrays × all x is O(n² · 50).
Approach:
base = count(k).- For each value
v ≠ k: Kadane over+1(v),−1(k), 0 otherwise; best ≥ 0. - Answer
base + max gain.
class Solution:
def maxFrequency(self, nums: List[int], k: int) -> int:
base = nums.count(k)
best_gain = 0
for v in set(nums):
if v == k:
continue
cur = 0
for x in nums:
if x == v:
cur += 1 # becomes k
elif x == k:
cur -= 1 # stops being k
cur = max(cur, 0) # start over if negative
best_gain = max(best_gain, cur)
return base + best_gain
What the code is doing, line by line:
- Inside the subarray only one value can be turned into
k(one fixedx). - Kadane finds the subarray where the gain
#v − #kis largest.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already all k.
- No k present.
- Several candidate values.
Complexity: O(50 · n) time, O(1) space.
Medium 3857. Minimum Cost to Split into Ones Amazon
Problem: Splitting x into a + b costs a · b. Return the minimum total cost to split n into n ones.
n = 33n = 46Constraints: 1 ≤ n ≤ 500
Hint 1
Try small n by hand. Does the order of splits matter?
Hint 2
Think of the n units as people; what does each split's cost count?
From brute force to the pattern: The DP is O(n²); the counting argument gives O(1).
Spot it: The natural formulation is a DP: dp[x] = min(a · (x − a) + dp[a] + dp[x − a]). Working it out shows every split order costs the same: each pair of the final ones is "separated" exactly once, so the total is n(n − 1)/2.
Why not another pattern? Nothing smarter than the formula is needed once you see the invariant; the DP confirms it.
Approach:
- Return
n · (n − 1) / 2(the DP gives the same value).
class Solution:
def minCost(self, n: int) -> int:
dp = [0] * (n + 1) # dp[x]: cost to split x into ones
for x in range(2, n + 1):
dp[x] = min(a * (x - a) + dp[a] + dp[x - a] for a in range(1, x // 2 + 1))
return dp[n] # always x * (x - 1) // 2
What the code is doing, line by line:
- Splitting
a + bcostsa · b, the number of pairs with one unit on each side. - Every pair of units is separated by exactly one split, so the total is the number of pairs
n(n − 1)/2.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- n = 1 → 0.
- n = 2 → 1.
- Large n.
Complexity: O(n²) for the DP (O(1) with the formula).
Hard 472. Concatenated Words Amazon
Problem: Return every word that is a concatenation of at least two other words from the list (any order).
words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]["catsdogcats","dogcatsdog","ratcatdogcat"]words = ["cat","dog","catdog"]["catdog"]Constraints: 1 ≤ words.length ≤ 10⁴ · 1 ≤ words[i].length ≤ 30 · words[i] consists of only lowercase English letters. · All the strings of words are unique. · 1 ≤ sum(words[i].length) ≤ 10⁵
Hint 1
Which words can be pieces of a given word?
Hint 2
Sort by length and run Word Break against the shorter words.
From brute force to the pattern: Trying combinations of words is exponential; per-word DP is polynomial.
Spot it: A word can only be built from shorter words. Process words by length and run Word Break's DP against the set of shorter words; add each word to the set afterwards.
Why not another pattern? Checking every pair or triple of words is exponential in the number of pieces.
Approach:
- Sort words by length;
known = set(). - For each word:
ok[0] = True;ok[j] = any(ok[i] and word[i:j] in known). - If
ok[len], it's concatenated. Add the word toknown.
class Solution:
def findAllConcatenatedWordsInADict(self, words: List[str]) -> List[str]:
known = set()
result = []
for word in sorted(words, key=len): # pieces are always shorter
if word:
ok = [True] + [False] * len(word) # ok[j]: word[:j] splits into known words
for j in range(1, len(word) + 1):
for i in range(j):
if ok[i] and word[i:j] in known:
ok[j] = True
break
if ok[-1]:
result.append(word) # built from at least two shorter words
known.add(word)
return result
What the code is doing, line by line:
- Because
knownholds only shorter words, a successful split always uses at least two pieces. - It's the Word Break DP per word.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Empty string in the list.
- Words built from repeated pieces (catcat).
- No concatenated words.
Complexity: O(Σ L³) worst case (slicing), usually much faster.
Hard 689. Maximum Sum of 3 Non-Overlapping Subarrays Amazon
Problem: Pick three non-overlapping subarrays of length k with the maximum total; return their start indices (lexicographically smallest on ties).
nums = [1,2,1,2,6,7,5,1], k = 2[0,3,5]nums = [1,2,1,2,1,2,1,2,1], k = 2[0,2,4]Constraints: 1 ≤ nums.length ≤ 2 * 10⁴ · 1 ≤ nums[i] < 2¹⁶ · 1 ≤ k ≤ floor(nums.length / 3)
Hint 1
Precompute all window sums.
Hint 2
Fix the middle window; what are the best choices for the other two?
From brute force to the pattern: All triples: O(n³); prefix/suffix best indices: O(n).
Spot it: Compute every window sum (sliding window / prefix sums). For each middle window, the best left window must end before it and the best right one start after it: prefix and suffix DP of the best window index.
Why not another pattern? Trying all triples of windows is O(n³).
Approach:
W[i]= sum ofnums[i..i+k−1].left[i]= index of the best window in[0, i](earliest on ties);right[i]= best in[i, end](earliest on ties).- For middle
j: totalW[left[j−k]] + W[j] + W[right[j+k]].
class Solution:
def maxSumOfThreeSubarrays(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
W = [sum(nums[:k])]
for i in range(k, n):
W.append(W[-1] + nums[i] - nums[i - k]) # window sums
m = len(W)
left = [0] * m # left[i]: best window start in [0, i]
for i in range(1, m):
left[i] = i if W[i] > W[left[i - 1]] else left[i - 1] # strict: keep the earliest
right = [m - 1] * m # right[i]: best window start in [i, m - 1]
for i in range(m - 2, -1, -1):
right[i] = i if W[i] >= W[right[i + 1]] else right[i + 1] # >=: prefer the earlier one
best = None
for j in range(k, m - k): # middle window
a, c = left[j - k], right[j + k]
total = W[a] + W[j] + W[c]
if best is None or total > best[0]:
best = (total, [a, j, c])
return best[1]
What the code is doing, line by line:
leftandrightare prefix/suffix maxima of window sums, storing indices.- Tie rules (strict on the left,
≥on the right, strict for the middle) give the lexicographically smallest answer. - The middle window leaves room for a full window on each side.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Ties (earliest indices).
- n = 3k (only one choice).
- Equal values everywhere.
Complexity: O(n) time and space.
Hard 805. Split Array With Same Average Amazon
Problem: Split nums into two non-empty parts with the same average.
nums = [1,2,3,4,5,6,7,8]truenums = [3,1]falseConstraints: 1 ≤ nums.length ≤ 30 · 0 ≤ nums[i] ≤ 10⁴
Hint 1
If both parts have the same average, what is that average?
Hint 2
For a part of size k, what must its sum be?
Hint 3
Subset-sum DP over (size, sum).
From brute force to the pattern: All subsets: 2ⁿ; meet in the middle: 2^(n/2); size-indexed bitsets: polynomial in n and the sum.
Spot it: Both parts have the overall average, so a subset A of size k must sum to total · k / n. That's a subset-sum DP indexed by size; Python integers as bitsets make each size's reachable sums one shift-or per number.
Why not another pattern? Trying all 2ⁿ subsets is 10⁹ for n = 30.
Approach:
reach[k]= bitset of sums of size-ksubsets;reach[0] = 1.- For each number
x, forkdownwards:reach[k + 1] |= reach[k] << x. - Check any
1 ≤ k ≤ n/2withtotal·k % n == 0and that bit set.
class Solution:
def splitArraySameAverage(self, nums: List[int]) -> bool:
n, total = len(nums), sum(nums)
half = n // 2
if not any(total * k % n == 0 for k in range(1, half + 1)):
return False # no subset size can have an integer target sum
reach = [0] * (half + 1) # reach[k]: bit s set if some k numbers sum to s
reach[0] = 1
for x in nums:
for k in range(half - 1, -1, -1): # downwards: each number used once
reach[k + 1] |= reach[k] << x
return any(total * k % n == 0 and reach[k] >> (total * k // n) & 1 for k in range(1, half + 1))
What the code is doing, line by line:
- If a part has the overall average, the other part does too.
- The smaller part has at most
n/2elements, so only those sizes are needed. - A left shift by
xaddsxto every reachable sum at once.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- n = 1 → false.
- All equal → true (n ≥ 2).
- No integer target for any size.
Complexity: O(n² · total / w) with big-integer bit operations.
Hard 940. Distinct Subsequences II Amazon
Problem: Count distinct non-empty subsequences of s (mod 10⁹ + 7).
s = "abc"7s = "aba"6Constraints: 1 ≤ s.length ≤ 2000 · s consists of lowercase English letters.
Hint 1
Group distinct subsequences by their last character.
Hint 2
When c appears again, what are all the distinct subsequences ending in c?
From brute force to the pattern: Enumerating subsequences is exponential; the per-letter DP is linear.
Spot it: Classify distinct subsequences by their last letter. When c appears again, every distinct subsequence so far (plus the empty one) can be extended by c, and these replace the old ones ending in c → a 26-entry DP.
Why not another pattern? Enumerating subsequences is O(2ⁿ); a plain doubling count over-counts duplicates.
Approach:
ends = [0] * 26.- For each
c:ends[c] = sum(ends) + 1. - Answer
sum(ends).
class Solution:
def distinctSubseqII(self, s: str) -> int:
MOD = 10**9 + 7
ends = [0] * 26 # distinct subsequences ending with each letter
for ch in s:
ends[ord(ch) - 97] = (sum(ends) + 1) % MOD # extend everything (and the empty one) by ch
return sum(ends) % MOD
What the code is doing, line by line:
- Every subsequence ending in
cso far is also counted in the new value (extend its prefix again), so overwriting avoids duplicates. - The
+ 1is the single letterc.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All the same letter → n.
- All distinct → 2ⁿ − 1.
- Repeated patterns.
Complexity: O(26 · n) time, O(1) space.
Hard 1000. Minimum Cost to Merge Stones Amazon Microsoft
Problem: Merge exactly k consecutive piles at a time (cost = their total). Return the minimum cost to end with one pile, or −1.
stones = [3,2,4,1], k = 220stones = [3,2,4,1], k = 3-1Constraints: n == stones.length · 1 ≤ n ≤ 30 · 1 ≤ stones[i] ≤ 100 · 2 ≤ k ≤ 30
Hint 1
When is it possible at all? Count how many piles each merge removes.
Hint 2
Interval DP: split so the left part becomes one pile.
Hint 3
Add the range sum when the range can collapse to one pile.
From brute force to the pattern: All merge orders are exponential; interval DP is O(n³ / k).
Spot it: Merges of consecutive piles → interval DP. A range of length L can become one pile iff (L − 1) % (k − 1) == 0. Split the range so the left part becomes one pile; when the whole range can become one pile, pay its sum once more.
Why not another pattern? Trying merge orders is exponential.
Approach:
- If
(n − 1) % (k − 1), return −1. dp[i][j] = min(dp[i][m] + dp[m + 1][j])overmstepping byk − 1(left part reducible to one pile).- If
(j − i) % (k − 1) == 0, addsum(stones[i..j]).
class Solution:
def mergeStones(self, stones: List[int], k: int) -> int:
n = len(stones)
if (n - 1) % (k - 1):
return -1 # piles drop by k - 1 each merge
P = [0]
for x in stones:
P.append(P[-1] + x)
dp = [[0] * n for _ in range(n)] # dp[i][j]: min cost to reduce stones[i..j] as far as possible
for length in range(k, n + 1):
for i in range(n - length + 1):
j = i + length - 1
dp[i][j] = min(dp[i][m] + dp[m + 1][j] for m in range(i, j, k - 1)) # left part ends as one pile
if (j - i) % (k - 1) == 0:
dp[i][j] += P[j + 1] - P[i] # final merge of this range into one pile
return dp[0][n - 1]
What the code is doing, line by line:
- Each merge reduces the pile count by
k − 1, which gives the feasibility test. - Stepping
mbyk − 1keeps the left part reducible to exactly one pile. - The last merge of a range costs its total, added once when the range can become a single pile.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Impossible → −1.
- k = 2 (classic merge).
- n = 1 → 0.
Complexity: O(n³ / k) time, O(n²) space.
Hard 1335. Minimum Difficulty of a Job Schedule Amazon Microsoft
Problem: Do jobs in order over exactly d days (at least one per day); a day's difficulty is its hardest job. Minimise the sum, or −1.
jobDifficulty = [6,5,4,3,2,1], d = 27jobDifficulty = [9,9,9], d = 4-1Constraints: 1 ≤ jobDifficulty.length ≤ 300 · 0 ≤ jobDifficulty[i] ≤ 1000 · 1 ≤ d ≤ 10
Hint 1
Think about the last day: which jobs does it cover?
Hint 2
dp over (days, jobs done), trying every start of the last day.
From brute force to the pattern: All split choices are combinatorial; the DP is O(dn²).
Spot it: Split the sequence into d contiguous parts → partition DP over (days used, jobs done), tracking the last part's maximum while extending it.
Why not another pattern? Trying all split points is C(n − 1, d − 1).
Approach:
- If
n < d, −1. dp[k][i]= best for the firstijobs inkdays.- For the last day covering jobs
j..i−1, extendjleftwards keeping the running max.
class Solution:
def minDifficulty(self, jobDifficulty: List[int], d: int) -> int:
n = len(jobDifficulty)
if n < d:
return -1 # each day needs a job
INF = float("inf")
dp = [[INF] * (n + 1) for _ in range(d + 1)] # dp[k][i]: first i jobs in k days
dp[0][0] = 0
for k in range(1, d + 1):
for i in range(k, n + 1):
hardest = 0
for j in range(i - 1, k - 2, -1): # last day does jobs j..i-1
hardest = max(hardest, jobDifficulty[j])
if dp[k - 1][j] < INF:
dp[k][i] = min(dp[k][i], dp[k - 1][j] + hardest)
return dp[d][n]
What the code is doing, line by line:
- Extending the last day leftwards updates its maximum in
O(1). - The bound
j ≥ k − 1leaves at least one job for each earlier day.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- n < d → −1.
- d = n (one job per day).
- d = 1 (max of all).
Complexity: O(d · n²) time, O(d · n) space.
Hard 1478. Allocate Mailboxes Amazon
Problem: Place k mailboxes on a street to minimise the total distance from each house to its nearest mailbox.
houses = [1,4,8,10,20], k = 35houses = [2,3,5,12,18], k = 29Constraints: 1 ≤ k ≤ houses.length ≤ 100 · 1 ≤ houses[i] ≤ 10⁴ · All the integers of houses are unique.
Hint 1
Sort the houses. What does the set of houses served by one mailbox look like?
Hint 2
Where should a single mailbox go for a group?
Hint 3
Partition the houses into k groups with DP.
From brute force to the pattern: Searching positions is huge; median costs + partition DP is polynomial.
Spot it: Sorted houses split into k contiguous groups, each served by a mailbox at its median. Precompute each group's cost, then partition DP over prefixes and number of boxes.
Why not another pattern? Trying mailbox positions is huge; groups are always contiguous after sorting.
Approach:
- Sort;
cost[i][j]= Σ |h − median| for housesi..j. dp[b][j]= min overiofdp[b − 1][i] + cost[i][j − 1].- Return
dp[k][n].
class Solution:
def minDistance(self, houses: List[int], k: int) -> int:
h = sorted(houses)
n = len(h)
cost = [[0] * n for _ in range(n)] # one mailbox at the median of h[i..j]
for i in range(n):
for j in range(i, n):
mid = h[(i + j) // 2]
cost[i][j] = sum(abs(h[t] - mid) for t in range(i, j + 1))
INF = float("inf")
dp = [[INF] * (n + 1) for _ in range(k + 1)] # dp[b][j]: first j houses with b mailboxes
dp[0][0] = 0
for b in range(1, k + 1):
for j in range(1, n + 1):
for i in range(j): # last group is houses i..j-1
if dp[b - 1][i] < INF:
dp[b][j] = min(dp[b][j], dp[b - 1][i] + cost[i][j - 1])
return dp[k][n]
What the code is doing, line by line:
- Each mailbox serves a contiguous block of sorted houses.
- The median minimises the sum of distances within a block.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- k equals the number of houses → 0.
- k = 1.
- Clustered houses.
Complexity: O(n³ + k n²) time, O(n²) space (n ≤ 100).
Hard 1494. Parallel Courses II Amazon
Problem: With prerequisites and at most k courses per semester, return the minimum number of semesters (n ≤ 15).
n = 4, relations = [[2,1],[3,1],[1,4]], k = 23n = 5, relations = [[2,1],[3,1],[4,1],[1,5]], k = 24Constraints: 1 ≤ n ≤ 15 · 1 ≤ k ≤ n · 0 ≤ relations.length ≤ n * (n-1) / 2 · relations[i].length == 2 · 1 ≤ prevCoursei, nextCoursei ≤ n · prevCoursei != nextCoursei · All the pairs [prevCoursei, nextCoursei] are unique. · The given graph is a directed acyclic graph.
Hint 1
Why does a greedy topological order fail?
Hint 2
n ≤ 15: use the set of taken courses as the DP state.
Hint 3
Each semester takes a subset of the available courses.
From brute force to the pattern: Greedy is wrong; bitmask DP over submasks is O(3ⁿ) and exact.
Spot it: Greedy (take any k available) is wrong. With n ≤ 15, the state is the set of courses taken → bitmask DP; each semester picks a submask of the available courses of size ≤ k (or all of them if fewer).
Why not another pattern? Topological sort with greedy choices can pick the wrong courses and need extra semesters.
Approach:
pre[i]= bitmask of prerequisites.- BFS/DP over masks: available = courses not taken whose
pre ⊆ mask. - If
popcount(available) ≤ k, take all; else try every submask of sizek.
class Solution:
def minNumberOfSemesters(self, n: int, relations: List[List[int]], k: int) -> int:
pre = [0] * n
for a, b in relations:
pre[b - 1] |= 1 << (a - 1)
full = (1 << n) - 1
INF = float("inf")
dp = [INF] * (1 << n) # dp[mask]: semesters to take exactly `mask`
dp[0] = 0
for mask in range(1 << n):
if dp[mask] == INF:
continue
avail = 0
for i in range(n):
if not mask >> i & 1 and pre[i] & mask == pre[i]:
avail |= 1 << i # prerequisites done, not yet taken
if bin(avail).count("1") <= k:
dp[mask | avail] = min(dp[mask | avail], dp[mask] + 1) # take them all
continue
sub = avail
while sub:
if bin(sub).count("1") == k:
dp[mask | sub] = min(dp[mask | sub], dp[mask] + 1)
sub = (sub - 1) & avail # next submask
return dp[full]
What the code is doing, line by line:
- Taking fewer than possible when at most
kare available never helps, so all are taken. - Otherwise exactly
k(more is not allowed, fewer is never better). - Submask enumeration
(sub − 1) & availvisits every subset of the available courses.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No relations (ceil(n / k)).
- A long chain (n semesters).
- k ≥ n.
Complexity: O(3ⁿ) time in the worst case, O(2ⁿ) space.
Hard 1547. Minimum Cost to Cut a Stick Amazon Microsoft
Problem: Cutting a stick costs its current length. Make all the given cuts in the best order; return the minimum total cost.
n = 7, cuts = [1,3,4,5]16n = 9, cuts = [5,6,1,4,2]22Constraints: 2 ≤ n ≤ 10⁶ · 1 ≤ cuts.length ≤ min(n - 1, 100) · 1 ≤ cuts[i] ≤ n - 1 · All the integers in cuts array are distinct.
Hint 1
The first cut on a piece splits it into two independent problems.
Hint 2
Define dp on pairs of cut positions.
From brute force to the pattern: All cut orders are c!; interval DP is O(c³).
Spot it: After the first cut inside (p_i, p_j) the two sides are independent → interval DP over consecutive cut points, like Burst Balloons.
Why not another pattern? Trying all cut orders is factorial.
Approach:
P = [0] + sorted(cuts) + [n].dp[i][j]= min cost to make every cut strictly betweenP[i]andP[j].dp[i][j] = P[j] − P[i] + min(dp[i][k] + dp[k][j])fori < k < j; 0 if no cut between.
class Solution:
def minCost(self, n: int, cuts: List[int]) -> int:
P = [0] + sorted(cuts) + [n]
m = len(P)
dp = [[0] * m for _ in range(m)] # dp[i][j]: cost of all cuts between P[i] and P[j]
for gap in range(2, m): # need at least one cut point inside
for i in range(m - gap):
j = i + gap
dp[i][j] = P[j] - P[i] + min(dp[i][k] + dp[k][j] for k in range(i + 1, j)) # first cut at P[k]
return dp[0][m - 1]
What the code is doing, line by line:
- Whatever the first cut inside a piece is, it costs the piece's length.
- Intervals are filled by increasing width so sub-intervals are ready.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- One cut.
- Cuts at many positions.
- Unsorted cuts.
Complexity: O(c³) time, O(c²) space for c cuts.
Hard 1691. Maximum Height by Stacking Cuboids Amazon
Problem: Stack cuboids (rotations allowed) where each one's width, length and height are ≤ those below. Maximise the total height.
cuboids = [[50,45,20],[95,37,53],[45,23,12]]190cuboids = [[38,25,45],[76,35,3]]76Constraints: n == cuboids.length · 1 ≤ n ≤ 100 · 1 ≤ widthi, lengthi, heighti ≤ 100
Hint 1
Which side should be the height?
Hint 2
After normalising each cuboid, it's a longest-chain (LIS) problem.
From brute force to the pattern: All rotations and orders are exponential; LIS-style DP is O(n²).
Spot it: Sorting each cuboid's dimensions and using the largest as height is always optimal. Then sort the cuboids; stacking is a chain where each dimension is non-decreasing → LIS-style DP.
Why not another pattern? Trying rotations and orders is exponential.
Approach:
- Sort each cuboid's three numbers; sort the list.
dp[i] = c[i][2] + max(dp[j])forj < iwith all three dims ofj≤ those ofi.- Return
max(dp).
class Solution:
def maxHeight(self, cuboids: List[List[int]]) -> int:
boxes = sorted(sorted(c) for c in cuboids) # each box: small, medium, large (= height)
dp = [0] * len(boxes) # tallest stack with box i on top
for i, (a, b, c) in enumerate(boxes):
dp[i] = c
for j in range(i):
if boxes[j][0] <= a and boxes[j][1] <= b and boxes[j][2] <= c:
dp[i] = max(dp[i], dp[j] + c) # box j can sit below box i
return max(dp)
What the code is doing, line by line:
- If one cuboid fits on another in some orientation, it also fits with both sorted (a known exchange argument), and sorted gives the tallest height.
- After sorting the list, a box can only sit on earlier boxes.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Identical cuboids.
- One cuboid.
- None fit on each other.
Complexity: O(n²) time, O(n) space.
Hard 2262. Total Appeal of A String Amazon
Problem: The appeal of a string is its number of distinct letters. Sum the appeal of all substrings.
s = "abbca"28s = "code"20Constraints: 1 ≤ s.length ≤ 10⁵ · s consists of lowercase English letters.
Hint 1
Instead of computing each substring's appeal, count each character's contribution.
Hint 2
When is the character at i the first of its letter in a substring?
From brute force to the pattern: All substrings: O(n²); contribution counting: O(n).
Spot it: Count each letter once per substring by crediting its first occurrence in that substring. Occurrence i is first for substrings starting after the previous same letter and ending anywhere at or after i → a counting DP with last positions.
Why not another pattern? Computing each substring's distinct count is O(n²) or worse.
Approach:
last[c] = −1.- For each
i: add(i − last[c]) · (n − i); setlast[c] = i.
class Solution:
def appealSum(self, s: str) -> int:
n = len(s)
last = {} # letter -> its previous index
total = 0
for i, ch in enumerate(s):
starts = i - last.get(ch, -1) # substrings where this is the first ch
total += starts * (n - i) # times the possible end positions
last[ch] = i
return total
What the code is doing, line by line:
- A substring
[l, r]counts lettercexactly once, at its first occurrence inside. - That occurrence is
iwhenlast[c] < l ≤ i ≤ r.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All the same letter.
- All distinct.
- Single character.
Complexity: O(n) time, O(26) space.
Hard 2272. Substring With Largest Variance Amazon
Problem: The variance of a string is the largest difference between counts of two letters present in it. Return the largest variance over all substrings.
s = "aababbb"3s = "abcde"0Constraints: 1 ≤ s.length ≤ 10⁴ · s consists of lowercase English letters.
Hint 1
Fix which letter is most frequent and which is least.
Hint 2
Then it's a maximum-subarray problem with +1 and −1, but the window must contain the rare letter.
From brute force to the pattern: All substrings with counts: O(26 n²); Kadane per letter pair: O(26² n).
Spot it: Fix the pair: letter a should be frequent and b rare. With a → +1, b → −1, others 0, it's maximum subarray (Kadane), with the extra rule that the substring must contain at least one b.
Why not another pattern? All substrings × letter counts is O(26 n²).
Approach:
- For each ordered pair
a ≠ b(both present): - Kadane with
cur,has_b, andstarted_with_btracking; whencurgoes negative, restart (but remember abwas just seen). - Update the answer only when the window contains a
b.
class Solution:
def largestVariance(self, s: str) -> int:
letters = set(s)
best = 0
for a in letters:
for b in letters:
if a == b:
continue
cur = 0 # (#a - #b) in the current window
has_b = False # does the window contain a b?
restarted_on_b = False # a b was dropped by the last restart (can be taken back)
for ch in s:
if ch == a:
cur += 1
elif ch == b:
has_b = True
if restarted_on_b and cur >= 0:
restarted_on_b = False # keep the previous b instead of this one
else:
cur -= 1
if cur < 0:
cur = -1 # restart at this b
restarted_on_b = True
if has_b:
best = max(best, cur)
return best
What the code is doing, line by line:
- With
aas +1 andbas −1, the window value iscount(a) − count(b). - A window must contain a
b(otherwiseb's count is 0 and it isn't present); thehas_bflag enforces that. - Restarting at a
bkeeps thatbin the window (value −1) so lateras still count with onebpresent.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All one letter → 0.
- Two letters only.
- The rare letter at the very start.
Complexity: O(26² · n) time, O(1) space.
Hard 2742. Painting the Walls Amazon
Problem: A paid painter takes time[i] and costs cost[i] for wall i; a free painter paints any wall in 1 unit but only while the paid one is busy. Minimise cost to paint all walls.
cost = [1,2,3,2], time = [1,2,3,2]3cost = [2,3,4,2], time = [1,1,1,1]4Constraints: 1 ≤ cost.length ≤ 500 · cost.length == time.length · 1 ≤ cost[i] ≤ 10⁶ · 1 ≤ time[i] ≤ 500
Hint 1
While the paid painter works on wall i, how many walls can the free painter finish?
Hint 2
Think of each paid wall as covering 1 + time[i] walls: a knapsack.
From brute force to the pattern: Choosing subsets of paid walls is O(2ⁿ); the knapsack is O(n²).
Spot it: While the paid painter spends time[i] on a wall, the free painter can finish time[i] other walls. So choosing wall i for the paid painter "covers" 1 + time[i] walls at cost[i] → a 0/1 knapsack minimising cost to cover at least n walls.
Why not another pattern? Simulating schedules or trying subsets is exponential.
Approach:
dp[c]= min cost to covercwalls (capped atn);dp[0] = 0.- For each wall, for
cfromndown:dp[c] = min(dp[c], dp[max(0, c − 1 − time[i])] + cost[i]). - Return
dp[n].
class Solution:
def paintWalls(self, cost: List[int], time: List[int]) -> int:
n = len(cost)
INF = float("inf")
dp = [0] + [INF] * n # dp[c]: min cost so that c walls are taken care of
for c_i, t_i in zip(cost, time):
for c in range(n, 0, -1): # 0/1 knapsack: go downwards
dp[c] = min(dp[c], dp[max(0, c - 1 - t_i)] + c_i) # this wall + t_i free ones
return dp[n]
What the code is doing, line by line:
- Covering more than
nwalls is the same as coveringn, hence the cap at 0 withmax. - The walls the free painter does are exactly the ones not chosen for the paid painter.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Long times (one paid wall covers everything).
- All time 1.
- Single wall.
Complexity: O(n²) time, O(n) space.
Hard 3130. Find All Possible Stable Binary Arrays II Amazon
Problem: Count binary arrays with exactly zero zeros and one ones where no run of equal bits is longer than limit (mod 10⁹ + 7).
zero = 1, one = 1, limit = 22zero = 1, one = 2, limit = 11Constraints: 1 ≤ zero, one, limit ≤ 1000
Hint 1
DP over how many zeros and ones are used and what the last bit is.
Hint 2
Appending a bit is invalid only if it creates a run of length limit + 1. Count those cases and subtract.
From brute force to the pattern: Enumerating arrays is exponential; summing over run lengths is O(z · o · limit); the subtraction trick is O(z · o).
Spot it: Count arrays by (zeros used, ones used, last bit) → DP. Appending a 0 to any array is fine unless it creates a run of limit + 1 zeros; those bad cases are exactly the arrays that ended in 1 with i − limit − 1 zeros, so subtract them in O(1).
Why not another pattern? Summing over the last run length is O(zero · one · limit).
Approach:
dp0[i][0] = 1for1 ≤ i ≤ limit;dp1[0][j] = 1for1 ≤ j ≤ limit.dp0[i][j] = dp0[i−1][j] + dp1[i−1][j] − dp1[i−limit−1][j](last term wheni > limit).dp1symmetric; answerdp0 + dp1at(zero, one).
class Solution:
def numberOfStableArrays(self, zero: int, one: int, limit: int) -> int:
MOD = 10**9 + 7
dp0 = [[0] * (one + 1) for _ in range(zero + 1)] # ends with 0
dp1 = [[0] * (one + 1) for _ in range(zero + 1)] # ends with 1
for i in range(1, min(zero, limit) + 1):
dp0[i][0] = 1 # a single run of zeros
for j in range(1, min(one, limit) + 1):
dp1[0][j] = 1
for i in range(1, zero + 1):
for j in range(1, one + 1):
dp0[i][j] = dp0[i - 1][j] + dp1[i - 1][j] # append a 0
if i > limit:
dp0[i][j] -= dp1[i - limit - 1][j] # that would make limit + 1 zeros in a row
dp1[i][j] = dp0[i][j - 1] + dp1[i][j - 1] # append a 1
if j > limit:
dp1[i][j] -= dp0[i][j - limit - 1]
dp0[i][j] %= MOD
dp1[i][j] %= MOD
return (dp0[zero][one] + dp1[zero][one]) % MOD
What the code is doing, line by line:
- Appending 0 to every valid array with
i − 1zeros counts all candidates; the only invalid ones end with exactlylimit + 1zeros. - Those are a valid array ending in 1 with
i − limit − 1zeros, followed bylimit + 1zeros:dp1[i − limit − 1][j]of them. - Python's
%keeps values non-negative after the subtraction.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- limit ≥ zero and one (no restriction).
- limit = 1 (strictly alternating).
- One of the counts is 1.
Complexity: O(zero · one) time and space.
Hard 3995. Minimum Cost to Convert String III Amazon
Problem: Rules pattern → replacement (with * wildcards) cost costs[i] + number of *. Each position can be rewritten at most once. Return the minimum cost to turn source into target, or −1.
source = "hello", target = "world", rules = [["he","wo"],["llo","rld"]], costs = [3,4]7source = "cat", target = "dog", rules = [["c*t","dog"]], costs = [2]3Constraints: 1 ≤ source.length == target.length ≤ 5000 · source and target consist of lowercase English letters. · 1 ≤ rules.length == costs.length ≤ 200 · rules[i] = [patterni, replacementi] · 1 ≤ patterni.length == replacementi.length ≤ 20 · patterni contains at least one lowercase English letter and at most 5 '*' characters. · replacementi contains only lowercase English letters. · 1 ≤ costs[i] ≤ 1000
Hint 1
Since a position is used at most once, what does each rule application see and produce?
Hint 2
Split the string into untouched characters and rule blocks: DP over prefixes.
From brute force to the pattern: Searching rule sequences is exponential; prefix DP is polynomial.
Spot it: Because a position is never touched twice, every rule application acts on a block of the original source and must produce exactly the target there. So the string splits into untouched matching characters and rule blocks → DP over prefixes: dp[i] = cost to fix source[:i].
Why not another pattern? Searching sequences of rule applications is exponential.
Approach:
dp[0] = 0.- If
source[i] == target[i],dp[i + 1] = min(dp[i + 1], dp[i]). - For each rule of length
Lstarting ati: pattern matchessource[i:i+L](with*) and replacement equalstarget[i:i+L]→dp[i + L] = min(dp[i + L], dp[i] + cost + stars).
class Solution:
def minCost(self, source: str, target: str, rules: List[List[str]], costs: List[int]) -> int:
n = len(source)
INF = float("inf")
dp = [0] + [INF] * n # dp[i]: cheapest way to make source[:i] equal target[:i]
for i in range(n):
if dp[i] == INF:
continue
if source[i] == target[i]:
dp[i + 1] = min(dp[i + 1], dp[i]) # leave this character alone
for (pattern, repl), cost in zip(rules, costs):
L = len(pattern)
if i + L > n or repl != target[i:i + L]:
continue # must produce the target exactly
if all(p == "*" or p == s for p, s in zip(pattern, source[i:i + L])):
dp[i + L] = min(dp[i + L], dp[i] + cost + pattern.count("*"))
return dp[n] if dp[n] < INF else -1
What the code is doing, line by line:
- A rewritten block can't be changed again, so its replacement must already equal the target.
- Rule blocks never overlap and always match the original characters, so the DP over prefixes covers every valid plan.
- Wildcards match anything but add 1 each to the cost.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- source == target → 0.
- No rule fits → −1.
- Wildcards in patterns.
Complexity: O(n · Σ L) time over all rules, O(n) space.
Hard 2719. Count of Integers Cisco
Problem: Count integers in [num1, num2] (up to 10²²) whose digit sum is between min_sum and max_sum (mod 10⁹ + 7).
num1 = "1", num2 = "12", min_sum = 1, max_sum = 811num1 = "1", num2 = "5", min_sum = 1, max_sum = 55Constraints: 1 ≤ num1 ≤ num2 ≤ 10²² · 1 ≤ min_sum ≤ max_sum ≤ 400
Hint 1
Count up to a bound and subtract.
Hint 2
Build numbers digit by digit, tracking the digit sum and whether you're still on the bound.
From brute force to the pattern: Enumerating the range is impossible; the digit DP has about 20,000 states.
Spot it: Counting numbers up to a bound by a digit property → digit DP over (position, digit sum so far, tight).
Why not another pattern? Iterating up to 10²² numbers is impossible.
Approach:
f(N)counts0..Nwith digit sum in range: memoisedgo(pos, sum, tight).- Answer
f(num2) − f(num1)plus 1 ifnum1itself qualifies.
from functools import lru_cache
class Solution:
def count(self, num1: str, num2: str, min_sum: int, max_sum: int) -> int:
MOD = 10**9 + 7
def up_to(N): # how many x in [0, N] have min_sum <= digit sum <= max_sum
digits = list(map(int, N))
@lru_cache(maxsize=None)
def go(pos, total, tight):
if total > max_sum:
return 0 # already too large
if pos == len(digits):
return 1 if total >= min_sum else 0
top = digits[pos] if tight else 9
return sum(go(pos + 1, total + d, tight and d == top) for d in range(top + 1)) % MOD
return go(0, 0, True)
own = 1 if min_sum <= sum(map(int, num1)) <= max_sum else 0 # num1 itself
return (up_to(num2) - up_to(num1) + own) % MOD
What the code is doing, line by line:
tightmeans the prefix equalsN's prefix, so the next digit is capped.- Stopping when the sum exceeds
max_sumprunes most states. f(num2) − f(num1)counts(num1, num2], sonum1is added back separately (avoids subtracting 1 from a string).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- num1 == num2.
- num1 qualifies (added back).
- max_sum larger than any digit sum.
Complexity: O(D · S · 10) time for D ≤ 23 digits and S ≤ 400.
Medium 464. Can I Win Microsoft
Problem: Players alternately take unused numbers 1..maxChoosableInteger; whoever makes the running total reach desiredTotal wins. Can the first player force a win?
maxChoosableInteger = 10, desiredTotal = 11falsemaxChoosableInteger = 10, desiredTotal = 0trueConstraints: 1 ≤ maxChoosableInteger ≤ 20 · 0 ≤ desiredTotal ≤ 300
Hint 1
What fully describes a game position?
Hint 2
A position is winning if some move leads to a losing position for the opponent.
From brute force to the pattern: The game tree is m!; memoising on the used-set is O(2^m · m).
Spot it: The state is which numbers are used (the remaining total follows from it) → bitmask DP with memoisation, the classic win/lose game recursion.
Why not another pattern? Exploring the game tree without memo is factorial.
Approach:
- If the sum of all numbers < total, false; if total ≤ 0, true.
win(mask, remaining): true if some unusedx ≥ remaining, or somexwithnot win(mask | x, remaining − x).
from functools import lru_cache
class Solution:
def canIWin(self, maxChoosableInteger: int, desiredTotal: int) -> bool:
m = maxChoosableInteger
if desiredTotal <= 0:
return True
if m * (m + 1) // 2 < desiredTotal:
return False # nobody can ever reach it
@lru_cache(maxsize=None)
def win(used, remaining): # can the player to move force a win?
for x in range(m, 0, -1):
bit = 1 << x
if used & bit:
continue
if x >= remaining or not win(used | bit, remaining - x):
return True # reach the total now, or leave a losing position
return False
return win(0, desiredTotal)
What the code is doing, line by line:
remainingis determined byused, so the cache size is bounded by2^m.- Trying big numbers first finds immediate wins sooner.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Total ≤ 0 → true.
- Sum of all numbers too small → false.
- First move wins immediately.
Complexity: O(2^m · m) time and space (m ≤ 20).
Medium 837. New 21 Game Microsoft
Problem: Starting at 0, draw uniform points 1..maxPts until reaching at least k. Return the probability of ending with at most n points.
n = 10, k = 1, maxPts = 101.00000n = 6, k = 1, maxPts = 100.60000Constraints: 0 ≤ k ≤ n ≤ 10⁴ · 1 ≤ maxPts ≤ 10⁴
Hint 1
Write P[x] in terms of earlier P values.
Hint 2
That sum covers a fixed-size range: maintain it as a sliding window.
From brute force to the pattern: Summing maxPts terms per x is O(n · maxPts); the window is O(n).
Spot it: Probability DP: P[x] = (P[x − 1] + … + P[x − maxPts]) / maxPts, counting only previous totals below k (still drawing). The sum is a sliding window maintained in O(1).
Why not another pattern? Summing maxPts terms per state is O(n · maxPts).
Approach:
- If
k == 0orn ≥ k − 1 + maxPts, return 1. P[0] = 1,window = 1.- For
x = 1..n:P[x] = window / maxPts; addP[x]to the window ifx < k; dropP[x − maxPts]if it was in the window. - Answer:
Σ P[k..n].
class Solution:
def new21Game(self, n: int, k: int, maxPts: int) -> float:
if k == 0 or n >= k - 1 + maxPts:
return 1.0 # you can never exceed n
P = [0.0] * (n + 1)
P[0] = 1.0
window = 1.0 # sum of P over the last maxPts totals that are still drawing (< k)
for x in range(1, n + 1):
P[x] = window / maxPts
if x < k:
window += P[x] # x still draws again
if x - maxPts >= 0 and x - maxPts < k:
window -= P[x - maxPts] # too far back to reach x + 1
return sum(P[k:n + 1])
What the code is doing, line by line:
- Only totals below
kkeep drawing, so only they feed later totals. - The window slides by one each step: add the newest drawing total, drop the one too far back.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- k = 0 → 1.
- n large enough → 1.
- maxPts = 1.
Complexity: O(n) time and space.
Medium 3946. Maximum Number of Items From Sale I Microsoft
Problem: Buying item i (any copies) also gives one free copy of every other item whose factor is a multiple of factor_i. Maximise the total copies within budget.
items = [[6,2],[2,6],[3,4]], budget = 94items = [[2,4],[3,2],[4,1],[6,4],[12,4]], budget = 810Constraints: 1 ≤ items.length ≤ 1000 · items[i] = [factori, pricei] · 1 ≤ factori, pricei ≤ 1500 · 1 ≤ budget ≤ 1500
Hint 1
How much is the first copy of an item worth? How much is each additional copy worth?
Hint 2
Choose first copies with a knapsack; spend the rest on the cheapest item.
From brute force to the pattern: Subsets of item types are exponential; the knapsack is O(n · budget).
Spot it: Only the first copy of a type triggers its bonus: it's worth 1 + bonus_i at price_i. Extra copies are worth 1 each, so extras should be the cheapest item. That's a 0/1 knapsack on first copies plus leftover budget spent at the minimum price.
Why not another pattern? Greedy by value per price fails for knapsacks; trying all subsets is exponential.
Approach:
bonus_i= number ofj ≠ iwithfactor_i | factor_j.- Knapsack
dp[b]= best value of first copies costing exactlyb. - Answer:
max(dp[b] + (budget − b) // min_price)over reachableb.
class Solution:
def maximumSaleItems(self, items: List[List[int]], budget: int) -> int:
n = len(items)
bonus = [sum(1 for j in range(n) if j != i and items[j][0] % items[i][0] == 0) for i in range(n)]
NEG = float("-inf")
dp = [0] + [NEG] * budget # dp[b]: best value from first copies costing exactly b
for (factor, price), extra in zip(items, bonus):
value = 1 + extra # the copy itself plus its free copies
for b in range(budget, price - 1, -1): # 0/1: each type's first copy once
if dp[b - price] + value > dp[b]:
dp[b] = dp[b - price] + value
cheapest = min(price for _, price in items)
return max(dp[b] + (budget - b) // cheapest for b in range(budget + 1) if dp[b] != NEG) # extra copies at the lowest price
What the code is doing, line by line:
- Free copies depend only on which types are bought, not how many copies.
- Any additional purchased copy is worth exactly 1, so the cheapest item is the best extra (whether or not it was a first copy).
- Using exact-cost knapsack states lets the leftover be computed precisely.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Budget below every price → 0.
- Factor 1 (gives a copy of everything).
- Duplicate factors.
Complexity: O(n² + n · budget) time, O(budget) space.
Hard 2472. Maximum Number of Non-overlapping Palindrome Substrings Microsoft
Problem: Select the most non-overlapping palindromic substrings, each of length at least k.
s = "abaccdbbd", k = 32s = "adbcda", k = 20Constraints: 1 ≤ k ≤ s.length ≤ 2000 · s consists of lowercase English letters.
Hint 1
Use a prefix DP over the string.
Hint 2
Do you ever need a palindrome longer than k + 1?
From brute force to the pattern: All palindromic substrings with an interval DP is O(n²); restricting to lengths k, k + 1 gives O(nk).
Spot it: Prefix DP: dp[i] = best for s[:i]. Any palindrome of length ≥ k + 2 contains a shorter one (strip both ends) that is also long enough, so only lengths k and k + 1 ending at i need checking.
Why not another pattern? Checking all palindromic substrings is O(n²) states with O(n) checks.
Approach:
dp[0] = 0;dp[i] = dp[i − 1](skip a character).- If
s[i−k:i]is a palindrome:dp[i] = max(dp[i], dp[i − k] + 1); same for lengthk + 1.
class Solution:
def maxPalindromes(self, s: str, k: int) -> int:
n = len(s)
dp = [0] * (n + 1) # dp[i]: most palindromes in s[:i]
def is_pal(a, b):
return s[a:b] == s[a:b][::-1]
for i in range(1, n + 1):
dp[i] = dp[i - 1] # character i-1 unused
for L in (k, k + 1): # shortest qualifying palindromes are enough
if i - L >= 0 and is_pal(i - L, i):
dp[i] = max(dp[i], dp[i - L] + 1)
return dp[n]
What the code is doing, line by line:
- Replacing a long palindrome by the shorter one in its middle frees characters and keeps the count.
- The middle of a palindrome of length L ≥ k + 2 has length L − 2 ≥ k.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- k = 1 (every character).
- No palindrome long enough → 0.
- Overlapping candidates.
Complexity: O(n · k) time, O(n) space.
Hard 2977. Minimum Cost to Convert String II Microsoft
Problem: Rules convert a substring x to y at a cost (repeatable on the same block, blocks otherwise disjoint). Return the minimum cost to turn source into target, or −1.
source = "abcd", target = "acbe", original = ["a","b","c","c","e","d"], changed = ["b","c","b","e","b","e"], cost = [2,5,5,1,2,20]28source = "abcdefgh", target = "acdeeghh", original = ["bcd","fgh","thh"], changed = ["cde","thh","ghh"], cost = [1,3,5]9Constraints: 1 ≤ source.length == target.length ≤ 1000 · source, target consist only of lowercase English characters. · 1 ≤ cost.length == original.length == changed.length ≤ 100 · 1 ≤ original[i].length == changed[i].length ≤ source.length · original[i], changed[i] consist only of lowercase English characters. · original[i] != changed[i] · 1 ≤ cost[i] ≤ 10⁶
Hint 1
Converting the same block several times is a shortest path between strings.
Hint 2
Different blocks are disjoint: use a DP over prefixes.
From brute force to the pattern: Searching sequences of operations is exponential; Floyd + prefix DP is polynomial.
Spot it: Two layers: repeated conversions of one block form a shortest path between strings → Floyd–Warshall over the distinct rule strings. Then blocks are disjoint → a prefix DP: dp[i + L] = dp[i] + dist(source[i:i+L], target[i:i+L]) for every rule length L.
Why not another pattern? Trying conversion sequences is exponential.
Approach:
- Give each distinct string an id;
dist[a][b]= min rule cost; Floyd–Warshall (only same-length strings connect). dp[0] = 0; ifsource[i] == target[i],dp[i + 1] = dp[i].- For each rule length
L: if both slices have ids anddistis finite,dp[i + L] = min(dp[i + L], dp[i] + dist).
class Solution:
def minimumCost(self, source: str, target: str, original: List[str], changed: List[str], cost: List[int]) -> int:
ids = {}
for w in original + changed:
ids.setdefault(w, len(ids))
m = len(ids)
INF = float("inf")
dist = [[INF] * m for _ in range(m)]
for i in range(m):
dist[i][i] = 0
for a, b, c in zip(original, changed, cost):
dist[ids[a]][ids[b]] = min(dist[ids[a]][ids[b]], c)
for mid in range(m): # Floyd-Warshall: chains of conversions on one block
dm = dist[mid]
for i in range(m):
dim = dist[i][mid]
if dim == INF:
continue
di = dist[i]
for j in range(m):
if dim + dm[j] < di[j]:
di[j] = dim + dm[j]
lengths = sorted({len(w) for w in original})
n = len(source)
dp = [0] + [INF] * n # dp[i]: cheapest way to fix source[:i]
for i in range(n):
if dp[i] == INF:
continue
if source[i] == target[i]:
dp[i + 1] = min(dp[i + 1], dp[i]) # nothing to convert here
for L in lengths:
if i + L > n:
break
a, b = ids.get(source[i:i + L]), ids.get(target[i:i + L])
if a is not None and b is not None and dist[a][b] < INF:
dp[i + L] = min(dp[i + L], dp[i] + dist[a][b]) # convert this whole block
return dp[n] if dp[n] < INF else -1
What the code is doing, line by line:
- Operations on the same block can chain (x → y → z), which is a path in the graph of strings; Floyd–Warshall gives all cheapest chains.
- Different blocks never overlap, so the string splits into converted blocks and untouched equal characters: the prefix DP.
- Only rule lengths can form a converted block.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Impossible → −1.
- Chained conversions on one block.
- Equal substrings need no cost.
Complexity: O(m³ + n · R · L) for m ≤ 200 distinct strings, R distinct lengths and slice length L.
Medium 688. Knight Probability in Chessboard Apple
Problem: A knight makes k random moves (each of 8 directions equally likely). Return the probability it is still on the board.
n = 3, k = 2, row = 0, column = 00.06250n = 1, k = 0, row = 0, column = 01.00000Constraints: 1 ≤ n ≤ 25 · 0 ≤ k ≤ 100 · 0 ≤ row, column ≤ n - 1
Hint 1
Track the probability of being on each cell after each move.
Hint 2
Each cell distributes 1/8 of its probability to each knight target.
From brute force to the pattern: All 8ᵏ sequences vs O(k n²) DP.
Spot it: Probabilities after t moves depend only on those after t − 1 → DP over moves on an n × n grid: each cell sends 1/8 of its probability to every target that stays on the board (mass that falls off is lost).
Why not another pattern? Simulating all 8ᵏ move sequences is impossible for k = 100.
Approach:
prob[row][column] = 1.- Repeat
ktimes: new grid, each cell spreadsp / 8to its on-board knight targets. - Return the sum of the final grid.
class Solution:
def knightProbability(self, n: int, k: int, row: int, column: int) -> float:
moves = [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)]
prob = [[0.0] * n for _ in range(n)]
prob[row][column] = 1.0
for _ in range(k):
nxt = [[0.0] * n for _ in range(n)]
for r in range(n):
for c in range(n):
p = prob[r][c]
if p:
for dr, dc in moves:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n:
nxt[nr][nc] += p / 8 # moves off the board are simply lost
prob = nxt
return sum(map(sum, prob))
What the code is doing, line by line:
- Each move multiplies by 1/8 per direction; probability leaving the board never returns.
- The total remaining mass is the probability of staying on.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- k = 0 → 1.
- n = 1 → 0 for k > 0.
- Starting in a corner.
Complexity: O(k · n² · 8) time, O(n²) space.