InterviewKata
Pattern 15 of 24

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?

Write 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:

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)

  1. Recursion (brute force). Write f(state) in terms of smaller states. It's correct but exponential, because it re-solves the same states.
  2. 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).
  3. Tabulation (bottom-up). Turn the cache into an array dp and fill it in an order where everything a cell needs is already filled. No recursion, so no stack-depth limit.
  4. Space optimization. Look at which cells the transition reads. If row i only reads row i−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

  1. Name the state in plain English. Complete the sentence "dp[i] (or dp[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.
  2. 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.
  3. Base cases. The smallest states you can answer without thinking: empty prefix, amount 0, empty string, first row/column.
  4. 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.
  5. Where's the answer? dp[n], dp[m][n], dp[0][n-1], or max(dp) when the state is "ending at i".
  6. Squeeze the space once it works.

The DP families at a glance

FamilyStateTransition (the last decision)Classic
1-D lineardp[i] = best for first i itemsmax(dp[i-1], dp[i-2] + nums[i])Climbing Stairs, House Robber
0/1 knapsackdp[c] = achievable with capacity cdp[c] |= dp[c - w], looping c backwardsPartition Equal Subset Sum, Target Sum
Unbounded knapsackdp[a] = best for amount amin(dp[a - c] + 1), looping a forwardsCoin Change, Coin Change II
Two stringsdp[i][j] = answer for prefixes a[:i], b[:j]match → diagonal; else combine up / left / diagonalLCS, Edit Distance
LISdp[i] = longest ending at i1 + max(dp[j]) for j < i, nums[j] < nums[i]LIS, Russian Doll Envelopes
Intervaldp[l][r] = best for the range l..rtry every split / last element k in (l, r)Burst Balloons, Matrix Chain
Griddp[r][c] = best to reach (r, c)from top and from leftUnique Paths, Min Path Sum
State machinedp[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²) or O(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.
The one question to ask"Can I describe the answer for the whole input using answers for smaller prefixes / amounts / ranges, and would brute force revisit the same smaller question?" If yes, it's DP. Then ask "what is the last decision?" to find the transition.

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 choicesYou 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 finen 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:

  1. The ladder, stage 1 (fib_rec): the recurrence written directly. It is correct but slow: fib_rec(5) calls fib_rec(3) twice, fib_rec(2) three times, and so on, so the work roughly doubles with every step of n.
  2. Stage 2 (fib_memo): the same recursion plus a dictionary memo. Before computing fib(n) we check if n in memo; after computing it we store memo[n] = answer. Each value is now computed once, so the time drops to O(n). The memo=None default plus if memo is None: memo = {} gives every top-level call a fresh dictionary, and passing memo into the recursive calls makes them all share it.
  3. Stage 3 (fib_tab): no recursion at all. A list dp is filled from index 2 upwards, and each cell only reads two cells that were filled earlier.
  4. Stage 4 (fib_opt): since only the last two cells are read, two variables replace the whole list. next_value is a temporary: if we wrote older = newer first, the old older would be lost before we could add it. The dry run below shows stages 3 and 4 side by side: older after n loop steps equals dp[n].
  5. 0/1 knapsack: dp[cap] is the best value for a bag of size cap. For each item, a while loop walks cap from capacity down to weight. Going downwards means dp[cap - weight] was not touched yet for this item, so the item can be counted at most once.
  6. Unbounded knapsack: the same update, but cap goes upwards. Now dp[cap - weight] may already contain this item, which is exactly what lets an item be reused.
  7. Two strings: the table is built row by row with append (one row of zeros per prefix of a). Cell dp[i][j] compares the last characters a[i - 1] and b[j - 1]: on a match it extends the diagonal, otherwise it keeps the better of dropping one character from either string.
  8. Interval DP: dp[left][right] is the best answer for the open interval between left and right. The outer loop goes over length from short to long; the inner loop tries every split point last and keeps the maximum in best (which starts at minus infinity so the first candidate always wins).
  9. State machine: one variable per state (hold, sold, rest). All three new values are computed into new_hold, new_sold, new_rest before any variable is overwritten, so each one is based on yesterday's numbers only.

Dry run on the example:

ndp[n] in fib_tabolder after n loop stepsnewer after n loop steps
0001
1111
2112
3223
4335
5558
68813
7131321

6. How to remember it forever

Memory hook: "Last decision, written down"Every DP is just two moves. (1) Ask what the last decision was, which gives the transition. (2) Write down every answer so you never redo it, which is the table. Name the state in one sentence before writing any code.
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 dp table), 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 storyIn the code
Name the state in one sentence"dp[i] = number of ways to reach stair i"
The last decisionthe transition dp[i] = dp[i - 1] + dp[i - 2]
Answers that need no decisionbase cases dp[1], dp[2]
Write it downmemo[n] = answer or the dp list
Fill in an order that's always readyfor i in range(3, n + 1)

Watch it on numbers

Climbing Stairs, n = 5, taking 1 or 2 steps at a time.

  1. Last decision: the final move was 1 step (from stair 4) or 2 steps (from stair 3). So ways(5) = ways(4) + ways(3).
  2. Base: ways(1) = 1, ways(2) = 2.
  3. Write down: ways(3) = 3, ways(4) = 5, ways(5) = 8. Each value is computed once.
  4. 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

New to this pattern? Start with theseSolve 509. Fibonacci Number, then 70. Climbing Stairs, then 198. House Robber (1-D linear). They are marked Start here below. Read the explanation, press Play on the step-through, then close the page and solve it yourself on LeetCode.
Medium 198. House Robber (1-D linear) Start here Google Meta Amazon Cisco Microsoft Array

Problem: Houses in a row hold nums[i] money. You can't rob two adjacent houses. Maximize the loot.

Example 1
Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3). Total amount you can rob = 1 + 3 = 4.
Example 2
Input: nums = [2,7,9,3,1]
Output: 12
Explanation: Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1). Total amount you can rob = 2 + 9 + 1 = 12.

Constraints: 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):

  1. State: dp[i] = the most money from houses 0..i.
  2. Transition: dp[i] = max(dp[i−1], dp[i−2] + nums[i]), i.e. skip house i or rob it.
  3. Base: before any house the loot is 0, so both "previous" values start at 0.
  4. Space: only the last two values are ever read, so keep two variables prev2 and prev1.
  5. Answer: prev1 after 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:

  1. prev2 and prev1 are the last two cells of the dp table (dp[i - 2] and dp[i - 1]). Both start at 0 because before any house you have robbed nothing.
  2. The loop visits every house by index and reads its money into money.
  3. best_here is dp[i]: the better of skipping house i (the best so far stays prev1) or robbing it (then house i - 1 is off-limits, so add money to prev2).
  4. Shift the window: the old prev1 becomes prev2, and best_here becomes prev1. The order matters: prev2 = prev1 must happen before prev1 is overwritten.
  5. After the last house, prev1 is the best loot for the whole street.

Dry run on the example:

nums = [2, 7, 9, 3, 1] (answer 12 = 2 + 9 + 1):

imoneyskip = prev1rob = prev2 + moneychoicenew prev2new prev1
0202rob02
1727rob27
29711rob711
331110skip1111
411112rob1112

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 Array

Problem: Can the array be split into two subsets with equal sums?

Example 1
Input: nums = [1,5,11,5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11].
Example 2
Input: nums = [1,2,3,5]
Output: false
Explanation: The array cannot be partitioned into equal sum subsets.

Constraints: 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):

  1. If the total is odd, return False right away.
  2. target = total // 2. State: dp[s] = can some subset of the items seen so far sum to s?
  3. Base: dp[0] = True (the empty subset).
  4. For each number x, loop s from target down to x: dp[s] = dp[s] or dp[s − x].
  5. 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:

  1. Add up the numbers. If the total is odd, two equal halves are impossible, so return False immediately.
  2. target is half the total. The question becomes: can some subset add up to exactly target?
  3. dp has one True/False cell per possible sum 0..target. Only dp[0] starts True (pick nothing).
  4. For each number num, a while loop walks subset_sum from target down to num. If subset_sum - num was reachable, adding num makes subset_sum reachable.
  5. Why downwards? Cells below subset_sum have not been updated for this num yet, so they describe subsets without it. Going upwards would let num be added twice (for [2, 2, 3, 5], target 6, it would wrongly reach 6 as 3 + 3).
  6. 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 =01234567891011
startTFFFFFFFFFFF
after num = 1TTFFFFFFFFFF
after num = 5TTFFFTTFFFFF
after num = 11TTFFFTTFFFFT
after num = 5TTFFFTTFFFTT

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 Array

Problem: Fewest coins (unlimited supply of each denomination) that make up amount, or -1 if impossible.

Example 1
Input: coins = [1,2,5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1
Example 2
Input: coins = [2], amount = 3
Output: -1

Constraints: 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):

  1. State: dp[a] = fewest coins that make amount a.
  2. Base: dp[0] = 0. Every other cell starts at a sentinel INF = amount + 1 (impossible).
  3. For a = 1..amount (forwards), for each coin c ≤ a: dp[a] = min(dp[a], dp[a − c] + 1). Coin c is the last coin added.
  4. If dp[amount] is still INF, 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:

  1. INF = amount + 1 is a sentinel for "impossible": even using only 1-coins you never need more than amount coins.
  2. dp starts with every amount impossible, except dp[0] = 0.
  3. The outer loop walks sub_amount from 1 up to amount. For each coin that fits, it asks: "if this coin is the last one I add, how many coins in total?" That is dp[sub_amount - coin] + 1, and we keep the minimum.
  4. 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).
  5. If dp[amount] is still INF, 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_amountdp[sub_amount]best last coin
00—
11coin 1 + dp[0] (= 0)
21coin 2 + dp[0] (= 0)
32coin 1 + dp[2] (= 1)
42coin 2 + dp[2] (= 1)
51coin 5 + dp[0] (= 0)
62coin 1 + dp[5] (= 1)
72coin 2 + dp[5] (= 1)
83coin 1 + dp[7] (= 2)
93coin 2 + dp[7] (= 2)
102coin 5 + dp[5] (= 1)
113coin 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 String

Problem: Minimum number of single-character inserts, deletes, or replaces to turn word1 into word2.

Example 1
Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation: horse -> rorse (replace 'h' with 'r') rorse -> rose (remove 'r') rose -> ros (remove 'e')
Example 2
Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation: intention -> inention (remove 't') inention -> enention (replace 'i' with 'e') enention -> exention (replace 'n' with 'x') exention -> exection (replace 'n' with 'c') exection -> execution (insert 'u')

Constraints: 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):

  1. State: dp[i][j] = edits to turn word1[:i] into word2[:j]. The table is (m+1) × (n+1).
  2. Base: dp[i][0] = i (delete everything) and dp[0][j] = j (insert everything).
  3. Fill row by row. If word1[i−1] == word2[j−1], copy the diagonal (no edit needed).
  4. Otherwise take 1 + min(up = delete, left = insert, diagonal = replace).
  5. 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:

  1. len1 and len2 are the word lengths (called m and n in the steps above).
  2. 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.
  3. Column 0 and row 0 are the base cases: turning word1[:i] into an empty string takes i deletes; turning an empty string into word2[:j] takes j inserts.
  4. 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).
  5. Otherwise we pay 1 edit plus the cheapest of three neighbors: up (delete_cost), left (insert_cost) or diagonal (replace_cost).
  6. 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 →""ros
""0123
h1123
o2212
r3222
s4332
e5443

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 Array

Problem: Length of the longest strictly increasing subsequence (elements don't have to be contiguous).

Example 1
Input: nums = [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.
Example 2
Input: nums = [0,1,0,3,2,3]
Output: 4

Constraints: 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):

  1. State: dp[i] = length of the longest increasing subsequence that ends at index i. Start every cell at 1.
  2. For each i, look at every j < i. If nums[j] < nums[i], nums[i] can extend that subsequence: dp[i] = max(dp[i], dp[j] + 1).
  3. Answer: max(dp), because the best subsequence can end anywhere.
  4. Faster version: keep tails, where tails[k] = the smallest possible tail of an increasing subsequence of length k+1. Binary-search each x into it: append if it's larger than every tail, otherwise overwrite the first tail ≥ x. The answer is len(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:

  1. Slow version. dp starts as all 1s: every number on its own is an increasing run of length 1.
  2. For each i, the inner loop looks at every earlier index j. If nums[j] < nums[i], then nums[i] can be appended to the best run ending at j, giving length dp[j] + 1.
  3. The longest run can end at any index, so a final loop finds the largest value in dp. Starting best at 0 also handles an empty list.
  4. Fast version. tails[k] remembers the smallest number that can end an increasing run of length k + 1. A smaller tail is always better, because more future numbers can extend it. tails stays sorted.
  5. For each num, the helper finds position = the first tail that is >= num. If there is none, num extends the longest run, so it is appended. Otherwise num replaces that tail: we found a run of the same length with a smaller ending.
  6. The hand-written binary search (first_tail_at_least): the answer is somewhere in low..high (where high = len(tails) means "after the end"). Look at the middle element. If it is smaller than num, the answer must be to the right, so low = middle + 1. Otherwise middle itself might be the answer, so high = middle. Each step halves the window; when low == high the loop stops and low is the answer. (Python's bisect.bisect_left does the same thing.)
  7. The length of tails is the LIS length. Careful: tails itself 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:

inums[i]dp[i] (slow version)position (binary search)actiontails after
01010append[10]
1910overwrite tails[0][9]
2210overwrite tails[0][2]
3521append[2, 5]
4321overwrite tails[1][2, 3]
5732append[2, 3, 7]
610143append[2, 3, 7, 101]
71843overwrite 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 Array

Problem: Bursting balloon i earns nums[left] · nums[i] · nums[right], using its current neighbors. Missing neighbors count as 1. Maximize total coins.

Example 1
Input: nums = [3,1,5,8]
Output: 167
Explanation: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> [] coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
Example 2
Input: nums = [1,5]
Output: 10

Constraints: 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):

  1. Pad the array with a 1 at each end. This encodes the "missing neighbor counts as 1" rule.
  2. State: dp[l][r] = the most coins from bursting every balloon strictly between l and r.
  3. Fill by increasing interval length, so shorter intervals are always ready first.
  4. Transition: try each k in (l, r) as the last balloon burst: dp[l][k] + a[l]·a[k]·a[r] + dp[k][r].
  5. 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:

  1. balloons is nums with an extra 1 at both ends, so every real balloon always has two neighbors.
  2. The table dp is built row by row with append. dp[left][right] means: the best total from bursting only the balloons strictly between positions left and right. Intervals with nothing inside (right = left + 1) stay 0.
  3. length is the distance right - left, going from 2 (one balloon inside) up to the whole array. Shorter intervals are filled first.
  4. For each interval, every last in between is tried as the final balloon burst there. Everything else inside was already burst, so its neighbors are balloons[left] and balloons[right]; it earns burst_last.
  5. The left part (left, last) and the right part (last, right) are independent, and their best totals are already in the table. total adds the three pieces; we keep the maximum.
  6. 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)··330159167
1 (3)···15135159
2 (1)····4048
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 Matrix

Problem: Move only right or down from the top-left to the bottom-right, minimizing the sum of the cells you pass through.

Example 1
Input: grid = [[1,3,1],[1,5,1],[4,2,1]]
Output: 7
Explanation: Because the path 1 → 3 → 1 → 1 → 1 minimizes the sum.
Example 2
Input: grid = [[1,2,3],[4,5,6]]
Output: 12

Constraints: 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):

  1. State: dp[r][c] = cheapest path sum to reach (r, c). Keep only one row: dp of size num_cols + 1.
  2. dp[0] stays ∞ as a left-wall sentinel. Set dp[1] = 0 so the start cell costs just its own value.
  3. For each row, go left to right: dp[j+1] = cell + min(dp[j+1] (from above), dp[j] (from the left)).
  4. 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:

  1. 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 column j; dp[0] is a wall that is always infinity.
  2. Before the first row everything is unreachable except dp[1] = 0. That makes the start cell cost exactly grid[0][0] (coming "from above" for free).
  3. For each row (left to right), dp[j + 1] still holds the value from the row above (from_above), and dp[j] was just updated for this row (from_left). The new value is the cell's own cost plus the cheaper of the two.
  4. In row 0, from_above is infinity (except at the start), so cells can only be reached from the left. In column 0, from_left is the wall, so cells can only be reached from above.
  5. 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 valuesdp[0] (wall)dp[1] (col 0)dp[2] (col 1)dp[3] (col 2)
start—∞0∞∞
after row 0[1, 3, 1]∞145
after row 1[1, 5, 1]∞276
after row 2[4, 2, 1]∞687

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 Array

Problem: Unlimited transactions, but after selling you must wait one day before buying again. Maximize profit.

Example 1
Input: prices = [1,2,3,0,2]
Output: 3
Explanation: transactions = [buy, sell, cooldown, buy, sell]
Example 2
Input: prices = [1]
Output: 0

Constraints: 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):

  1. Define three states for the end of each day: hold (own a share), sold (sold today), rest (no share and free to buy).
  2. Start with hold = sold = −∞ (impossible before day 0) and rest = 0.
  3. Each day, compute all three from yesterday's values: hold = max(hold, rest − p), sold = hold + p, rest = max(rest, sold).
  4. Buying reads rest, never sold. That one detail enforces the cooldown.
  5. 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:

  1. Three variables, one per state, each holding the best profit you can have at the end of the current day in that state.
  2. hold and sold start at minus infinity because neither state is possible before the first day; rest starts at 0.
  3. Each day computes new_hold, new_sold and new_rest from yesterday's values. The temporaries are needed because, for example, new_sold uses yesterday's hold; if we had already overwritten hold, we could buy and sell on the same day, and new_rest would see today's sold and skip the cooldown.
  4. Buying reads rest, never sold: that is the cooldown. After a sale you spend a day in sold before moving to rest, and only then can you buy.
  5. The answer is the better of sold and rest.

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:

daypriceholdsoldrest
start—−∞−∞0
01-1−∞0
12-110
23-121
301-12
42132

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

Wrong loop direction in knapsack0/1 (each item once): capacity loop goes backwards. Unbounded (reuse): forwards. Getting this wrong silently changes which problem you're solving. [2,2,3,5] is a classic failing test for 416.
A vague stateIf you can't finish the sentence "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].
Off-by-one with string prefixesWith 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.
Loop order when countingCoin Change II (number of combinations): coins in the outer loop, amounts in the inner loop. Swap them and you count permutations (1+2 and 2+1 separately).
SentinelsUse 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.
Memoization pitfalls in Python@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?

Backwards keeps 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?

Two strings and prefixes → 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?

The best subsequence can end at any index, not necessarily the last one.

Burst Balloons: what makes the sub-intervals independent?

If k bursts last in (l, r), its neighbors at that moment are exactly l and r, and the left and right parts never interact.

"Return the minimum number of perfect squares that sum to n."

A minimum over reusable "coins" (1, 4, 9, …) is unbounded knapsack, just like Coin Change.

10. Practice list (in order)

#ProblemLevelData structureTwist
70Climbing StairsEasyMathWarm-up: Fibonacci in disguise
509Fibonacci NumberEasyMathThe smallest DP: f(n) = f(n−1) + f(n−2) with two rolling variables
746Min Cost Climbing StairsEasyArrayClimbing Stairs with a price: take the cheaper of the two ways in
198House RobberMediumArrayTake / skip, 1-D
213House Robber IIMediumArrayCircle → run 198 twice (drop first / drop last)
322Coin ChangeMediumArrayUnbounded knapsack, min
518Coin Change IIMediumArrayCount combinations: coins in the outer loop
416Partition Equal Subset SumMediumArray0/1 knapsack, backwards
300Longest Increasing SubsequenceMediumArray"Ending at i" + O(n log n) tails
1143Longest Common SubsequenceMediumStringTwo-string template
72Edit DistanceMediumString3-way min
64Minimum Path SumMediumMatrixGrid DP, 1 row of memory
309Best Time to Buy and Sell Stock with CooldownMediumArrayState machine
53Maximum SubarrayMediumArrayKadane: best sum ending here = max(x, best ending before + x)
62Unique PathsMediumMatrixGrid DP: paths to a cell = paths from above + paths from the left
63Unique Paths IIMediumMatrixSame grid DP, but an obstacle cell has 0 paths
120TriangleMediumArrayBottom-up: each cell adds the cheaper of its two children
91Decode WaysMediumStringFibonacci-like: last step decodes one digit (not 0) or two digits (10–26)
97Interleaving StringMediumStringdp[i][j]: can s1[:i] and s2[:j] interleave into s3[:i+j]? The last char came from s1 or s2
139Word BreakMediumStringHash Setok[i] = some dictionary word ends at i and ok[start of that word]
152Maximum Product SubarrayMediumArrayTrack both the max and min product ending here: a negative flips them
221Maximal SquareMediumMatrixside[r][c] = 1 + min(top, left, top-left) for a '1' cell
343Integer BreakMediumMathbest[i] = max over the first part j of j · max(i − j, best[i − j])
368Largest Divisible SubsetMediumArraySort; LIS-style DP where 'increasing' means 'divides'; keep parents to rebuild
377Combination Sum IVMediumArrayOrder matters: ways[t] = Σ ways[t − x] (target in the outer loop)
396Rotate FunctionMediumArrayMathF(k) = F(k − 1) + sum − n · (the element that wraps to the front)
486Predict the WinnerMediumArraydiff[i][j] = best (my score − opponent's) on nums[i..j]; take an end, subtract their best
494Target SumMediumArrayHash MapCount ways to reach each running sum; each number adds or subtracts
516Longest Palindromic SubsequenceMediumStringInterval DP: equal ends add 2 to the inside; otherwise drop one end
583Delete Operation for Two StringsMediumStringKeep the longest common subsequence; delete everything else: m + n − 2·LCS
638Shopping OffersMediumArrayHash MapMemoised recursion on the remaining needs tuple; try each offer that doesn't overshoot
647Palindromic SubstringsMediumStringExpand around each of the 2n − 1 centres; each successful expansion is one palindrome
673Number of Longest Increasing SubsequenceMediumArrayLIS DP with a second array: how many LIS end at each index
714Best Time to Buy and Sell Stock with Transaction FeeMediumArrayTwo states per day: holding / not holding; pay the fee when selling
740Delete and EarnMediumArrayHash MapTotal points per value, then House Robber over values (adjacent values conflict)
788Rotated DigitsMediumMathDigit state per number from its prefix: invalid / same / changed (dp[i] from dp[i // 10])
790Domino and Tromino TilingMediumMathfull[i] = full[i−1] + full[i−2] + 2·half[i−2]; half[i] = half[i−1] + full[i−1]
799Champagne TowerMediumArraySimulate the flow row by row: each glass keeps 1 and splits the excess evenly
877Stone GameMediumArrayInterval DP on score difference (and Alice always wins: she can take all even or all odd piles)
918Maximum Sum Circular SubarrayMediumArrayMax of (Kadane max) and (total − Kadane min); if all negative, just the Kadane max
931Minimum Falling Path SumMediumMatrixRow by row: each cell adds the cheapest of the three cells above it
983Minimum Cost For TicketsMediumArrayHash Setcost[d] = cost[d−1] if not travelling; else min of 1-, 7-, 30-day pass ending today
1043Partition Array for Maximum SumMediumArraybest[i] = max over the last block length L ≤ k of best[i − L] + L · max(block)
1048Longest String ChainMediumStringHash MapSort by length; chain[w] = 1 + max chain of w with one letter removed
1049Last Stone Weight IIMediumArrayHash SetSplit stones into two groups as evenly as possible: subset-sum up to total/2
1140Stone Game IIMediumArraybest(i, M) = suffix sum − min over X ≤ 2M of best(i + X, max(M, X))
1277Count Square Submatrices with All OnesMediumMatrixside[r][c] = 1 + min(top, left, diagonal); the sum of all sides counts every square
1749Maximum Absolute Sum of Any SubarrayMediumArrayKadane twice: the largest sum and the smallest sum; answer max(|both|)
1884Egg Drop With 2 Eggs and N FloorsMediumMathFlip the question: with m moves you can cover floors[m] = floors[m − 1] + m; find the first m reaching n
2140Solving Questions With BrainpowerMediumArrayFrom the back: best[i] = max(skip → best[i + 1], solve → points + best[i + brainpower + 1])
2400Number of Ways to Reach a Position After Exactly k StepsMediumMathChoose which steps go right: C(k, (k + d) / 2) when parity and distance allow
3573Best Time to Buy and Sell Stock VMediumArrayThree states per transaction count: free, holding long, holding short
3592Inverse Coin ChangeMediumArrayRebuild the Coin Change II table amount by amount; amount i is a coin iff the count is one short
3660Jump Game IXMediumArrayAnswer = prefix max, unless the prefix max beats the suffix min to its right; then reuse ans[i + 1]
3738Longest Non-Decreasing Subarray After Replacing at Most One ElementMediumArrayleft[i] = run ending at i, right[i] = run starting at i; try replacing each element to glue its neighbours' runs
3742Maximum Path Score in a GridMediumMatrixGrid DP with a cost dimension: best[r][c][cost], cost capped at min(k, m + n)
3751Total Waviness of Numbers in Range IMediumMathRange is small (≤ 10⁵): count peaks and valleys of each number directly
3976Maximum Subarray Sum After MultiplierMediumArrayKadane with three states: before the operation, inside it, after it (for both × k and ÷ k)
4002Count Valid SequencesMediumMathAll compositions C(n−1, k−1) minus the all-odd ones C((n−k)/2 + k − 1, k − 1)
279Perfect SquaresMediumMathArrayUnbounded knapsack: least[x] = 1 + min(least[x − s²]) over squares ≤ x
357Count Numbers with Unique DigitsMediumMathNumbers with exactly k distinct digits: 9 · 9 · 8 · … (k factors); sum over k ≤ n
1871Jump Game VIIMediumStringArrayreach[i] = s[i] is '0' and some reachable index lies in [i − maxJump, i − minJump]; keep that count in a sliding window
241Different Ways to Add ParenthesesMediumStringSplit at every operator, combine all results of both sides; memoise on the substring
313Super Ugly NumberMediumArrayMathUgly Number II with k pointers: next = min(ugly[ptr[i]] · primes[i]); advance every pointer that produced it
376Wiggle SubsequenceMediumArrayTwo states: longest wiggle ending with an up step, and with a down step
474Ones and ZeroesMediumStringArray0/1 knapsack with two capacities (zeros, ones); iterate capacities downwards
1014Best Sightseeing PairMediumArrayScore = (values[i] + i) + (values[j] − j): keep the best values[i] + i seen so far
1594Maximum Non Negative Product in a MatrixMediumMatrixKeep both the max and min product per cell: a negative cell swaps them
2770Maximum Number of Jumps to Reach the Last IndexMediumArraydp[j] = 1 + max dp[i] over earlier i with |nums[j] − nums[i]| ≤ target
3418Maximum Amount of Money Robot Can EarnMediumMatrixGrid DP with a third index: neutralisations used so far (0, 1 or 2)
3434Maximum Frequency After Subarray OperationMediumArrayFor each value v: Kadane on +1 for v, −1 for k gives the best gain of turning v into k inside one subarray
3857Minimum Cost to Split into OnesMediumMathdp[x] = min over a of a·(x − a) + dp[a] + dp[x − a]; every order gives x(x − 1)/2
464Can I WinMediumMathGame DP over the bitmask of used numbers: a state wins if some pick reaches the total or leaves a losing state
837New 21 GameMediumMathP[x] = (sum of P over the last maxPts points still drawing) / maxPts; keep that sum as a sliding window
3946Maximum Number of Items From Sale IMediumArrayThe 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
688Knight Probability in ChessboardMediumMatrixdp over moves: probability of being on each cell after t moves; each move spreads 1/8 to every knight target on the board
312Burst BalloonsHardArrayInterval DP, "last" trick
10Regular Expression MatchingHardStringTwo-string DP with *
123Best Time to Buy and Sell Stock IIIHardArray4-state machine (buy1, sell1, buy2, sell2)
32Longest Valid ParenthesesHardStringdp[i] = longest valid substring ending at i; a ')' extends the match before it
44Wildcard MatchingHardStringdp[i][j]: does p[:j] match s[:i]? '*' matches empty (dp[i][j−1]) or one more char (dp[i−1][j])
87Scramble StringHardStringHash MapMemoised recursion: try every split, swapped or not; prune with letter counts
132Palindrome Partitioning IIHardStringExpand palindromes around each centre; each palindrome s[l..r] gives cuts[r+1] ≤ cuts[l] + 1
174Dungeon GameHardMatrixDP backwards from the princess: need[r][c] = max(1, min(need right, need down) − dungeon[r][c])
188Best Time to Buy and Sell Stock IVHardArrayState machine: buy[j] / sell[j] = best balance after the j-th buy / sell
233Number of Digit OneHardMathCount 1s per digit position from the high part, the current digit and the low part
403Frog JumpHardArrayHash MapHash SetFor each stone, the set of jump sizes that can land on it; extend by k−1, k, k+1
546Remove BoxesHardArraydp(i, j, k): boxes[i..j] with k extra boxes equal to boxes[i] attached on the left
741Cherry PickupHardMatrixTwo people walk down-right together; state (step, row1, row2); count a shared cell once
879Profitable SchemesHardArray0/1 knapsack over (members used, profit capped at minProfit)
1092Shortest Common Supersequence HardStringBuild the LCS table, then walk back: shared letters once, other letters from whichever side
1301Number of Paths with Max ScoreHardMatrixDP of (best score, number of ways) from 'S' moving up/left/up-left; combine equal bests by adding ways
1320Minimum Distance to Type a Word Using Two FingersHardStringOne finger is always on the last letter; DP over the other finger's position (26 + 'free')
1340Jump Game VHardArrayMemoised DFS: visits(i) = 1 + max over reachable lower indices (stop at the first taller-or-equal)
1406Stone Game IIIHardArraylead[i] = max over taking 1–3 stones of (their sum − lead[i + taken])
1463Cherry Pickup IIHardMatrixBoth robots move down one row at a time: DP over (column 1, column 2), 9 moves per state
1510Stone Game IVHardMathwin[n] = some square k² ≤ n leaves a losing position
2376Count Special IntegersHardMathHash SetDigit DP: count shorter numbers by permutations, then walk n's digits with a used-digit set
2484Count Palindromic SubsequencesHardStringArrayFix the middle; multiply 'pairs ab before' by 'pairs ba after' for all 100 digit pairs
3225Maximum Score From Grid OperationsHardMatrixEach column's score comes from its left or right neighbour; DP over (height, is this column's credit pending to the right)
3336Find the Number of Subsequences With Equal GCDHardArrayMathDP over (gcd of seq1, gcd of seq2); each number goes to seq1, seq2 or neither
3700Number of ZigZag Arrays IIHardMatrixMathDP over the last value and direction, folded by symmetry into one vector; matrix power for n up to 10⁹
3743Maximize Cyclic Partition ScoreHardArrayRange = pick one + and one − element per part (a stock-trading DP); handle the cycle by cutting just before or after the maximum
3753Total Waviness of Numbers in Range IIHardMathDigit DP returning (count of numbers, total waviness) with state (pos, last two digits, tight, started)
3962Maximum Subarray Sum After at Most K SwapsHardArrayHeapFix the left end; as the window grows, keep U = window ∪ best k outside values; answer = sum(U) − its smallest m
115Distinct SubsequencesHardStringways[j] = ways to form t[:j]; for each char of s, update j from right to left when s[i] == t[j−1]
472Concatenated WordsHardStringHash SetProcess words by length; Word Break DP against the shorter words seen so far
689Maximum Sum of 3 Non-Overlapping SubarraysHardArrayWindow sums; best left window index up to i and best right window from i; try every middle window
805Split Array With Same AverageHardArraySame average ⇔ some subset of size k sums to total·k/n; subset-sum DP per size using bitsets
940Distinct Subsequences IIHardStringends[c] = number of distinct subsequences ending with c; a new c makes it (sum of all ends) + 1
1000Minimum Cost to Merge StonesHardArrayInterval 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
1335Minimum Difficulty of a Job ScheduleHardArraydp[d][i] = min over the last day's start j of dp[d−1][j] + max(jobs[j..i−1])
1478Allocate MailboxesHardArraySort houses; one mailbox for houses i..j goes at their median; dp[k][j] = best for the first j houses with k boxes
1494Parallel Courses IIHardGraphArrayBitmask DP over taken courses; each semester takes a subset (≤ k) of the currently available courses
1547Minimum Cost to Cut a StickHardArrayInterval DP on the sorted cut positions (with the ends added): cost(i, j) = (P_j − P_i) + min over the first cut k
1691Maximum Height by Stacking Cuboids HardArraySort each cuboid's sides (tallest as height), sort cuboids, then LIS-style DP on 3D dominance
2262Total Appeal of A StringHardStringCount each character where it's the first occurrence of its letter in a substring: (i − last[c]) · (n − i)
2272Substring With Largest VarianceHardStringFor each ordered letter pair (a, b): Kadane on +1 for a, −1 for b, requiring at least one b
2742Painting the WallsHardArrayKnapsack: a paid painter on wall i covers 1 + time[i] walls (the free painter works meanwhile); min cost to cover n
3130Find All Possible Stable Binary Arrays IIHardArraydp0/dp1[i][j] = arrays with i zeros, j ones ending in 0/1; subtract arrays whose last run would exceed limit
3995Minimum Cost to Convert String IIIHardStringPositions are used at most once, so rules apply to disjoint original segments: dp over prefixes
2719Count of IntegersHardStringMathDigit DP f(N) = numbers ≤ N with digit sum in range; answer f(num2) − f(num1) + (num1 is good)
2472Maximum Number of Non-overlapping Palindrome SubstringsHardStringdp[i] = best for s[:i]; only palindromes of length k or k + 1 ending at i are worth taking
2977Minimum Cost to Convert String IIHardStringGraphFloyd–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 Math

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?

Example 1
Input: n = 2
Output: 2
Explanation: There are two ways to climb to the top. 1. 1 step + 1 step 2. 2 steps
Example 2
Input: n = 3
Output: 3
Explanation: There are three ways to climb to the top. 1. 1 step + 1 step + 1 step 2. 1 step + 2 steps 3. 2 steps + 1 step

Constraints: 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):

  1. State: ways(i) = number of ways to stand on step i.
  2. Last decision: the final move was either 1 step (from i−1) or 2 steps (from i−2), so ways(i) = ways(i−1) + ways(i−2).
  3. Base: ways(0) = 1 (stand still) and ways(1) = 1.
  4. 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:

  1. prev2 and prev1 hold the number of ways to reach the two steps just below the current one. Both start at 1 (ways(0) and ways(1)).
  2. 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).
  3. Then the window slides: prev2 takes the old prev1, and prev1 takes ways_here.
  4. If n is 1 the loop never runs and prev1 = 1 is returned. Otherwise prev1 ends as ways(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:

stepways_here = prev1 + prev2new prev2new prev1
start—11
2212
3323
4535
5858

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 Array

Problem: House Robber (198), but the houses stand in a circle: the first and last houses are neighbors.

Example 1
Input: nums = [2,3,2]
Output: 3
Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2), because they are adjacent houses.
Example 2
Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3). Total amount you can rob = 1 + 3 = 4.

Constraints: 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):

  1. If there is only one house, return its value.
  2. Case A: ignore the last house and run the linear robber on nums[:-1].
  3. Case B: ignore the first house and run it on nums[1:].
  4. 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:

  1. With a single house there is no conflict, so return its value. (Both slices below would be empty and give 0.)
  2. 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 inside nums[1:] (all but the first).
  3. 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.
  4. rob_line keeps prev2 and prev1, picks the better of skipping or robbing each house, and slides the window.
  5. 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.

casemoneynew prev2new prev1
A: nums[:-1]101
A: nums[:-1]212
A: nums[:-1]324
B: nums[1:]202
B: nums[1:]323
B: nums[1:]133

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 Array

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.

Example 1
Input: amount = 5, coins = [1,2,5]
Output: 4
Explanation: there are four ways to make up the amount: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1
Example 2
Input: amount = 3, coins = [2]
Output: 0
Explanation: the amount of 3 cannot be made up just with coins of 2.

Constraints: 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):

  1. State: dp[a] = number of ways to make a using the coins processed so far.
  2. Base: dp[0] = 1 (one way to make 0: take nothing).
  3. Coins in the outer loop, amounts in the inner loop going forwards: dp[a] += dp[a − c].
  4. 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:

  1. dp counts 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.
  2. The outer loop goes over coins. After finishing coin number i, dp counts combinations that use only the first i + 1 coins. Because coins are added in a fixed order, each combination is counted once.
  3. The inner loop goes forwards over amounts from coin to amount. dp[sub_amount - coin] may already include this coin, so taking it again is allowed (unlimited supply).
  4. Each update adds the ways that make sub_amount - coin and then finish with this coin.
  5. 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 =012345
start100000
after coin = 1111111
after coin = 2112233
after coin = 5112234

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 String

Problem: Return the length of the longest subsequence that appears in both strings (characters in order, not necessarily contiguous).

Example 1
Input: text1 = "abcde", text2 = "ace"
Output: 3
Explanation: The longest common subsequence is "ace" and its length is 3.
Example 2
Input: text1 = "abc", text2 = "abc"
Output: 3
Explanation: The longest common subsequence is "abc" and its length is 3.

Constraints: 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):

  1. State: dp[i][j] = LCS length of a[:i] and b[:j]. The table is (m+1) × (n+1), and row 0 / column 0 (empty prefixes) are 0.
  2. If a[i−1] == b[j−1], both last characters join the LCS: dp[i][j] = dp[i−1][j−1] + 1.
  3. Otherwise drop one of the two last characters: dp[i][j] = max(dp[i−1][j], dp[i][j−1]).
  4. 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:

  1. len1 and len2 are the lengths (m and n in the steps).
  2. 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.
  3. Cell dp[i][j] describes text1[:i] and text2[:j], so their last characters are text1[i - 1] and text2[j - 1].
  4. If they are equal, both join the common subsequence: take the diagonal and add 1.
  5. 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 of text2 (cell to the left).
  6. 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 →""ace
""0000
a0111
b0111
c0122
d0122
e0123

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 String

Problem: Implement matching with '.' (any single character) and '*' (zero or more of the preceding element). The pattern must match the entire string.

Example 1
Input: s = "aa", p = "a"
Output: false
Explanation: "a" does not match the entire string "aa".
Example 2
Input: s = "aa", p = "a*"
Output: true
Explanation: '*' means zero or more of the preceding element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".

Constraints: 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):

  1. State: dp[i][j] = does s[:i] match p[:j]? Base: dp[0][0] = True.
  2. Empty string vs pattern: dp[0][j] = dp[0][j−2] when p[j−1] == '*'. Patterns like a*b* can match nothing.
  3. If p[j−1] is a letter or '.': dp[i][j] = dp[i−1][j−1] and the characters match.
  4. If p[j−1] == '*': either use "x*" zero times (dp[i][j−2]), or, if s[i−1] matches x, eat one more character with it (dp[i−1][j]).
  5. 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:

  1. The table has one row per prefix of s and one column per prefix of p, all starting as False, built row by row with append. dp[0][0] = True because an empty pattern matches an empty string.
  2. Row 0 (empty string): a pattern can only match nothing if it is made of x* pairs. When p[j - 1] is *, copy the answer from two columns back (drop the pair).
  3. For a * at p[j - 1], the element it repeats is p[j - 2]. zero_times drops the pair entirely. one_more says: if s[i - 1] matches that element, let the star swallow it and look at dp[i - 1][j] (same pattern, string one character shorter), because the star can still be used again.
  4. For a plain letter or ., the last characters must match and the shorter prefixes (dp[i - 1][j - 1]) must match too.
  5. The helper method chars_match handles the . wildcard so the main loop stays readable.
  6. 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
""TFTFTF
aFFFTTF
aFFFFTF
bFFFFFT

Edge cases to test:

  • .* matches anything, including an empty string.
  • a* matching zero as.
  • 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 Array

Problem: Make at most two transactions (buy then sell, never holding two shares at once). Maximize profit.

Example 1
Input: prices = [3,3,5,0,0,3,1,4]
Output: 6
Explanation: Buy on day 4 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3. Then buy on day 7 (price = 1) and sell on day 8 (price = 4), profit = 4-1 = 3.
Example 2
Input: prices = [1,2,3,4,5]
Output: 4
Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4. Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are engaging multiple transactions at the same time. You must sell before buying again.

Constraints: 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):

  1. buy1 = best balance after the first buy, sell1 = after the first sell, buy2 = after the second buy, sell2 = after the second sell.
  2. Start with buy1 = buy2 = −∞ (not possible yet) and sell1 = sell2 = 0.
  3. For each price p: buy1 = max(buy1, −p), sell1 = max(sell1, buy1 + p), buy2 = max(buy2, sell1 − p), sell2 = max(sell2, buy2 + p).
  4. 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.
  5. 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:

  1. 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.
  2. For each day's price, each state either keeps its old value (do nothing today) or moves forward from the previous state: buying subtracts price, selling adds it.
  3. 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.
  4. sell2 is 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:

daypricebuy1sell1buy2sell2
start—−∞0−∞0
03-30-30
13-30-30
25-32-32
300222
400222
530325
610325
740426

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 Math

Problem: F(0) = 0, F(1) = 1, and F(n) = F(n − 1) + F(n − 2) for n > 1. Return F(n).

Example 1
Input: n = 2
Output: 1
Explanation: F(2) = F(1) + F(0) = 1 + 0 = 1.
Example 2
Input: n = 3
Output: 2
Explanation: F(3) = F(2) + F(1) = 1 + 1 = 2.

Constraints: 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:

  1. Handle n < 2 directly: the answer is n.
  2. Keep only the last two values: prev2 = F(i − 2), prev1 = F(i − 1).
  3. For i = 2..n: current = prev1 + prev2, then slide the two variables forward.
  4. Return prev1, which is now F(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:

  1. if n < 2: return n: F(0) = 0 and F(1) = 1 are the base cases.
  2. prev2 = 0, prev1 = 1: we only ever need the last two values, so no array is needed (O(1) space).
  3. current = prev1 + prev2: the recurrence.
  4. prev2 = prev1, prev1 = current: shift both forward. The order matters: overwrite prev2 first, or its new value would be lost.
  5. return prev1: after the loop, prev1 holds F(n).

Dry run on the example:

n = 5. Returns 5.

iprev2prev1current = F(i)
2011
3112
4123
5235

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 Array

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.

Example 1
Input: cost = [10,15,20]
Output: 15
Explanation: You will start at index 1. - Pay 15 and climb two steps to reach the top. The total cost is 15.
Example 2
Input: cost = [1,100,1,1,1,100,1,1,100,1]
Output: 6
Explanation: You will start at index 0. - Pay 1 and climb two steps to reach index 2. - Pay 1 and climb two steps to reach index 4. - Pay 1 and climb two steps to reach index 6. - Pay 1 and climb one step to reach index 7. - Pay 1 and climb two steps to reach index 9. - Pay 1 and climb one step to reach the top. The total cost is 6.

Constraints: 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:

  1. dp[0] = dp[1] = 0: starting on either of the first two stairs is free.
  2. For i = 2..n (where n is the top): you arrive from i − 1 (paying cost[i − 1]) or from i − 2 (paying cost[i − 2]).
  3. dp[i] = min(dp[i − 1] + cost[i − 1], dp[i − 2] + cost[i − 2]).
  4. 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:

  1. dp = [0] * (n + 1): one entry per stair plus the top. Entries 0 and 1 stay 0.
  2. from_one_below = dp[i - 1] + cost[i - 1]: stand on i − 1 as cheaply as possible, pay to leave it, take one step.
  3. from_two_below: the same, from two stairs down.
  4. dp[i] = min(...): the cheaper way in.
  5. return dp[n]: the top. Only dp[i − 1] and dp[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).

ifrom i−1from i−2dp[i]
20 + 15 = 150 + 10 = 1010
3 (top)10 + 20 = 300 + 15 = 1515

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 Array

Problem: Return the largest sum of any non-empty contiguous subarray of nums.

Example 1
Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6
Explanation: The subarray [4,-1,2,1] has the largest sum 6.
Example 2
Input: nums = [1]
Output: 1
Explanation: The subarray [1] has the largest sum 1.

Constraints: 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:

  1. current = best sum of a subarray ending at the current index; best = the overall answer.
  2. For each x: current = max(x, current + x). If the running sum went negative, it only drags x down, so start over at x.
  3. 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:

  1. 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).
  2. current = max(x, current + x): if current is negative, current + x < x, so dropping the past is better.
  3. 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]).

xcurrent + xcurrentbest
-2— (start)-2-2
1-11 (restart)1
-3-2-21
424 (restart)4
-1334
2555
1666
-5116
4556

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 Matrix

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?

Example 1
Input: m = 3, n = 7
Output: 28
Example 2
Input: m = 3, n = 2
Output: 3
Explanation: From the top-left corner, there are a total of 3 ways to reach the bottom-right corner: 1. Right -> Down -> Down 2. Down -> Down -> Right 3. Down -> Right -> Down

Constraints: 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:

  1. Every cell in the first row and first column has exactly 1 path (straight right or straight down).
  2. Fill the rest row by row: dp[r][c] = dp[r − 1][c] + dp[r][c − 1].
  3. 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:

  1. dp = [[1] * n for _ in range(m)]: a fresh row for each r (writing [[1] * n] * m would make every row the same list). The 1s in row 0 and column 0 are already correct.
  2. for r in range(1, m): for c in range(1, n): row by row, so dp[r − 1][c] and dp[r][c − 1] are always ready.
  3. dp[r][c] = dp[r - 1][c] + dp[r][c - 1]: add the two disjoint ways in.
  4. 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:

rowc=0c=1c=2c=3c=4c=5c=6
01111111
11234567
213610152128

Edge cases to test:

  • m = 1 or n = 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 Matrix

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.

Example 1
Input: obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
Output: 2
Explanation: There is one obstacle in the middle of the 3x3 grid above. There are two ways to reach the bottom-right corner: 1. Right -> Right -> Down -> Down 2. Down -> Down -> Right -> Right
Example 2
Input: obstacleGrid = [[0,1],[0,0]]
Output: 1

Constraints: 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:

  1. dp[r][c] = number of paths to (r, c). If the start is blocked, the answer is 0.
  2. Set dp[0][0] = 1, then visit cells row by row.
  3. Obstacle → dp[r][c] = 0. Otherwise add the count from above (if r > 0) and from the left (if c > 0).
  4. 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:

  1. dp = [[0] * cols ...]: start every count at 0.
  2. if obstacleGrid[0][0] == 1: return 0: a blocked start means no paths at all.
  3. dp[0][0] = 1: one way to be at the start.
  4. if obstacleGrid[r][c] == 1: dp[r][c] = 0; continue: an obstacle contributes nothing to its neighbours.
  5. 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):

rowc=0c=1c=2
0111
11X (0)1
2112

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 Array

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.

Example 1
Input: triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
Output: 11
Explanation: The triangle looks like: 2 3 4 6 5 7 4 1 8 3 The minimum path sum from top to bottom is 2 + 3 + 5 + 1 = 11 (underlined above).
Example 2
Input: triangle = [[-10]]
Output: -10

Constraints: 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:

  1. Copy the bottom row into best: from a bottom cell, the cheapest path is the cell itself.
  2. For each row above, going up: best[i] = row[i] + min(best[i], best[i + 1]).
  3. 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:

  1. best = list(triangle[-1]): a copy of the bottom row, so the input is left unchanged.
  2. for r in range(len(triangle) - 2, -1, -1): from the second-to-last row up to row 0.
  3. best[i] = row[i] + min(best[i], best[i + 1]): best[i] and best[i + 1] still describe the row below, the two cells reachable from (r, i). Going left to right is safe because best[i + 1] hasn't been overwritten yet when we read it.
  4. 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 processedbest 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 String

Problem: Letters map to 1..26. Count the ways to decode a digit string.

Example 1
Input: s = "12"
Output: 2
Explanation: "12" could be decoded as "AB" (1 2) or "L" (12).
Example 2
Input: s = "226"
Output: 3
Explanation: "226" could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).

Constraints: 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:

  1. prev2 = 1 (empty prefix), prev1 = ways for s[:1].
  2. For each i: ways = (s[i−1] != '0' ? prev1 : 0) + (10 ≤ int(s[i−2:i]) ≤ 26 ? prev2 : 0).
  3. 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:

  1. A single digit decodes only if it isn't 0.
  2. A pair decodes only in 10..26 (a leading 0 like 06 isn't allowed).
  3. 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 String

Problem: Is s3 formed by interleaving s1 and s2 (keeping each one's order)?

Example 1
Input: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
Output: true
Explanation: One way to obtain s3 is: Split s1 into s1 = "aa" + "bc" + "c", and s2 into s2 = "dbbc" + "a". Interleaving the two splits, we get "aa" + "dbbc" + "bc" + "a" + "c" = "aadbbcbcac". Since s3 can be obtained by interleaving s1 and s2, we return true.
Example 2
Input: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
Output: false
Explanation: Notice how it is impossible to interleave s2 with any other string to obtain s3.

Constraints: 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:

  1. If lengths don't add up, false.
  2. dp[0][0] = True.
  3. 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:

  1. The state (i, j) determines exactly which prefix of s3 has been built.
  2. Both sources are checked, which handles ties where s1 and s2 offer 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 StringHash Set

Problem: Can s be split into a sequence of dictionary words (words reusable)?

Example 1
Input: s = "leetcode", wordDict = ["leet","code"]
Output: true
Explanation: Return true because "leetcode" can be segmented as "leet code".
Example 2
Input: s = "applepenapple", wordDict = ["apple","pen"]
Output: true
Explanation: Return true because "applepenapple" can be segmented as "apple pen apple". Note that you are allowed to reuse a dictionary word.

Constraints: 1 ≤ s.length ≤ 300 · 1 ≤ wordDict.length ≤ 1000 · 1 ≤ wordDict[i].length ≤ 20 · s and wordDict[i] consist of only lowercase English letters. · All 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:

  1. ok[0] = True.
  2. For each end i, for each word length L in the dictionary: if ok[i − L] and s[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:

  1. Only word lengths that actually occur need checking.
  2. 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 Array

Problem: Return the largest product of a non-empty contiguous subarray.

Example 1
Input: nums = [2,3,-2,4]
Output: 6
Explanation: [2,3] has the largest product 6.
Example 2
Input: nums = [-2,0,-1]
Output: 0
Explanation: The result cannot be 2, because [-2,-1] is not a subarray.

Constraints: 1 ≤ nums.length ≤ 2 * 10⁴ · -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:

  1. hi = lo = best = nums[0].
  2. For each x: candidates x, hi · x, lo · x; hi = max, lo = min of them.
  3. 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:

  1. Starting fresh at x handles zeros and bad prefixes.
  2. 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 Matrix

Problem: In a binary matrix, return the area of the largest square containing only 1s.

Example 1
Input: matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
Output: 4
Example 2
Input: matrix = [["0","1"],["1","0"]]
Output: 1

Constraints: 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:

  1. side[r][c] = 0 for a '0'.
  2. Otherwise side[r][c] = 1 + min(side[r−1][c], side[r][c−1], side[r−1][c−1]).
  3. 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:

  1. A square of side s ending at (r, c) needs squares of side s − 1 ending above, left and diagonally.
  2. 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 Math

Problem: Split n ≥ 2 into at least two positive integers to maximise their product.

Example 1
Input: n = 2
Output: 1
Explanation: 2 = 1 + 1, 1 × 1 = 1.
Example 2
Input: n = 10
Output: 36
Explanation: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36.

Constraints: 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:

  1. best[1] = 1.
  2. For i from 2 to n: best[i] = max(j · max(i − j, best[i − j])) over j in 1..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:

  1. max(i − j, best[i − j]) allows the rest to stay whole, since best itself forces at least two parts.
  2. 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 Array

Problem: Return a largest subset where every pair (a, b) satisfies a % b == 0 or b % a == 0.

Example 1
Input: nums = [1,2,3]
Output: [1,2]
Explanation: [1,3] is also accepted.
Example 2
Input: nums = [1,2,4,8]
Output: [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:

  1. Sort. size[i] = longest chain ending at i; parent[i] = previous element.
  2. For each j < i with nums[i] % nums[j] == 0: extend if size[j] + 1 is bigger.
  3. 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:

  1. If a | b and b | c then a | c, so checking only consecutive chain elements is enough.
  2. This is the O(n²) LIS DP with a different link condition.
  3. 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 Array

Problem: Count ordered sequences of numbers from nums (reusable) that add up to target.

Example 1
Input: nums = [1,2,3], target = 4
Output: 7
Explanation: The possible combination ways are: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) Note that different sequences are counted as different combinations.
Example 2
Input: nums = [9], target = 3
Output: 0

Constraints: 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:

  1. ways[0] = 1.
  2. For t from 1 to target: ways[t] = Σ ways[t − x] over x ≤ 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:

  1. Classifying sequences by their last number gives each sequence exactly once.
  2. 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.
  • target smaller than all numbers.
  • One number.

Complexity: O(target · n) time, O(target) space.

Medium 396. Rotate Function Google Meta Amazon ArrayMath

Problem: F(k) = Σ i · arrₖ[i] where arrₖ is nums rotated right by k. Return the maximum F(k).

Example 1
Input: nums = [4,3,2,6]
Output: 26
Explanation: F(0) = (0 * 4) + (1 * 3) + (2 * 2) + (3 * 6) = 0 + 3 + 4 + 18 = 25 F(1) = (0 * 6) + (1 * 4) + (2 * 3) + (3 * 2) = 0 + 4 + 6 + 6 = 16 F(2) = (0 * 2) + (1 * 6) + (2 * 4) + (3 * 3) = 0 + 6 + 8 + 9 = 23 F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4) = 0 + 2 + 12 + 12 = 26 So the maximum value of F(0), F(1), F(2), F(3) is F(3) = 26.
Example 2
Input: nums = [100]
Output: 0

Constraints: 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:

  1. F = Σ i · nums[i], total = sum(nums).
  2. For k = 1..n − 1: F += total − n · nums[n − k].
  3. 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:

  1. Rotating right moves every element one index up (adding its value once more), so F grows by total.
  2. The element at the end wraps to index 0: it had coefficient n − 1, now 0, a loss of n · value after the total was added.
  3. In rotation k the wrapping element is nums[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 Array

Problem: Players alternately take a number from either end. Does player 1 win (ties count as a win) with optimal play?

Example 1
Input: nums = [1,5,2]
Output: false
Explanation: Player 1 must open by taking either 1 or 2, and both choices lead to the same result. If Player 1 takes 1, the array becomes [5,2]. Player 2 takes 5, leaving 2 for Player 1. If Player 1 takes 2, the array becomes [1,5]. Player 2 takes 5, leaving 1 for Player 1. Either way, Player 1 finishes with 1 + 2 = 3 and Player 2 finishes with 5. Player 1 can never win, so return false.
Example 2
Input: nums = [1,5,233,7]
Output: true
Explanation: Player 1 first takes 1, leaving [5,233,7]. Player 2 must then choose between 5 and 7, and no matter which number Player 2 chooses, Player 1 can take 233 on the next turn. Player 1 finishes with a higher score (234) than Player 2 (12), so return true.

Constraints: 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:

  1. diff[i][i] = nums[i].
  2. diff[i][j] = max(nums[i] − diff[i+1][j], nums[j] − diff[i][j−1]).
  3. 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:

  1. Using the difference removes the need to track whose turn it is: the opponent's best difference is subtracted.
  2. Rows go from the bottom up so diff[i + 1][j] is ready before diff[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 ArrayHash Map

Problem: Put + or − before each number; count the expressions that evaluate to target.

Example 1
Input: nums = [1,1,1,1,1], target = 3
Output: 5
Explanation: There are 5 ways to assign symbols to make the sum of nums be target 3. -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3
Example 2
Input: nums = [1], target = 1
Output: 1

Constraints: 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:

  1. ways = {0: 1}.
  2. For each number x: new map with s + x and s − x for every s, adding counts.
  3. 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:

  1. Different sign choices that reach the same sum are merged, so the state space is bounded by the range of sums.
  2. 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 String

Problem: Return the length of the longest palindromic subsequence of s.

Example 1
Input: s = "bbbab"
Output: 4
Explanation: One possible longest palindromic subsequence is "bbbb".
Example 2
Input: s = "cbbd"
Output: 2
Explanation: One possible longest palindromic subsequence is "bb".

Constraints: 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:

  1. L[i][i] = 1.
  2. s[i] == s[j]: L[i][j] = L[i+1][j−1] + 2; else max(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:

  1. Rows from bottom to top make L[i + 1][…] ready first.
  2. When j = i + 1 and 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 String

Problem: Return the minimum number of deletions (in either string) to make word1 and word2 equal.

Example 1
Input: word1 = "sea", word2 = "eat"
Output: 2
Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea".
Example 2
Input: word1 = "leetcode", word2 = "etco"
Output: 4

Constraints: 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:

  1. LCS table: L[i][j] = L[i−1][j−1] + 1 if chars match else max(L[i−1][j], L[i][j−1]).
  2. 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:

  1. The final common string must be a subsequence of both words.
  2. 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 ArrayHash Map

Problem: Buy exactly the needed quantities of up to 6 items using single prices and bundle offers. Return the minimum cost.

Example 1
Input: price = [2,5], special = [[3,0,5],[1,2,10]], needs = [3,2]
Output: 14
Explanation: There are two kinds of items, A and B. Their prices are $2 and $5 respectively. In special offer 1, you can pay $5 for 3A and 0B In special offer 2, you can pay $10 for 1A and 2B. You need to buy 3A and 2B, so you may pay $10 for 1A and 2B (special offer #2), and $4 for 2A.
Example 2
Input: price = [2,3,4], special = [[1,1,0,4],[2,2,1,9]], needs = [1,2,1]
Output: 11
Explanation: The price of A is $2, and $3 for B, $4 for C. You may pay $4 for 1A and 1B, and $9 for 2A ,2B and 1C. You need to buy 1A ,2B and 1C, so you may pay $4 for 1A and 1B (special offer #1), and $3 for 1B, $4 for 1C. You cannot add more items, though only $9 for 2A ,2B and 1C.

Constraints: 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:

  1. cost(needs): start with buying everything at single prices.
  2. For each offer that doesn't exceed any need: offer price + cost(needs − offer).
  3. Memoise on the needs tuple.
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:

  1. Each state is a tuple, so it can be a cache key.
  2. Offers that would overshoot any item are skipped (buying extra isn't allowed).
  3. 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 String

Problem: Count the substrings of s that are palindromes.

Example 1
Input: s = "abc"
Output: 3
Explanation: Three palindromic strings: "a", "b", "c".
Example 2
Input: s = "aaa"
Output: 6
Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".

Constraints: 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:

  1. For each centre (a character, or a gap between two): l, r start there.
  2. 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:

  1. Even center values are letters (odd-length palindromes); odd values are gaps (even length).
  2. 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 Array

Problem: Return how many longest strictly increasing subsequences there are.

Example 1
Input: nums = [1,3,5,4,7]
Output: 2
Explanation: The two longest increasing subsequences are [1, 3, 4, 7] and [1, 3, 5, 7].
Example 2
Input: nums = [2,2,2,2,2]
Output: 5
Explanation: The length of the longest increasing subsequence is 1, and there are 5 increasing subsequences of length 1, so output 5.

Constraints: 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:

  1. length[i] = count[i] = 1.
  2. For j < i with nums[j] < nums[i]: if length[j] + 1 > length[i], take it and copy count[j]; if equal, add count[j].
  3. 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:

  1. count[i] counts distinct index sequences, matching the problem's definition.
  2. 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 Array

Problem: Unlimited transactions, each costing fee. Return the maximum profit.

Example 1
Input: prices = [1,3,2,8,4,9], fee = 2
Output: 8
Explanation: The maximum profit can be achieved by: - Buying at prices[0] = 1 - Selling at prices[3] = 8 - Buying at prices[4] = 4 - Selling at prices[5] = 9 The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
Example 2
Input: prices = [1,3,7,5,10,3], fee = 3
Output: 6

Constraints: 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:

  1. cash = 0, hold = −prices[0].
  2. 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:

  1. Using the updated cash for hold on the same day only allows sell-then-buy on one day, which never helps, so it's safe.
  2. 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 ArrayHash Map

Problem: Taking a number x earns x but deletes every x − 1 and x + 1. Return the maximum points.

Example 1
Input: nums = [3,4,2]
Output: 6
Explanation: You can perform the following operations: - Delete 4 to earn 4 points. Consequently, 3 is also deleted. nums = [2]. - Delete 2 to earn 2 points. nums = []. You earn a total of 6 points.
Example 2
Input: nums = [2,2,3,3,3,4]
Output: 9
Explanation: You can perform the following operations: - Delete a 3 to earn 3 points. All 2's and 4's are also deleted. nums = [3,3]. - Delete a 3 again to earn 3 points. nums = [3]. - Delete a 3 once more to earn 3 points. nums = []. You earn a total of 9 points.

Constraints: 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:

  1. points[v] = v · count(v) for v up to max(nums).
  2. House Robber: take, skip running 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:

  1. Taking one copy of x already deletes all x ± 1, so the other copies of x are free to take too.
  2. 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 Math

Problem: A number is good if rotating each digit 180° gives a valid, different number. Count good numbers in 1..n.

Example 1
Input: n = 10
Output: 4
Explanation: There are four good numbers in the range [1, 10] : 2, 5, 6, 9. Note that 1 and 10 are not good numbers, since they remain unchanged after rotating.
Example 2
Input: n = 1
Output: 0

Constraints: 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:

  1. state[i]: 0 = invalid, 1 = valid and unchanged, 2 = valid and changed. state[0] = 1.
  2. state[i] = invalid if the last digit is 3/4/7 or the prefix is invalid; changed if either is changed; else unchanged.
  3. Count i ≤ n with state[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:

  1. i // 10 is i without its last digit, already computed.
  2. Using 0 < 1 < 2 for bad < same < changed makes max the 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 Math

Problem: Count tilings of a 2 × n board with dominoes and L-trominoes (mod 10⁹ + 7).

Example 1
Input: n = 3
Output: 5
Explanation: The five different ways are shown above.
Example 2
Input: n = 1
Output: 1

Constraints: 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:

  1. full[0] = full[1] = 1, half[0] = 0, half[1] = 1.
  2. full[i] = full[i−1] (vertical domino) + full[i−2] (two horizontal dominoes) + 2 · half[i−2] (a tromino fills the gap and column i).
  3. half[i] = full[i−1] (a tromino covers column i and 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:

  1. The jagged edge has two mirror images (top or bottom cell sticking out); half counts one and the factor 2 covers both.
  2. From a jagged state ending at column i − 2 with one cell in column i − 1, a tromino covers the other cell of i − 1 and all of column i.
  3. 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 Array

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)?

Example 1
Input: poured = 1, query_row = 1, query_glass = 1
Output: 0.00000
Explanation: We poured 1 cup of champange to the top glass of the tower (which is indexed as (0, 0)). There will be no excess liquid so all the glasses under the top glass will remain empty.
Example 2
Input: poured = 2, query_row = 1, query_glass = 1
Output: 0.50000
Explanation: We poured 2 cups of champange to the top glass of the tower (which is indexed as (0, 0)). There is one cup of excess liquid. The glass indexed as (1, 0) and the glass indexed as (1, 1) will share the excess liquid equally, and each will get half cup of champange.

Constraints: 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:

  1. row = [poured].
  2. For each next row: every glass with x > 1 sends (x − 1) / 2 to each child.
  3. 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:

  1. The value stored is the total that flows into a glass, which may exceed 1.
  2. Only the excess over 1 is shared, half to each child.
  3. 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 Array

Problem: Even number of piles, odd total; players take a pile from either end. Does Alice (first) win with optimal play?

Example 1
Input: piles = [5,3,4,5]
Output: true
Explanation: Alice starts first, and can only take the first 5 or the last 5. Say she takes the first 5, so that the row becomes [3, 4, 5]. If Bob takes 3, then the board is [4, 5], and Alice takes 5 to win with 10 points. If Bob takes the last 5, then the board is [3, 4], and Alice takes 4 to win with 9 points. This demonstrated that taking the first 5 was a winning move for Alice, so we return true.
Example 2
Input: piles = [3,7,2,3]
Output: true

Constraints: 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:

  1. diff[i][j] = best (mover − other) on piles i..j.
  2. diff[i][j] = max(piles[i] − diff[i+1][j], piles[j] − diff[i][j−1]).
  3. 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:

  1. Each choice gives the opponent the remaining interval, where they get their own best lead.
  2. 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 Array

Problem: Return the maximum sum of a non-empty subarray of a circular array.

Example 1
Input: nums = [1,-2,3,-2]
Output: 3
Explanation: Subarray [3] has maximum sum 3.
Example 2
Input: nums = [5,-3,5]
Output: 10
Explanation: Subarray [5,5] has maximum sum 5 + 5 = 10.

Constraints: 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:

  1. Kadane for the maximum subarray and for the minimum subarray, in one pass.
  2. If the max is negative (all numbers negative), return it.
  3. 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:

  1. A wrapping subarray leaves out one contiguous middle part; leaving out the smallest one gives the biggest wrap.
  2. If every number is negative, total − min would 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 Matrix

Problem: A falling path takes one cell per row, moving to the same or an adjacent column. Return the minimum path sum.

Example 1
Input: matrix = [[2,1,3],[6,5,4],[7,8,9]]
Output: 13
Explanation: There are two falling paths with a minimum sum as shown.
Example 2
Input: matrix = [[-19,57],[-40,-5]]
Output: -59
Explanation: The falling path with a minimum sum is shown.

Constraints: 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:

  1. prev = first row.
  2. Each next row: cur[c] = matrix[r][c] + min(prev[c − 1], prev[c], prev[c + 1]) (within bounds).
  3. 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:

  1. The slice prev[max(0, c − 1):c + 2] handles both edges.
  2. 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 ArrayHash Set

Problem: Travel days are given; passes cost costs[0] (1 day), costs[1] (7 days), costs[2] (30 days). Return the minimum cost.

Example 1
Input: days = [1,4,6,7,8,20], costs = [2,7,15]
Output: 11
Explanation: For example, here is one way to buy passes that lets you travel your travel plan: On day 1, you bought a 1-day pass for costs[0] = $2, which covered day 1. On day 3, you bought a 7-day pass for costs[1] = $7, which covered days 3, 4, ..., 9. On day 20, you bought a 1-day pass for costs[0] = $2, which covered day 20. In total, you spent $11 and covered all the days of your travel.
Example 2
Input: days = [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15]
Output: 17
Explanation: For example, here is one way to buy passes that lets you travel your travel plan: On day 1, you bought a 30-day pass for costs[2] = $15 which covered days 1, 2, ..., 30. On day 31, you bought a 1-day pass for costs[0] = $2 which covered day 31. In total, you spent $17 and covered all the days of your travel.

Constraints: 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:

  1. cost[0] = 0.
  2. Day d not a travel day: cost[d] = cost[d − 1].
  3. 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:

  1. A pass bought to cover day d can be assumed to end on day d, covering the days before it.
  2. 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 Array

Problem: Split the array into blocks of length ≤ k; each block becomes its maximum. Return the largest possible sum.

Example 1
Input: arr = [1,15,7,9,2,5,10], k = 3
Output: 84
Explanation: arr becomes [15,15,15,9,10,10,10]
Example 2
Input: arr = [1,4,1,5,7,3,6,1,9,9,3], k = 4
Output: 83

Constraints: 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:

  1. best[0] = 0.
  2. For i = 1..n: for L = 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:

  1. Growing the last block one element to the left updates its maximum in O(1).
  2. 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 StringHash Map

Problem: A word is a predecessor of another if inserting one letter makes it. Return the longest chain length.

Example 1
Input: words = ["a","b","ba","bca","bda","bdca"]
Output: 4
Explanation: One of the longest word chains is ["a","ba","bda","bdca"].
Example 2
Input: words = ["xbc","pcxbcf","xb","cxbc","pcxbc"]
Output: 5
Explanation: All the words can be put in a word chain ["xb", "xbc", "cxbc", "pcxbc", "pcxbcf"].

Constraints: 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:

  1. Sort words by length.
  2. For each word, try deleting each position; chain[word] = max(chain.get(shorter, 0) + 1).
  3. 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:

  1. Deleting letters from the longer word generates its only possible predecessors (L of them).
  2. 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 ArrayHash Set

Problem: Smashing stones x ≤ y leaves y − x. Return the smallest possible final weight.

Example 1
Input: stones = [2,7,4,1,8,1]
Output: 1
Explanation: We can combine 2 and 4 to get 2, so the array converts to [2,7,1,8,1] then, we can combine 7 and 8 to get 1, so the array converts to [2,1,1,1] then, we can combine 2 and 1 to get 1, so the array converts to [1,1,1] then, we can combine 1 and 1 to get 0, so the array converts to [1], then that's the optimal value.
Example 2
Input: stones = [31,26,33,21,40]
Output: 5

Constraints: 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:

  1. sums = {0}; for each stone, add s + stone for every reachable s ≤ total / 2.
  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:

  1. Each smash subtracts one group from another, so the end weight is a signed sum of the stones.
  2. The best split has one group as close to total / 2 as 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 Array

Problem: Players take the first X piles (1 ≤ X ≤ 2M), then M = max(M, X). Return the most stones Alice gets with optimal play.

Example 1
Input: piles = [2,7,9,4,4]
Output: 10
Explanation: If Alice takes one pile at the beginning, Bob takes two piles, then Alice takes 2 piles again. Alice can get 2 + 4 + 4 = 10 stones in total. If Alice takes two piles at the beginning, then Bob can take all three piles left. In this case, Alice get 2 + 7 = 9 stones in total. So we return 10 since it's larger.
Example 2
Input: piles = [1,2,3,4,5,100]
Output: 104

Constraints: 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:

  1. suffix[i] = stones from pile i to the end.
  2. best(i, M): if i + 2M ≥ n, take everything: suffix[i].
  3. Else suffix[i] − min(best(i + X, max(M, X))) for X = 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:

  1. Both players share the remaining stones, so the mover gets suffix[i] − what the opponent gets next.
  2. Minimising the opponent's best is the same as maximising your own.
  3. 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 Matrix

Problem: Count all square submatrices that contain only 1s.

Example 1
Input: matrix = [ [0,1,1,1], [1,1,1,1], [0,1,1,1] ]
Output: 15
Explanation: There are 10 squares of side 1. There are 4 squares of side 2. There is 1 square of side 3. Total number of squares = 10 + 4 + 1 = 15.
Example 2
Input: matrix = [ [1,0,1], [1,1,0], [1,1,0] ]
Output: 7
Explanation: There are 6 squares of side 1. There is 1 square of side 2. Total number of squares = 6 + 1 = 7.

Constraints: 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:

  1. For each 1 cell (not in the first row/column): side = 1 + min(up, left, up-left).
  2. Add side to 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:

  1. First row and column cells can only be 1×1 squares, so they keep their value.
  2. A corner with largest square s also 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 Array

Problem: Return the maximum absolute value of the sum of any (possibly empty) subarray.

Example 1
Input: nums = [1,-3,2,3,-4]
Output: 5
Explanation: The subarray [2,3] has absolute sum = abs(2+3) = abs(5) = 5.
Example 2
Input: nums = [2,-5,1,-4,3,-2]
Output: 8
Explanation: The subarray [-5,1,-4] has absolute sum = abs(-5+1-4) = abs(-8) = 8.

Constraints: 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:

  1. Track the max and min subarray sums ending here (allowing restart at 0).
  2. 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:

  1. Empty subarrays are allowed, so both running sums can reset to 0.
  2. 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 Math

Problem: With two eggs and n floors, return the minimum number of drops that always finds the critical floor.

Example 1
Input: n = 2
Output: 2
Explanation: We can drop the first egg from floor 1 and the second egg from floor 2. If the first egg breaks, we know that f = 0. If the second egg breaks but the first egg didn't, we know that f = 1. Otherwise, if both eggs survive, we know that f = 2.
Example 2
Input: n = 100
Output: 14
Explanation: One optimal strategy is: - Drop the 1st egg at floor 9. If it breaks, we know f is between 0 and 8. Drop the 2nd egg starting from floor 1 and going up one at a time to find f within 8 more drops. Total drops is 1 + 8 = 9. - If the 1st egg does not break, drop the 1st egg again at floor 22. If it breaks, we know f is between 9 and 21. Drop the 2nd egg starting from floor 10 and going up one at a time to find f within 12 more drops. Total drops is 2 + 12 = 14. - If the 1st egg does not break again, follow a similar process dropping the 1st egg from floors 34, 45, 55, 64, 72, 79, 85, 90, 94, 97, 99, and 100. Regardless of the outcome, it takes at most 14 drops to determine f.

Constraints: 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:

  1. covered = 0, m = 0.
  2. While covered < n: m += 1, covered += m.
  3. 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:

  1. With m moves, the first drop goes at floor m; a break leaves m − 1 drops for the m − 1 floors below.
  2. No break: the problem repeats above with m − 1 moves, which cover floors[m − 1] more floors.
  3. So floors[m] = 1 + 2 + … + m = m(m + 1)/2; the answer is the first m with 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.
  • n exactly triangular.

Complexity: O(√n) time, O(1) space.

Medium 2140. Solving Questions With Brainpower Google Array

Problem: Solving question i earns points but skips the next brainpower questions. Return the maximum points.

Example 1
Input: questions = [[3,2],[4,3],[4,4],[2,5]]
Output: 5
Explanation: The maximum points can be earned by solving questions 0 and 3. - Solve question 0: Earn 3 points, will be unable to solve the next 2 questions - Unable to solve questions 1 and 2 - Solve question 3: Earn 2 points Total points earned: 3 + 2 = 5. There is no other way to earn 5 or more points.
Example 2
Input: questions = [[1,1],[2,2],[3,3],[4,4],[5,5]]
Output: 7
Explanation: The maximum points can be earned by solving questions 1 and 4. - Skip question 0 - Solve question 1: Earn 2 points, will be unable to solve the next 2 questions - Unable to solve questions 2 and 3 - Solve question 4: Earn 5 points Total points earned: 2 + 5 = 7. There is no other way to earn 7 or more points.

Constraints: 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:

  1. best[n] = 0 (and beyond n counts as 0).
  2. 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:

  1. Each question is either skipped or solved; solving fixes the next index you can use.
  2. 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 Math

Problem: Starting at startPos, take exactly k steps of ±1. Count the step sequences ending at endPos (mod 10⁹ + 7).

Example 1
Input: startPos = 1, endPos = 2, k = 3
Output: 3
Explanation: We can reach position 2 from 1 in exactly 3 steps in three ways: - 1 -> 2 -> 3 -> 2. - 1 -> 2 -> 1 -> 2. - 1 -> 0 -> 1 -> 2. It can be proven that no other way is possible, so we return 3.
Example 2
Input: startPos = 2, endPos = 5, k = 10
Output: 0
Explanation: It is impossible to reach position 5 from position 2 in exactly 10 steps.

Constraints: 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:

  1. d = |endPos − startPos|.
  2. If d > k or k − d is odd → 0.
  3. 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:

  1. Every step is +1 or −1, so the net displacement fixes how many steps go each way.
  2. The order is free: any right-sized subset of the k steps works.
  3. Python's comb is 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 Array

Problem: At most k transactions, each a normal trade (buy then sell) or a short sale (sell then buy back). Return the maximum profit.

Example 1
Input: prices = [1,7,9,8,2], k = 2
Output: 14
Explanation: We can make $14 of profit through 2 transactions: A normal transaction: buy the stock on day 0 for $1 then sell it on day 2 for $9. A short selling transaction: sell the stock on day 3 for $8 then buy back on day 4 for $2.
Example 2
Input: prices = [12,16,19,19,8,1,19,13,9], k = 3
Output: 36
Explanation: We can make $36 of profit through 3 transactions: A normal transaction: buy the stock on day 0 for $12 then sell it on day 2 for $19. A short selling transaction: sell the stock on day 3 for $19 then buy back on day 4 for $8. A normal transaction: buy the stock on day 5 for $1 then sell it on day 6 for $19.

Constraints: 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:

  1. free[0] = 0; everything else −∞.
  2. Each day p (using yesterday's values): open long free[j−1] − p, open short free[j−1] + p; close long long[j] + p, close short short[j] − p.
  3. 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:

  1. Using yesterday's values stops a transaction from closing and the next one opening on the same day.
  2. A short trade earns sell price − buy-back price, mirroring the long trade.
  3. max(free) allows using fewer than k 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:

  • 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 Array

Problem: numWays[i] is the number of ways to make amount i (1-indexed) with unknown coins ≤ n. Recover the coins or return [].

Example 1
Input: numWays = [0,1,0,2,0,3,0,4,0,5]
Output: [2,4,6]
Explanation: Amount Number of ways Explanation 1 0 There is no way to select coins with total value 1. 2 1 The only way is [2]. 3 0 There is no way to select coins with total value 3. 4 2 The ways are [2, 2] and [4]. 5 0 There is no way to select coins with total value 5. 6 3 The ways are [2, 2, 2], [2, 4], and [6]. 7 0 There is no way to select coins with total value 7. 8 4 The ways are [2, 2, 2, 2], [2, 2, 4], [2, 6], and [4, 4]. 9 0 There is no way to select coins with total value 9. 10 5 The ways are [2, 2, 2, 2, 2], [2, 2, 2, 4], [2, 4, 4], [2, 2, 6], and [4, 6]. Example 2: Input: numWays = [1,2,2,3,4] Output: [1,2,5] Explanation: Amount Number of ways Explanation 1 1 The only way is [1]. 2 2 The ways are [1, 1] and [2]. 3 2 The ways are [1, 1, 1] and [1, 2]. 4 3 The ways are [1, 1, 1, 1], [1, 1, 2], and [2, 2]. 5 4 The ways are [1, 1, 1, 1, 1], [1, 1, 1, 2], [1, 2, 2], and [5].
Example 2
Input: numWays = [1,2,3,4,15]
Output: []
Explanation: No set of denomination satisfies this array.

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:

  1. ways = [1, 0, …] for the coins chosen so far.
  2. For each amount i: if numWays[i] == ways[i], not a coin; if == ways[i] + 1, add coin i and update ways for 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:

  1. Amount i can only be influenced by coins ≤ i; coins below i are already decided.
  2. Adding coin i adds ways[0] = 1 to ways[i], so the count must be exactly one more.
  3. 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 Array

Problem: Jump right only to smaller values, left only to larger ones. For each start, return the largest value reachable.

Example 1
Input: nums = [2,1,3]
Output: [2,2,3]
Explanation: For i = 0: No jump increases the value. For i = 1: Jump to j = 0 as nums[j] = 2 is greater than nums[i]. For i = 2: Since nums[2] = 3 is the maximum value in nums, no jump increases the value. Thus, ans = [2, 2, 3].
Example 2
Input: nums = [2,3,1]
Output: [3,3,3]
Explanation: For i = 0: Jump forward to j = 2 as nums[j] = 1 is less than nums[i] = 2, then from i = 2 jump to j = 1 as nums[j] = 3 is greater than nums[2]. For i = 1: Since nums[1] = 3 is the maximum value in nums, no jump increases the value. For i = 2: Jump to j = 1 as nums[j] = 3 is greater than nums[2] = 1. Thus, ans = [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:

  1. pre[i] = max of nums[0..i]; suf[i] = min of nums[i..n−1].
  2. ans[n−1] = pre[n−1].
  3. From the right: ans[i] = ans[i + 1] if pre[i] > suf[i + 1], else pre[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:

  1. Within the prefix, the maximum is reachable: from any index, jump left to a larger value until you reach it.
  2. If every value on the right is ≥ the prefix max, no jump can cross the boundary in either direction.
  3. Otherwise the two sides are connected, so i reaches whatever i + 1 reaches.

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 Array

Problem: Replace at most one element with any value; return the longest non-decreasing subarray possible.

Example 1
Input: nums = [1,2,3,1,2]
Output: 4
Explanation: Replacing nums[3] = 1 with 3 gives the array [1, 2, 3, 3, 2]. The longest non-decreasing subarray is [1, 2, 3, 3], which has a length of 4.
Example 2
Input: nums = [2,2,2,2,2]
Output: 5
Explanation: All elements in nums are equal, so it is already non-decreasing and the entire nums forms a subarray of length 5.

Constraints: 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:

  1. left[i]: length of the non-decreasing run ending at i; right[i]: starting at i.
  2. For each i: L = left[i − 1] (0 if none), R = right[i + 1] (0 if none).
  3. If both exist and nums[i − 1] ≤ nums[i + 1]: candidate L + 1 + R; else max(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:

  1. The replaced element can take any value, so it fits after the left run or before the right run.
  2. It bridges both runs only when the left run's last value ≤ the right run's first value.
  3. 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 Matrix

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.

Example 1
Input: grid = [[0, 1],[2, 0]], k = 1
Output: 2
Explanation: The optimal path is: Cell grid[i][j] Score Total Score Cost Total Cost (0, 0) 0 0 0 0 0 (1, 0) 2 2 2 1 1 (1, 1) 0 0 2 0 1 Thus, the maximum possible score is 2.
Example 2
Input: grid = [[0, 1],[1, 2]], k = 1
Output: -1
Explanation: There is no path that reaches cell (1, 1) without exceeding cost k. Thus, the answer is -1.

Constraints: 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:

  1. K = min(k, m + n); best[c][spent] for the current row (−∞ = impossible).
  2. For each cell, combine the states from above and from the left, then shift by the cell's cost and add its score.
  3. Answer: max over spent ≤ K at 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:

  1. The extra dimension is the cost spent so far; each cell keeps the best score for every possible cost.
  2. A cell with cost 1 shifts the whole list by one position (costs above K are dropped).
  3. Capping K at m + n keeps the lists short when k is 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 Math

Problem: Sum, over every number in [num1, num2], the count of inner digits that are strictly above or below both neighbours.

Example 1
Input: num1 = 120, num2 = 130
Output: 3
Explanation: In the range [120, 130]: 120: middle digit 2 is a peak, waviness = 1. 121: middle digit 2 is a peak, waviness = 1. 130: middle digit 3 is a peak, waviness = 1. All other numbers in the range have a waviness of 0. Thus, total waviness is 1 + 1 + 1 = 3.
Example 2
Input: num1 = 198, num2 = 202
Output: 3
Explanation: In the range [198, 202]: 198: middle digit 9 is a peak, waviness = 1. 201: middle digit 0 is a valley, waviness = 1. 202: middle digit 0 is a valley, waviness = 1. All other numbers in the range have a waviness of 0. Thus, total waviness is 1 + 1 + 1 = 3.

Constraints: 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:

  1. For each x in range, take its digits.
  2. For each inner position i: peak if d[i] > d[i−1] and d[i] > d[i+1]; valley if both are <.
  3. 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:

  1. Digits as characters compare the same way as their values.
  2. 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 Array

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.

Example 1
Input: nums = [1,-2,3,4,-5], k = 2
Output: 14
Explanation: Multiply each number in the subarray [3, 4] by 2. This results in nums = [1, -2, 6, 8, -5]. The subarray with the largest sum is [6, 8], so the output is 6 + 8 = 14.
Example 2
Input: nums = [-5,-4,-3], k = 2
Output: -1
Explanation: Divide each number in the subarray [-3] by 2. This results in nums = [-5, -4, -1]. The subarray with the largest sum is [-1], so the output is -1.

Constraints: 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:

  1. For f in (×k, ÷k): states before (no op yet), inside (op running), after (op finished), each = best sum ending here.
  2. inside = max(before_prev, inside_prev, 0) + f(x); after = max(inside_prev, after_prev) + x; before = max(before_prev, 0) + x.
  3. Best = max over inside and after; also allow a plain subarray that leaves at least one element free (nums[1:] or nums[:-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:

  1. before is ordinary Kadane; inside adds transformed values; after adds plain values once the operated part has ended.
  2. The operated segment may start fresh, after a plain prefix, or continue.
  3. 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 Math

Problem: Count sequences of k positive integers summing to n whose product is even (mod 10⁹ + 7).

Example 1
Input: n = 5, k = 3
Output: 3
Explanation: The sequences of length k = 3 whose sum is 5 are: Sequence Product Parity [1, 1, 3] 1 * 1 * 3 = 3 Odd [1, 2, 2] 1 * 2 * 2 = 4 Even [2, 1, 2] 2 * 1 * 2 = 4 Even [2, 2, 1] 2 * 2 * 1 = 4 Even [1, 3, 1] 1 * 3 * 1 = 3 Odd [3, 1, 1] 3 * 1 * 1 = 3 Odd There are 3 sequences with an even product, thus the answer is 3.
Example 2
Input: n = 3, k = 2
Output: 2
Explanation: The sequences of length k = 2 whose sum is 3 are: Sequence Product Parity [1, 2] 1 * 2 = 2 Even [2, 1] 2 * 1 = 2 Even There are 2 sequences with an even product, thus the answer is 2.

Constraints: 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:

  1. Precompute factorials mod p up to n.
  2. all = C(n − 1, k − 1).
  3. odd = C((n − k)/2 + k − 1, k − 1) if n − k is even, else 0.
  4. 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:

  1. Sequences of k positive integers summing to n correspond to choosing k − 1 cut points among n − 1 gaps.
  2. All parts odd: subtract 1 from each and halve, giving k non-negative numbers summing to (n − k)/2: C((n − k)/2 + k − 1, k − 1).
  3. If n − k is 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 if n is even, else 0.
  • n − k odd (no all-odd sequences).

Complexity: O(n) time and space for the factorial tables.

Hard 32. Longest Valid Parentheses Google Meta Amazon Microsoft String

Problem: Return the length of the longest well-formed parentheses substring.

Example 1
Input: s = "(()"
Output: 2
Explanation: The longest valid parentheses substring is "()".
Example 2
Input: s = ")()())"
Output: 4
Explanation: The longest valid parentheses substring is "()()".

Constraints: 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:

  1. dp[i] = length of the longest valid substring ending exactly at i (0 if s[i] == '(').
  2. s[i] == ')': let j = i − dp[i − 1] − 1 (the position that must hold the matching ().
  3. If j ≥ 0 and s[j] == '(': dp[i] = dp[i − 1] + 2 + dp[j − 1] (glue the block before j).
  4. 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:

  1. If s[i − 1] is (, then dp[i − 1] = 0 and j = i − 1: the simple () case.
  2. Otherwise dp[i − 1] is a valid block (...) ending at i − 1; the ( matching s[i] sits right before it.
  3. 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 String

Problem: Match the whole string s against pattern p, where ? matches one character and * matches any sequence (including empty).

Example 1
Input: s = "aa", p = "a"
Output: false
Explanation: "a" does not match the entire string "aa".
Example 2
Input: s = "aa", p = "*"
Output: true
Explanation: '*' matches any sequence.

Constraints: 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:

  1. dp[0][0] = True; dp[0][j] is true while p[:j] is all *.
  2. p[j−1] == '*': dp[i][j] = dp[i][j−1] or dp[i−1][j].
  3. p[j−1] == '?' or equals s[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:

  1. dp[i][j − 1]: the * stands for the empty sequence.
  2. dp[i − 1][j]: the * swallows s[i − 1] and may keep going.
  3. 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 s with 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 StringHash Map

Problem: Can s2 be obtained from s1 by recursively splitting it into two parts and optionally swapping them?

Example 1
Input: s1 = "great", s2 = "rgeat"
Output: true
Explanation: One possible scenario applied on s1 is: "great" --> "gr/eat" // divide at random index. "gr/eat" --> "gr/eat" // random decision is not to swap the two substrings and keep them in order. "gr/eat" --> "g/r / e/at" // apply the same algorithm recursively on both substrings. divide at random index each of them. "g/r / e/at" --> "r/g / e/at" // random decision was to swap the first substring and to keep the second substring in the same order. "r/g / e/at" --> "r/g / e/ a/t" // again apply the algorithm recursively, divide "at" to "a/t". "r/g / e/ a/t" --> "r/g / e/ a/t" // random decision is to keep both substrings in the same order. The algorithm stops now, and the result string is "rgeat" which is s2. As one possible scenario led s1 to be scrambled to s2, we return true.
Example 2
Input: s1 = "abcde", s2 = "caebd"
Output: false

Constraints: 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:

  1. scramble(a, b): equal → true; different letter counts → false.
  2. For each split k in 1..len − 1: no swap scramble(a[:k], b[:k]) and scramble(a[k:], b[k:]).
  3. Swap: scramble(a[:k], b[−k:]) and scramble(a[k:], b[:−k]).
  4. 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:

  1. The top-level split of s1 produces two parts; in s2 they appear in the same order or swapped.
  2. Letter counts must match for any pair of substrings to be scrambles, which prunes most branches.
  3. 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 String

Problem: Return the minimum number of cuts to split s so every piece is a palindrome.

Example 1
Input: s = "aab"
Output: 1
Explanation: The palindrome partitioning ["aa","b"] could be produced using 1 cut.
Example 2
Input: s = "a"
Output: 0

Constraints: 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:

  1. pieces[i] = min palindromes for s[:i]; pieces[0] = 0, others = i (all single letters).
  2. For each centre (odd and even), expand while s[l] == s[r]: pieces[r + 1] = min(pieces[r + 1], pieces[l] + 1).
  3. Answer: pieces[n] − 1 cuts.
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:

  1. Every palindrome s[l..r] lets a partition of s[:l] be extended to one of s[:r + 1] with one more piece.
  2. Expanding around centres visits each palindrome once, without building an n × n table.
  3. Updates to pieces[r + 1] always use pieces[l] with l ≤ center. Those were final before this centre (every palindrome ending before center comes 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 Matrix

Problem: Moving right or down from top-left to bottom-right, health must stay ≥ 1. Return the minimum starting health.

Example 1
Input: dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]
Output: 7
Explanation: The initial health of the knight must be at least 7 if he follows the optimal path: RIGHT-> RIGHT -> DOWN -> DOWN.
Example 2
Input: dungeon = [[0]]
Output: 1

Constraints: 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:

  1. need[r][c] = minimum health when entering (r, c) to survive to the end.
  2. need beyond the last cell (right of or below it) = 1.
  3. 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:

  1. Working backwards, each cell knows exactly what the rest of the path demands.
  2. A healing cell lowers the requirement but never below 1, since health must stay positive inside the cell too.
  3. The INF border 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 Array

Problem: With at most k transactions (buy then sell, one at a time), return the maximum profit.

Example 1
Input: k = 2, prices = [2,4,1]
Output: 2
Explanation: Buy on day 1 (price = 2) and sell on day 2 (price = 4), profit = 4-2 = 2.
Example 2
Input: k = 2, prices = [3,2,6,5,0,3]
Output: 7
Explanation: Buy on day 2 (price = 2) and sell on day 3 (price = 6), profit = 6-2 = 4. Then buy on day 5 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3.

Constraints: 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:

  1. buy[j] = −∞, sell[j] = 0 for j = 1..k.
  2. Each price p: buy[j] = max(buy[j], sell[j−1] − p); sell[j] = max(sell[j], buy[j] + p).
  3. 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:

  1. buy[j] is the best money left after buying for the j-th time; sell[j] after selling the j-th time.
  2. Updating buy[j] then sell[j] on the same day is harmless (buying and selling on one day gives 0 profit).
  3. 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 Math

Problem: Count the total number of digit 1 appearing in all integers from 0 to n.

Example 1
Input: n = 13
Output: 6
Example 2
Input: n = 0
Output: 0

Constraints: 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:

  1. For p = 1, 10, 100, …: high = n // (10p), cur = (n // p) % 10, low = n % p.
  2. 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:

  1. For a fixed position, each value of the higher digits gives p numbers with a 1 there (the low part runs through 0..p−1).
  2. When the current digit is 1, only low + 1 numbers of the last block are ≤ n.
  3. 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.
  • n with a 1 in a position (partial block).
  • n = 10ᵏ.

Complexity: O(log n) time, O(1) space.

Hard 403. Frog Jump Google Amazon ArrayHash MapHash Set

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?

Example 1
Input: stones = [0,1,3,5,6,8,12,17]
Output: true
Explanation: The frog can jump to the last stone by jumping 1 unit to the 2nd stone, then 2 units to the 3rd stone, then 2 units to the 4th stone, then 3 units to the 6th stone, 4 units to the 7th stone, and 5 units to the 8th stone.
Example 2
Input: stones = [0,1,2,3,4,8,9,11]
Output: false
Explanation: There is no way to jump to the last stone as the gap between the 5th and 6th stone is too large.

Constraints: 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:

  1. jumps[pos] = set() for every stone; jumps[0] = {0}.
  2. For each stone in order, for each k in its set, for step in k − 1, k, k + 1 (> 0): if pos + step is a stone, add step to its set.
  3. 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:

  1. Starting with a "jump of 0" at the first stone makes the first real jump 1 (0 + 1).
  2. Stones are processed left to right, and jumps only go right, so each set is complete before it's used.
  3. Each stone has at most O(n) jump sizes, so there are O(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 Array

Problem: Removing k adjacent equal boxes scores k². Return the maximum total score to remove all boxes.

Example 1
Input: boxes = [1,3,2,2,2,3,4,3,1]
Output: 23
Explanation: [1, 3, 2, 2, 2, 3, 4, 3, 1] ----> [1, 3, 3, 4, 3, 1] (3*3=9 points) ----> [1, 3, 3, 3, 1] (1*1=1 points) ----> [1, 1] (3*3=9 points) ----> [] (2*2=4 points)
Example 2
Input: boxes = [1,1,1]
Output: 9

Constraints: 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:

  1. dp(i, j, k): first absorb a run of equal boxes at the start into k.
  2. Option 1: remove boxes[i] with its k companions now: (k + 1)² + dp(i + 1, j, 0).
  3. Option 2: for each m in i + 1..j with boxes[m] == boxes[i]: clear i + 1..m − 1 first, then boxes[i] joins boxes[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:

  1. k remembers the boxes that will be removed together with boxes[i], wherever they came from.
  2. Removing the middle segment first lets boxes[i] (and its k companions) merge with an equal box at m.
  3. 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 Matrix

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.

Example 1
Input: grid = [[0,1,-1],[1,0,-1],[1,1,1]]
Output: 5
Explanation: The player started at (0, 0) and went down, down, right right to reach (2, 2). 4 cherries were picked up during this single trip, and the matrix becomes [[0,1,-1],[0,0,-1],[0,0,0]]. Then, the player went left, up, up, left to return home, picking up one more cherry. The total number of cherries picked up is 5, and this is the maximum possible.
Example 2
Input: grid = [[1,1,-1],[1,-1,1],[-1,1,1]]
Output: 0

Constraints: 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:

  1. dp[r1][r2] for the current step t (columns are t − r1, t − r2); start dp[0][0] = grid[0][0].
  2. For each step, each pair of rows: take the best of the 4 previous moves (each person came from up or left).
  3. Add cherries at both cells (once if the same); thorns make the state −∞.
  4. 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:

  1. A round trip equals two forward paths, which can be walked in lockstep.
  2. Walking in lockstep means both walkers are on the same anti-diagonal, so a shared cell is detected by r1 == r2.
  3. Keeping r2 ≥ r1 halves the states (the walkers are interchangeable); dp[r1][r2 − 1] with r2 − 1 < r1 is still −∞, so it's harmless.
  4. 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 Array

Problem: Each crime needs group[i] members and earns profit[i]. Count subsets using ≤ n members with total profit ≥ minProfit (mod 10⁹ + 7).

Example 1
Input: n = 5, minProfit = 3, group = [2,2], profit = [2,3]
Output: 2
Explanation: To make a profit of at least 3, the group could either commit crimes 0 and 1, or just crime 1. In total, there are 2 schemes.
Example 2
Input: n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8]
Output: 7
Explanation: To make a profit of at least 5, the group could commit any crimes, as long as they commit one. There are 7 possible schemes: (0), (1), (2), (0,1), (0,2), (1,2), and (0,1,2).

Constraints: 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:

  1. ways[m][p] = subsets using m members with capped profit p; ways[0][0] = 1.
  2. For each crime, iterate m downwards (0/1): ways[m + g][min(P, p + gain)] += ways[m][p].
  3. Answer: Σ ways[m][minProfit] over m ≤ 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:

  1. Iterating members downwards keeps each crime from being counted twice in one pass.
  2. Capping profit merges all "profitable enough" states into one column.
  3. 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 n members.
  • Duplicate crimes.

Complexity: O(crimes · n · minProfit) time, O(n · minProfit) space.

Hard 1092. Shortest Common Supersequence Google String

Problem: Return a shortest string that has both str1 and str2 as subsequences.

Example 1
Input: str1 = "abac", str2 = "cab"
Output: "cabac"
Explanation: str1 = "abac" is a subsequence of "cabac" because we can delete the first "c". str2 = "cab" is a subsequence of "cabac" because we can delete the last "ac". The answer provided is the shortest such string that satisfies these properties.
Example 2
Input: str1 = "aaaaaaaa", str2 = "aaaaaaaa"
Output: "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:

  1. LCS DP table L.
  2. From (m, n): equal letters → output once, move diagonally. Otherwise move toward the larger L neighbour, outputting that string's letter.
  3. 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:

  1. The walk back follows one LCS: matched letters are shared, the rest are copied from their own string.
  2. The result has length m + n − LCS, the minimum possible.
  3. 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 Matrix

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].

Example 1
Input: board = ["E23","2X2","12S"]
Output: [7,1]
Example 2
Input: board = ["E12","1X1","21S"]
Output: [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:

  1. score[S] = 0, ways[S] = 1; process cells from bottom-right to top-left.
  2. For each non-X cell, look at the cell below, to the right, and diagonal. Take the best score; sum the ways of every neighbour with that score.
  3. Add the cell's digit (E and S count 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:

  1. The padding row and column are unreachable (−1), so the borders need no special case.
  2. When neighbours tie on the best score, their path counts add up.
  3. 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:

  • E unreachable → [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 String

Problem: Letters sit on a 6-wide keyboard grid. Type word with two fingers (free initial positions), minimising total Manhattan movement.

Example 1
Input: word = "CAKE"
Output: 3
Explanation: Using two fingers, one optimal way to type "CAKE" is: Finger 1 on letter 'C' -> cost = 0 Finger 1 on letter 'A' -> cost = Distance from letter 'C' to letter 'A' = 2 Finger 2 on letter 'K' -> cost = 0 Finger 2 on letter 'E' -> cost = Distance from letter 'K' to letter 'E' = 1 Total distance = 3
Example 2
Input: word = "HAPPY"
Output: 6
Explanation: Using two fingers, one optimal way to type "HAPPY" is: Finger 1 on letter 'H' -> cost = 0 Finger 1 on letter 'A' -> cost = Distance from letter 'H' to letter 'A' = 2 Finger 2 on letter 'P' -> cost = 0 Finger 2 on letter 'P' -> cost = Distance from letter 'P' to letter 'P' = 0 Finger 1 on letter 'Y' -> cost = Distance from letter 'A' to letter 'Y' = 4 Total distance = 6

Constraints: 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:

  1. dp[o] = min cost with the other finger at o (26 = not placed); start {26: 0}.
  2. To type the next letter c after p: move the same finger (dp[o] + dist(p, c), other stays o) or the other one (dp[o] + dist(o, c), other becomes p).
  3. 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:

  1. The finger that just typed is always on the previous letter, so it needn't be stored.
  2. Each new letter is typed by either finger: two transitions per state.
  3. 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 Array

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.

Example 1
Input: arr = [6,4,14,6,8,13,9,7,10,6,12], d = 2
Output: 4
Explanation: You can start at index 10. You can jump 10 --> 8 --> 6 --> 7 as shown. Note that if you start at index 6 you can only jump to index 7. You cannot jump to index 5 because 13 > 9. You cannot jump to index 4 because index 5 is between index 4 and 6 and 13 > 9. Similarly You cannot jump from index 3 to index 2 or index 1.
Example 2
Input: arr = [3,3,3,3,3], d = 3
Output: 1
Explanation: You can start at any index. You always cannot jump to any index.

Constraints: 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:

  1. visits(i) = 1 + max visits(j) over valid jumps.
  2. Scan right from i up to d steps; stop at the first value ≥ arr[i]. Same to the left.
  3. 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:

  1. A taller-or-equal value blocks every jump beyond it in that direction, so the scan stops there.
  2. Values strictly decrease along jumps, so the recursion always ends.
  3. 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 Array

Problem: Players take 1, 2 or 3 stones from the front (values may be negative). Return "Alice", "Bob" or "Tie".

Example 1
Input: stoneValue = [1,2,3,7]
Output: "Bob"
Explanation: Alice will always lose. Her best move will be to take three piles and the score become 6. Now the score of Bob is 7 and Bob wins.
Example 2
Input: stoneValue = [1,2,3,-9]
Output: "Alice"
Explanation: Alice must choose all the three piles at the first move to win and leave Bob with negative score. If Alice chooses one pile her score will be 1 and the next move Bob's score becomes 5. In the next move, Alice will take the pile with value = -9 and lose. If Alice chooses two piles her score will be 3 and the next move Bob's score becomes 3. In the next move, Alice will take the pile with value = -9 and also lose. Remember that both play optimally so here Alice will choose the scenario that makes her win.

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:

  1. lead[n] = 0.
  2. lead[i] = max(sum(values[i:i+k]) − lead[i + k]) for k = 1..3.
  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:

  1. The difference formulation lets one array describe both players.
  2. 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 Matrix

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).

Example 1
Input: grid = [[3,1,1],[2,5,1],[1,5,5],[2,1,1]]
Output: 24
Explanation: Path of robot #1 and #2 are described in color green and blue respectively. Cherries taken by Robot #1, (3 + 2 + 5 + 2) = 12. Cherries taken by Robot #2, (1 + 5 + 5 + 1) = 12. Total of cherries: 12 + 12 = 24.
Example 2
Input: grid = [[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]]
Output: 28
Explanation: Path of robot #1 and #2 are described in color green and blue respectively. Cherries taken by Robot #1, (1 + 9 + 5 + 2) = 17. Cherries taken by Robot #2, (1 + 3 + 4 + 3) = 11. Total of cherries: 17 + 11 = 28.

Constraints: 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:

  1. dp[c1][c2] for the current row; start with (0, cols − 1).
  2. For the next row, each state tries all 9 combinations of moves.
  3. Gain: grid[r][c1] + grid[r][c2] (once if c1 == 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:

  1. Both robots move one row per step, so a pair of columns fully describes the state.
  2. Unreachable pairs stay −∞ and are skipped.
  3. 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 Math

Problem: Players remove a non-zero square number of stones; the player who can't move loses. Does Alice win with n stones?

Example 1
Input: n = 1
Output: true
Explanation: Alice can remove 1 stone winning the game because Bob doesn't have any moves.
Example 2
Input: n = 2
Output: false
Explanation: Alice can only remove 1 stone, after that Bob removes the last one winning the game (2 -> 1 -> 0).

Constraints: 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:

  1. win[0] = False.
  2. win[i] = any(not win[i − k²]) for k² ≤ 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:

  1. With 0 stones the player to move can't move, so it's a loss.
  2. 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:

  • n a perfect square → true.
  • n = 2 → false.
  • Large n.

Complexity: O(n √n) time, O(n) space.

Hard 2376. Count Special Integers Google MathHash Set

Problem: Count integers in [1, n] whose digits are all distinct (n ≤ 2·10⁹).

Example 1
Input: n = 20
Output: 19
Explanation: All the integers from 1 to 20, except 11, are special. Thus, there are 19 special integers.
Example 2
Input: n = 5
Output: 5
Explanation: All the integers from 1 to 5 are special.

Constraints: 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:

  1. perm(m, r) = ways to arrange r digits out of m available.
  2. Shorter lengths L: 9 · perm(9, L − 1) (no leading zero).
  3. Same length: for position i, for each unused digit d < digits[i] (not 0 at the front): add perm(10 − i − 1, len − i − 1).
  4. If digits[i] is already used, stop; if all positions pass, count n itself.
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:

  1. Shorter numbers are all below n; count them by length.
  2. Choosing a smaller digit at position i (with the same prefix as n) frees all later positions: perm(remaining digits, remaining positions).
  3. 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.
  • n itself has a repeated digit.
  • n = 10ᵏ.

Complexity: O(10 · log n) time, O(1) space.

Hard 2484. Count Palindromic Subsequences Google Microsoft StringArray

Problem: Count palindromic subsequences of length 5 in a digit string (mod 10⁹ + 7).

Example 1
Input: s = "103301"
Output: 2
Explanation: There are 6 possible subsequences of length 5: "10330","10331","10301","10301","13301","03301". Two of them (both equal to "10301") are palindromic.
Example 2
Input: s = "0000000"
Output: 21
Explanation: All 21 subsequences are "00000", which is palindromic.

Constraints: 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:

  1. Suffix pass: for each position, after[i][a][b] = number of ab subsequences in s[i:] (as a running table).
  2. Prefix pass with running before[a][b] and digit counts.
  3. 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:

  1. A palindrome of length 5 is determined by its outer pair (a, b) on the left mirrored as (b, a) on the right.
  2. pairs[x][y] counts subsequences x y and is updated in O(10) per character using single-digit counts.
  3. 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 Matrix

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.

Example 1
Input: 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]]
Output: 11
Explanation: In the first operation, we color all cells in column 1 down to row 3, and in the second operation, we color all cells in column 4 down to the last row. The score of the resulting grid is grid[3][0] + grid[1][2] + grid[3][3] which is equal to 11.
Example 2
Input: grid = [[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]]
Output: 94
Explanation: We perform operations on 1, 2, and 3 down to rows 1, 4, and 0, respectively. The score of the resulting grid is grid[0][0] + grid[1][0] + grid[2][1] + grid[4][1] + grid[1][3] + grid[2][3] + grid[3][3] + grid[4][3] + grid[0][4] which is equal to 94.

Constraints: 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:

  1. col[j][h] = sum of the top h cells of column j.
  2. State after column j: (h, pending); pending means column j will be credited from its right neighbour.
  3. Moving to column j + 1 with height h2: if pending, add max(0, col[j][h2] − col[j][h]). Column j + 1 then either takes credit from the left now (max(0, col[j+1][h] − col[j+1][h2])) or becomes pending.
  4. 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:

  1. Black cells are a top prefix of each column, so a column's score is a difference of prefix sums.
  2. 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.
  3. pending defers column j's gain until column j + 1's height is known.
  4. 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 ArrayMath

Problem: Count pairs of disjoint non-empty subsequences with equal GCD (mod 10⁹ + 7). Values and length ≤ 200.

Example 1
Input: nums = [1,2,3,4]
Output: 10
Explanation: The subsequence pairs which have the GCD of their elements equal to 1 are: ([1, 2, 3, 4], [1, 2, 3, 4]) ([1, 2, 3, 4], [1, 2, 3, 4]) ([1, 2, 3, 4], [1, 2, 3, 4]) ([1, 2, 3, 4], [1, 2, 3, 4]) ([1, 2, 3, 4], [1, 2, 3, 4]) ([1, 2, 3, 4], [1, 2, 3, 4]) ([1, 2, 3, 4], [1, 2, 3, 4]) ([1, 2, 3, 4], [1, 2, 3, 4]) ([1, 2, 3, 4], [1, 2, 3, 4]) ([1, 2, 3, 4], [1, 2, 3, 4])
Example 2
Input: nums = [10,20,30]
Output: 2
Explanation: The subsequence pairs which have the GCD of their elements equal to 10 are: ([10, 20, 30], [10, 20, 30]) ([10, 20, 30], [10, 20, 30])

Constraints: 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:

  1. ways[g1][g2]; start ways[0][0] = 1.
  2. For each x: copy (skip) and add transitions to (gcd(g1, x), g2) and (g1, gcd(g2, x)).
  3. Answer: Σ ways[g][g] for g ≥ 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:

  1. gcd(0, x) = x, so 0 works as the GCD of an empty sequence.
  2. Every assignment of elements to seq1 / seq2 / neither is counted exactly once.
  3. Only g ≥ 1 on 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 MatrixMath

Problem: Count arrays of length n (up to 10⁹) with values in [l, r] that strictly alternate up and down (mod 10⁹ + 7).

Example 1
Input: n = 3, l = 4, r = 5
Output: 2
Explanation: There are only 2 valid ZigZag arrays of length n = 3 using values in the range [4, 5]: [4, 5, 4] [5, 4, 5]
Example 2
Input: n = 3, l = 1, r = 3
Output: 10
Explanation: There are 10 valid ZigZag arrays of length n = 3 using values in the range [1, 3]: [1, 2, 1], [1, 3, 1], [1, 3, 2] [2, 1, 2], [2, 1, 3], [2, 3, 1], [2, 3, 2] [3, 1, 2], [3, 1, 3], [3, 2, 3] All arrays meet the ZigZag conditions.

Constraints: 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:

  1. Shift values to 0..m−1. After 2 elements: up[v] = v (any smaller first value).
  2. One step: up'[v] = Σ_{u<v} down[u] = Σ_{w > m−1−v} up[w] (using down[u] = up[m−1−u]), a fixed matrix A.
  3. up_n = A^(n−2) · up_2; answer 2 · Σ 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:

  1. A zigzag array alternates directions, so the state is the last value and the last direction.
  2. Mirroring every value (v → m − 1 − v) swaps up and down, so down[v] = up[m − 1 − v] always holds.
  3. Applying A to the vector is one step of the DP; binary exponentiation applies it n − 2 times in O(log n) matrix products.
  4. Since matrix powers commute, applying the blocks of A to 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 Array

Problem: Partition a cyclic array into at most k contiguous parts to maximise the sum of (max − min) of each part.

Example 1
Input: nums = [1,2,3,3], k = 2
Output: 3
Explanation: Partition nums into [2, 3] and [3, 1] (wrapped around). The range of [2, 3] is max(2, 3) - min(2, 3) = 3 - 2 = 1. The range of [3, 1] is max(3, 1) - min(3, 1) = 3 - 1 = 2. The score is 1 + 2 = 3.
Example 2
Input: nums = [1,2,3,3], k = 1
Output: 2
Explanation: Partition nums into [1, 2, 3, 3]. The range of [1, 2, 3, 3] is max(1, 2, 3, 3) - min(1, 2, 3, 3) = 3 - 1 = 2. The score is 2.

Constraints: 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:

  1. linear(a): states free[j], plus[j] (picked the max of part j), minus[j] (picked the min first).
  2. Each element x: open plus with free[j−1] + x or minus with free[j−1] − x; close with plus[j] − x or minus[j] + x.
  3. 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:

  1. Any +x − y inside one part is at most its range; choosing its max and min gives exactly the range.
  2. Elements not chosen just belong to some neighbouring part, so the parts stay contiguous.
  3. j goes downwards so that free[j − 1] is still the previous element's value when it's used (and closing uses the old plus/minus).
  4. 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 Math

Problem: Same as Range I but with num2 up to 10¹⁵: sum the peaks and valleys of every number in [num1, num2].

Example 1
Input: num1 = 120, num2 = 130
Output: 3
Explanation: In the range [120, 130]: 120: middle digit 2 is a peak, waviness = 1. 121: middle digit 2 is a peak, waviness = 1. 130: middle digit 3 is a peak, waviness = 1. All other numbers in the range have a waviness of 0. Thus, total waviness is 1 + 1 + 1 = 3.
Example 2
Input: num1 = 198, num2 = 202
Output: 3
Explanation: In the range [198, 202]: 198: middle digit 9 is a peak, waviness = 1. 201: middle digit 0 is a valley, waviness = 1. 202: middle digit 0 is a valley, waviness = 1. All other numbers in the range have a waviness of 0. Thus, total waviness is 1 + 1 + 1 = 3.

Constraints: 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:

  1. f(N) = total waviness of 1..N; answer f(num2) − f(num1 − 1).
  2. go(pos, p2, p1, tight, length) → (count, waviness); length (capped at 2) says how many real digits precede.
  3. Placing digit d with length == 2: p1 is a peak/valley if p2 < p1 > d or p2 > p1 < d; add count_of_rest for 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:

  1. length tracks how many real digits are already placed (0, 1 or 2+), which separates leading zeros from real zeros.
  2. When the third real digit arrives, the middle one (p1) can be judged: a peak or valley adds 1 to each of the c numbers that continue from here.
  3. tight keeps 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 ArrayHeap

Problem: With at most k swaps of any two elements, return the largest possible subarray sum.

Example 1
Input: nums = [1,-1,0,2], k = 1
Output: 3
Explanation: We can swap on indices 1 and 3, resulting in the array [1, 2, 0, -1]. The subarray [1, 2] has a sum of 3, which is the maximum possible subarray sum after at most k = 1 swap.
Example 2
Input: nums = [4,3,2,4], k = 2
Output: 13
Explanation: The maximum possible subarray sum after at most k = 2 swaps is the sum of the entire array, which is 13.

Constraints: 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:

  1. For each left end l: out = all values sorted (window empty); U = top m = min(k, n) values; small = max-heap of U's smallest m (all of U).
  2. Grow r: if nums[r] is among the top m of out, remove it and, if out still has ≥ k values, the new out[−k] joins U; else nums[r] joins U.
  3. Keep small holding the smallest m of U (pop one when m shrinks). The window's answer is sum(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:

  1. 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.
  2. That equals the sum of the L largest values of U (window plus the top min(k, |O|) outside values) — U minus its smallest m values.
  3. was_top uses the sorted out list: x is in the top m if its position is at least len(out) − m (ties are interchangeable).
  4. small is a max-heap (stored negated) of the m smallest values of U; heapreplace swaps in a smaller value, and a pop handles m shrinking.

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 MathArray

Problem: Return the least number of perfect squares that sum to n.

Example 1
Input: n = 12
Output: 3
Explanation: 12 = 4 + 4 + 4.
Example 2
Input: n = 13
Output: 2
Explanation: 13 = 4 + 9.

Constraints: 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:

  1. least[0] = 0.
  2. For x in 1..n: least[x] = 1 + min(least[x − s²]) over s² ≤ x.
  3. 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:

  1. Each x tries every square as the last piece, like coin change.
  2. n itself 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:

  • n itself 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 Math

Problem: Count numbers 0 ≤ x < 10ⁿ whose digits are all distinct.

Example 1
Input: n = 2
Output: 91
Explanation: The answer should be the total numbers in the range of 0 ≤ x < 100, excluding 11,22,33,44,55,66,77,88,99
Example 2
Input: n = 0
Output: 1

Constraints: 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:

  1. total = 1 (the number 0), count = 9 (one-digit numbers 1–9).
  2. For k = 1..n: add count; then count *= 10 − k for 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:

  1. For length k: 9 options for the first digit, then 9, 8, … for the next ones.
  2. 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 StringArray

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?

Example 1
Input: s = "011010", minJump = 2, maxJump = 3
Output: true
Explanation: In the first step, move from index 0 to index 3. In the second step, move from index 3 to index 5.
Example 2
Input: s = "01101110", minJump = 2, maxJump = 3
Output: false

Constraints: 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:

  1. reach[0] = True; window = number of reachable indices in [i − maxJump, i − minJump].
  2. Moving to i: add reach[i − minJump] (entering), remove reach[i − maxJump − 1] (leaving).
  3. 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:

  1. Exactly one index enters and one leaves the range per step, so the count updates in O(1).
  2. 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 String

Problem: Count the distinct subsequences of s that equal t.

Example 1
Input: s = "rabbbit", t = "rabbit"
Output: 3
Explanation: As shown below, there are 3 ways you can generate "rabbit" from s. rabbbit rabbbit rabbbit
Example 2
Input: s = "babgbag", t = "bag"
Output: 5
Explanation: As shown below, there are 5 ways you can generate "bag" from s. babgbag babgbag babgbag babgbag babgbag

Constraints: 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:

  1. ways = [1, 0, …, 0] (one way to form the empty prefix).
  2. For each ch in s: for j from len(t) down to 1: if t[j − 1] == ch, ways[j] += ways[j − 1].
  3. 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:

  1. Skipping ch keeps ways[j] unchanged; using it adds the ways to form t[:j − 1] before it.
  2. 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:

  • t longer than s → 0.
  • Repeated letters (rabbbit).
  • t empty → 1.

Complexity: O(len(s) · len(t)) time, O(len(t)) space.

Medium 241. Different Ways to Add Parentheses Amazon String

Problem: Return all results of fully parenthesising an expression of numbers and + − * (any order).

Example 1
Input: expression = "2-1-1"
Output: [0,2]
Explanation: ((2-1)-1) = 0 (2-(1-1)) = 2
Example 2
Input: expression = "2*3-4*5"
Output: [-34,-14,-10,-10,10]
Explanation: (2*(3-(4*5))) = -34 ((2*3)-(4*5)) = -14 ((2*(3-4))*5) = -10 (2*((3-4)*5)) = -10 (((2*3)-4)*5) = 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:

  1. ways(expr): if it's a number, [int(expr)].
  2. For each operator at i: combine every a in ways(left) with every b in ways(right).
  3. 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:

  1. Every parenthesisation has a last operator; splitting there covers all of them exactly once.
  2. 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 ArrayMath

Problem: Return the n-th number whose prime factors all come from primes (1 counts).

Example 1
Input: n = 12, primes = [2,7,13,19]
Output: 32
Explanation: [1,2,4,7,8,13,14,16,19,26,28,32] is the sequence of the first 12 super ugly numbers given primes = [2,7,13,19].
Example 2
Input: n = 1, primes = [2,3,5]
Output: 1
Explanation: 1 has no prime factors, therefore all of its prime factors are in the array primes = [2,3,5].

Constraints: 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:

  1. ugly = [1], ptr = [0] * k.
  2. Repeat: nxt = min(ugly[ptr[i]] * p_i); append it; advance all pointers whose product equals nxt.
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:

  1. Every super ugly number > 1 is a smaller one times one of the primes.
  2. 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 Array

Problem: Return the length of the longest subsequence whose differences strictly alternate in sign.

Example 1
Input: nums = [1,7,4,9,2,5]
Output: 6
Explanation: The entire sequence is a wiggle sequence with differences (6, -3, 5, -7, 3).
Example 2
Input: nums = [1,17,5,10,13,15,10,5,16,8]
Output: 7
Explanation: There are several subsequences that achieve this length. One is [1, 17, 10, 13, 10, 16, 8] with differences (16, -7, 3, -3, 6, -8).

Constraints: 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:

  1. up = down = 1.
  2. Rise: up = down + 1. Fall: down = up + 1. Equal: nothing.
  3. 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:

  1. Only the latest best up and down matter, because extending with the newest element is never worse.
  2. 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 StringArray

Problem: Pick the largest subset of binary strings using at most m zeros and n ones in total.

Example 1
Input: strs = ["10","0001","111001","1","0"], m = 5, n = 3
Output: 4
Explanation: The largest subset with at most 5 0's and 3 1's is {"10", "0001", "1", "0"}, so the answer is 4. Other valid but smaller subsets include {"0001", "1"} and {"10", "1", "0"}. {"111001"} is an invalid subset because it contains 4 1's, greater than the maximum of 3.
Example 2
Input: strs = ["10","0","1"], m = 1, n = 1
Output: 2
Explanation: The largest subset is {"0", "1"}, so the answer is 2.

Constraints: 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:

  1. dp[i][j] = most strings with ≤ i zeros and ≤ j ones.
  2. For each string with z zeros and o ones: for i from m down to z, j from n down to o: 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:

  1. Iterating capacities from high to low reads the previous item's values, so each string is used once.
  2. 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 Array

Problem: Maximise values[i] + values[j] + i − j over i < j.

Example 1
Input: values = [8,1,5,2,6]
Output: 11
Explanation: i = 0, j = 2, values[i] + values[j] + i - j = 8 + 5 + 0 - 2 = 11
Example 2
Input: values = [1,2]
Output: 2

Constraints: 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:

  1. best_i = values[0].
  2. For j ≥ 1: answer = max(answer, best_i + values[j] − j); then best_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:

  1. The score separates into a term depending only on i and one only on j.
  2. Updating best_i after using it keeps 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:

  • 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 Matrix

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.

Example 1
Input: grid = [[-1,-2,-3],[-2,-3,-3],[-3,-3,-2]]
Output: -1
Explanation: It is not possible to get non-negative product in the path from (0, 0) to (2, 2), so return -1.
Example 2
Input: grid = [[1,-2,1],[1,-2,1],[3,-4,1]]
Output: 8
Explanation: Maximum non-negative product is shown (1 * 1 * -2 * -4 * 1 = 8).

Constraints: 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:

  1. hi[r][c], lo[r][c] = largest / smallest product to reach the cell.
  2. From the top and left neighbours: candidates x · hi, x · lo.
  3. Answer hi[end] % MOD if ≥ 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:

  1. Multiplying by a negative maps the smallest product to the largest, so both extremes are needed.
  2. 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 Array

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.

Example 1
Input: nums = [1,3,6,4,1,2], target = 2
Output: 3
Explanation: To go from index 0 to index n - 1 with the maximum number of jumps, you can perform the following jumping sequence: - Jump from index 0 to index 1. - Jump from index 1 to index 3. - Jump from index 3 to index 5. It can be proven that there is no other jumping sequence that goes from 0 to n - 1 with more than 3 jumps. Hence, the answer is 3.
Example 2
Input: nums = [1,3,6,4,1,2], target = 3
Output: 5
Explanation: To go from index 0 to index n - 1 with the maximum number of jumps, you can perform the following jumping sequence: - Jump from index 0 to index 1. - Jump from index 1 to index 2. - Jump from index 2 to index 3. - Jump from index 3 to index 4. - Jump from index 4 to index 5. It can be proven that there is no other jumping sequence that goes from 0 to n - 1 with more than 5 jumps. Hence, the answer is 5.

Constraints: 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:

  1. dp[0] = 0, others −∞.
  2. For each j, for each i < j with reachable dp[i] and |nums[j] − nums[i]| ≤ target: dp[j] = max(dp[j], dp[i] + 1).
  3. 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:

  1. Jumps only go forward, so indices are already in topological order.
  2. 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 Matrix

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.

Example 1
Input: coins = [[0,1,-1],[1,-2,3],[2,-3,4]]
Output: 8
Explanation: An optimal path for maximum coins is: Start at (0, 0) with 0 coins (total coins = 0). Move to (0, 1), gaining 1 coin (total coins = 0 + 1 = 1). Move to (1, 1), where there's a robber stealing 2 coins. The robot uses one neutralization here, avoiding the robbery (total coins = 1). Move to (1, 2), gaining 3 coins (total coins = 1 + 3 = 4). Move to (2, 2), gaining 4 coins (total coins = 4 + 4 = 8).
Example 2
Input: coins = [[10,10,10],[10,10,10]]
Output: 40
Explanation: An optimal path for maximum coins is: Start at (0, 0) with 10 coins (total coins = 10). Move to (0, 1), gaining 10 coins (total coins = 10 + 10 = 20). Move to (0, 2), gaining another 10 coins (total coins = 20 + 10 = 30). Move to (1, 2), gaining the final 10 coins (total coins = 30 + 10 = 40).

Constraints: 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:

  1. dp[r][c][k] = best total at (r, c) having used k neutralisations.
  2. From up/left with the same k: add the cell value; if negative and k > 0, also from k − 1 adding 0.
  3. 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:

  1. k counts robbers neutralised so far; neutralising this cell moves from k − 1 to k and adds 0.
  2. The start cell can also be a robber, hence the special case.
  3. 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 Array

Problem: Add one value x to every element of a chosen subarray once. Return the maximum possible frequency of k.

Example 1
Input: nums = [1,2,3,4,5,6], k = 1
Output: 2
Explanation: After adding -5 to nums[2..5], 1 has a frequency of 2 in [1, 2, -2, -1, 0, 1].
Example 2
Input: nums = [10,2,3,4,5,5,4,3,2,2], k = 10
Output: 4
Explanation: After adding 8 to nums[1..9], 10 has a frequency of 4 in [10, 10, 11, 12, 13, 13, 12, 11, 10, 10].

Constraints: 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:

  1. base = count(k).
  2. For each value v ≠ k: Kadane over +1 (v), −1 (k), 0 otherwise; best ≥ 0.
  3. 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:

  1. Inside the subarray only one value can be turned into k (one fixed x).
  2. Kadane finds the subarray where the gain #v − #k is 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 Math

Problem: Splitting x into a + b costs a · b. Return the minimum total cost to split n into n ones.

Example 1
Input: n = 3
Output: 3
Explanation: One optimal set of operations is: x a b a + b a * b Cost 3 1 2 3 2 2 2 1 1 2 1 1 Thus, the minimum total cost is 2 + 1 = 3.
Example 2
Input: n = 4
Output: 6
Explanation: One optimal set of operations is: x a b a + b a * b Cost 4 2 2 4 4 4 2 1 1 2 1 1 2 1 1 2 1 1 Thus, the minimum total cost is 4 + 1 + 1 = 6.

Constraints: 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:

  1. 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:

  1. Splitting a + b costs a · b, the number of pairs with one unit on each side.
  2. 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 StringHash Set

Problem: Return every word that is a concatenation of at least two other words from the list (any order).

Example 1
Input: words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]
Output: ["catsdogcats","dogcatsdog","ratcatdogcat"]
Explanation: "catsdogcats" can be concatenated by "cats", "dog" and "cats"; "dogcatsdog" can be concatenated by "dog", "cats" and "dog"; "ratcatdogcat" can be concatenated by "rat", "cat", "dog" and "cat".
Example 2
Input: words = ["cat","dog","catdog"]
Output: ["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:

  1. Sort words by length; known = set().
  2. For each word: ok[0] = True; ok[j] = any(ok[i] and word[i:j] in known).
  3. If ok[len], it's concatenated. Add the word to known.
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:

  1. Because known holds only shorter words, a successful split always uses at least two pieces.
  2. 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 Array

Problem: Pick three non-overlapping subarrays of length k with the maximum total; return their start indices (lexicographically smallest on ties).

Example 1
Input: nums = [1,2,1,2,6,7,5,1], k = 2
Output: [0,3,5]
Explanation: Subarrays [1, 2], [2, 6], [7, 5] correspond to the starting indices [0, 3, 5]. We could have also taken [2, 1], but an answer of [1, 3, 5] would be lexicographically larger.
Example 2
Input: nums = [1,2,1,2,1,2,1,2,1], k = 2
Output: [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:

  1. W[i] = sum of nums[i..i+k−1].
  2. left[i] = index of the best window in [0, i] (earliest on ties); right[i] = best in [i, end] (earliest on ties).
  3. For middle j: total W[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:

  1. left and right are prefix/suffix maxima of window sums, storing indices.
  2. Tie rules (strict on the left, ≥ on the right, strict for the middle) give the lexicographically smallest answer.
  3. 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 Array

Problem: Split nums into two non-empty parts with the same average.

Example 1
Input: nums = [1,2,3,4,5,6,7,8]
Output: true
Explanation: We can split the array into [1,4,5,8] and [2,3,6,7], and both of them have an average of 4.5.
Example 2
Input: nums = [3,1]
Output: false

Constraints: 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:

  1. reach[k] = bitset of sums of size-k subsets; reach[0] = 1.
  2. For each number x, for k downwards: reach[k + 1] |= reach[k] << x.
  3. Check any 1 ≤ k ≤ n/2 with total·k % n == 0 and 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:

  1. If a part has the overall average, the other part does too.
  2. The smaller part has at most n/2 elements, so only those sizes are needed.
  3. A left shift by x adds x to 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 String

Problem: Count distinct non-empty subsequences of s (mod 10⁹ + 7).

Example 1
Input: s = "abc"
Output: 7
Explanation: The 7 distinct subsequences are "a", "b", "c", "ab", "ac", "bc", and "abc".
Example 2
Input: s = "aba"
Output: 6
Explanation: The 6 distinct subsequences are "a", "b", "ab", "aa", "ba", and "aba".

Constraints: 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:

  1. ends = [0] * 26.
  2. For each c: ends[c] = sum(ends) + 1.
  3. 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:

  1. Every subsequence ending in c so far is also counted in the new value (extend its prefix again), so overwriting avoids duplicates.
  2. The + 1 is the single letter c.

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 Array

Problem: Merge exactly k consecutive piles at a time (cost = their total). Return the minimum cost to end with one pile, or −1.

Example 1
Input: stones = [3,2,4,1], k = 2
Output: 20
Explanation: We start with [3, 2, 4, 1]. We merge [3, 2] for a cost of 5, and we are left with [5, 4, 1]. We merge [4, 1] for a cost of 5, and we are left with [5, 5]. We merge [5, 5] for a cost of 10, and we are left with [10]. The total cost was 20, and this is the minimum possible.
Example 2
Input: stones = [3,2,4,1], k = 3
Output: -1
Explanation: After any merge operation, there are 2 piles left, and we can't merge anymore. So the task is impossible.

Constraints: 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:

  1. If (n − 1) % (k − 1), return −1.
  2. dp[i][j] = min(dp[i][m] + dp[m + 1][j]) over m stepping by k − 1 (left part reducible to one pile).
  3. If (j − i) % (k − 1) == 0, add sum(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:

  1. Each merge reduces the pile count by k − 1, which gives the feasibility test.
  2. Stepping m by k − 1 keeps the left part reducible to exactly one pile.
  3. 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 Array

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.

Example 1
Input: jobDifficulty = [6,5,4,3,2,1], d = 2
Output: 7
Explanation: First day you can finish the first 5 jobs, total difficulty = 6. Second day you can finish the last job, total difficulty = 1. The difficulty of the schedule = 6 + 1 = 7
Example 2
Input: jobDifficulty = [9,9,9], d = 4
Output: -1
Explanation: If you finish a job per day you will still have a free day. you cannot find a schedule for the given jobs.

Constraints: 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:

  1. If n < d, −1.
  2. dp[k][i] = best for the first i jobs in k days.
  3. For the last day covering jobs j..i−1, extend j leftwards 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:

  1. Extending the last day leftwards updates its maximum in O(1).
  2. The bound j ≥ k − 1 leaves 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 Array

Problem: Place k mailboxes on a street to minimise the total distance from each house to its nearest mailbox.

Example 1
Input: houses = [1,4,8,10,20], k = 3
Output: 5
Explanation: Allocate mailboxes in position 3, 9 and 20. Minimum total distance from each houses to nearest mailboxes is |3-1| + |4-3| + |9-8| + |10-9| + |20-20| = 5
Example 2
Input: houses = [2,3,5,12,18], k = 2
Output: 9
Explanation: Allocate mailboxes in position 3 and 14. Minimum total distance from each houses to nearest mailboxes is |2-3| + |3-3| + |5-3| + |12-14| + |18-14| = 9.

Constraints: 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:

  1. Sort; cost[i][j] = Σ |h − median| for houses i..j.
  2. dp[b][j] = min over i of dp[b − 1][i] + cost[i][j − 1].
  3. 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:

  1. Each mailbox serves a contiguous block of sorted houses.
  2. 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 GraphArray

Problem: With prerequisites and at most k courses per semester, return the minimum number of semesters (n ≤ 15).

Example 1
Input: n = 4, relations = [[2,1],[3,1],[1,4]], k = 2
Output: 3
Explanation: The figure above represents the given graph. In the first semester, you can take courses 2 and 3. In the second semester, you can take course 1. In the third semester, you can take course 4.
Example 2
Input: n = 5, relations = [[2,1],[3,1],[4,1],[1,5]], k = 2
Output: 4
Explanation: The figure above represents the given graph. In the first semester, you can only take courses 2 and 3 since you cannot take more than two per semester. In the second semester, you can take course 4. In the third semester, you can take course 1. In the fourth semester, you can take course 5.

Constraints: 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:

  1. pre[i] = bitmask of prerequisites.
  2. BFS/DP over masks: available = courses not taken whose pre ⊆ mask.
  3. If popcount(available) ≤ k, take all; else try every submask of size k.
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:

  1. Taking fewer than possible when at most k are available never helps, so all are taken.
  2. Otherwise exactly k (more is not allowed, fewer is never better).
  3. Submask enumeration (sub − 1) & avail visits 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 Array

Problem: Cutting a stick costs its current length. Make all the given cuts in the best order; return the minimum total cost.

Example 1
Input: n = 7, cuts = [1,3,4,5]
Output: 16
Explanation: Using cuts order = [1, 3, 4, 5] as in the input leads to the following scenario: The first cut is done to a rod of length 7 so the cost is 7. The second cut is done to a rod of length 6 (i.e. the second part of the first cut), the third is done to a rod of length 4 and the last cut is to a rod of length 3. The total cost is 7 + 6 + 4 + 3 = 20. Rearranging the cuts to be [3, 5, 1, 4] for example will lead to a scenario with total cost = 16 (as shown in the example photo 7 + 4 + 3 + 2 = 16).
Example 2
Input: n = 9, cuts = [5,6,1,4,2]
Output: 22
Explanation: If you try the given cuts ordering the cost will be 25. There are much ordering with total cost <= 25, for example, the order [4, 6, 5, 2, 1] has total cost = 22 which is the minimum possible.

Constraints: 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:

  1. P = [0] + sorted(cuts) + [n].
  2. dp[i][j] = min cost to make every cut strictly between P[i] and P[j].
  3. dp[i][j] = P[j] − P[i] + min(dp[i][k] + dp[k][j]) for i < 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:

  1. Whatever the first cut inside a piece is, it costs the piece's length.
  2. 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 Array

Problem: Stack cuboids (rotations allowed) where each one's width, length and height are ≤ those below. Maximise the total height.

Example 1
Input: cuboids = [[50,45,20],[95,37,53],[45,23,12]]
Output: 190
Explanation: Cuboid 1 is placed on the bottom with the 53x37 side facing down with height 95. Cuboid 0 is placed next with the 45x20 side facing down with height 50. Cuboid 2 is placed next with the 23x12 side facing down with height 45. The total height is 95 + 50 + 45 = 190.
Example 2
Input: cuboids = [[38,25,45],[76,35,3]]
Output: 76
Explanation: You can't place any of the cuboids on the other. We choose cuboid 1 and rotate it so that the 35x3 side is facing down and its height is 76.

Constraints: 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:

  1. Sort each cuboid's three numbers; sort the list.
  2. dp[i] = c[i][2] + max(dp[j]) for j < i with all three dims of j ≤ those of i.
  3. 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:

  1. 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.
  2. 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 String

Problem: The appeal of a string is its number of distinct letters. Sum the appeal of all substrings.

Example 1
Input: s = "abbca"
Output: 28
Explanation: The following are the substrings of "abbca": - Substrings of length 1: "a", "b", "b", "c", "a" have an appeal of 1, 1, 1, 1, and 1 respectively. The sum is 5. - Substrings of length 2: "ab", "bb", "bc", "ca" have an appeal of 2, 1, 2, and 2 respectively. The sum is 7. - Substrings of length 3: "abb", "bbc", "bca" have an appeal of 2, 2, and 3 respectively. The sum is 7. - Substrings of length 4: "abbc", "bbca" have an appeal of 3 and 3 respectively. The sum is 6. - Substrings of length 5: "abbca" has an appeal of 3. The sum is 3. The total sum is 5 + 7 + 7 + 6 + 3 = 28.
Example 2
Input: s = "code"
Output: 20
Explanation: The following are the substrings of "code": - Substrings of length 1: "c", "o", "d", "e" have an appeal of 1, 1, 1, and 1 respectively. The sum is 4. - Substrings of length 2: "co", "od", "de" have an appeal of 2, 2, and 2 respectively. The sum is 6. - Substrings of length 3: "cod", "ode" have an appeal of 3 and 3 respectively. The sum is 6. - Substrings of length 4: "code" has an appeal of 4. The sum is 4. The total sum is 4 + 6 + 6 + 4 = 20.

Constraints: 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:

  1. last[c] = −1.
  2. For each i: add (i − last[c]) · (n − i); set last[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:

  1. A substring [l, r] counts letter c exactly once, at its first occurrence inside.
  2. That occurrence is i when last[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 String

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.

Example 1
Input: s = "aababbb"
Output: 3
Explanation: All possible variances along with their respective substrings are listed below: - Variance 0 for substrings "a", "aa", "ab", "abab", "aababb", "ba", "b", "bb", and "bbb". - Variance 1 for substrings "aab", "aba", "abb", "aabab", "ababb", "aababbb", and "bab". - Variance 2 for substrings "aaba", "ababbb", "abbb", and "babb". - Variance 3 for substring "babbb". Since the largest possible variance is 3, we return it.
Example 2
Input: s = "abcde"
Output: 0
Explanation: No letter occurs more than once in s, so the variance of every substring is 0.

Constraints: 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:

  1. For each ordered pair a ≠ b (both present):
  2. Kadane with cur, has_b, and started_with_b tracking; when cur goes negative, restart (but remember a b was just seen).
  3. 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:

  1. With a as +1 and b as −1, the window value is count(a) − count(b).
  2. A window must contain a b (otherwise b's count is 0 and it isn't present); the has_b flag enforces that.
  3. Restarting at a b keeps that b in the window (value −1) so later as still count with one b present.

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 Array

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.

Example 1
Input: cost = [1,2,3,2], time = [1,2,3,2]
Output: 3
Explanation: The walls at index 0 and 1 will be painted by the paid painter, and it will take 3 units of time; meanwhile, the free painter will paint the walls at index 2 and 3, free of cost in 2 units of time. Thus, the total cost is 1 + 2 = 3.
Example 2
Input: cost = [2,3,4,2], time = [1,1,1,1]
Output: 4
Explanation: The walls at index 0 and 3 will be painted by the paid painter, and it will take 2 units of time; meanwhile, the free painter will paint the walls at index 1 and 2, free of cost in 2 units of time. Thus, the total cost is 2 + 2 = 4.

Constraints: 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:

  1. dp[c] = min cost to cover c walls (capped at n); dp[0] = 0.
  2. For each wall, for c from n down: dp[c] = min(dp[c], dp[max(0, c − 1 − time[i])] + cost[i]).
  3. 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:

  1. Covering more than n walls is the same as covering n, hence the cap at 0 with max.
  2. 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 Array

Problem: Count binary arrays with exactly zero zeros and one ones where no run of equal bits is longer than limit (mod 10⁹ + 7).

Example 1
Input: zero = 1, one = 1, limit = 2
Output: 2
Explanation: The two possible stable binary arrays are [1,0] and [0,1].
Example 2
Input: zero = 1, one = 2, limit = 1
Output: 1
Explanation: The only possible stable binary array is [1,0,1].

Constraints: 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:

  1. dp0[i][0] = 1 for 1 ≤ i ≤ limit; dp1[0][j] = 1 for 1 ≤ j ≤ limit.
  2. dp0[i][j] = dp0[i−1][j] + dp1[i−1][j] − dp1[i−limit−1][j] (last term when i > limit).
  3. dp1 symmetric; answer dp0 + dp1 at (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:

  1. Appending 0 to every valid array with i − 1 zeros counts all candidates; the only invalid ones end with exactly limit + 1 zeros.
  2. Those are a valid array ending in 1 with i − limit − 1 zeros, followed by limit + 1 zeros: dp1[i − limit − 1][j] of them.
  3. 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 String

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.

Example 1
Input: source = "hello", target = "world", rules = [["he","wo"],["llo","rld"]], costs = [3,4]
Output: 7
Explanation: Apply rules[0] to replace "he" with "wo" at cost 3, so the string becomes "wollo". Apply rules[1] to replace "llo" with "rld" at cost 4, so the string becomes "world". The total cost is 3 + 4 = 7.
Example 2
Input: source = "cat", target = "dog", rules = [["c*t","dog"]], costs = [2]
Output: 3
Explanation: Apply rules[0] to replace "cat" with "dog". The wildcard '*' matches 'a', adding 1 to the base cost 2. The total cost is 2 + 1 = 3.

Constraints: 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:

  1. dp[0] = 0.
  2. If source[i] == target[i], dp[i + 1] = min(dp[i + 1], dp[i]).
  3. For each rule of length L starting at i: pattern matches source[i:i+L] (with *) and replacement equals target[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:

  1. A rewritten block can't be changed again, so its replacement must already equal the target.
  2. Rule blocks never overlap and always match the original characters, so the DP over prefixes covers every valid plan.
  3. 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 StringMath

Problem: Count integers in [num1, num2] (up to 10²²) whose digit sum is between min_sum and max_sum (mod 10⁹ + 7).

Example 1
Input: num1 = "1", num2 = "12", min_sum = 1, max_sum = 8
Output: 11
Explanation: There are 11 integers whose sum of digits lies between 1 and 8 are 1,2,3,4,5,6,7,8,10,11, and 12. Thus, we return 11.
Example 2
Input: num1 = "1", num2 = "5", min_sum = 1, max_sum = 5
Output: 5
Explanation: The 5 integers whose sum of digits lies between 1 and 5 are 1,2,3,4, and 5. Thus, we return 5.

Constraints: 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:

  1. f(N) counts 0..N with digit sum in range: memoised go(pos, sum, tight).
  2. Answer f(num2) − f(num1) plus 1 if num1 itself 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:

  1. tight means the prefix equals N's prefix, so the next digit is capped.
  2. Stopping when the sum exceeds max_sum prunes most states.
  3. f(num2) − f(num1) counts (num1, num2], so num1 is 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 Math

Problem: Players alternately take unused numbers 1..maxChoosableInteger; whoever makes the running total reach desiredTotal wins. Can the first player force a win?

Example 1
Input: maxChoosableInteger = 10, desiredTotal = 11
Output: false
Explanation: No matter which integer the first player choose, the first player will lose. The first player can choose an integer from 1 up to 10. If the first player choose 1, the second player can only choose integers from 2 up to 10. The second player will win by choosing 10 and get a total = 11, which is >= desiredTotal. Same with other integers chosen by the first player, the second player will always win.
Example 2
Input: maxChoosableInteger = 10, desiredTotal = 0
Output: true

Constraints: 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:

  1. If the sum of all numbers < total, false; if total ≤ 0, true.
  2. win(mask, remaining): true if some unused x ≥ remaining, or some x with not 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:

  1. remaining is determined by used, so the cache size is bounded by 2^m.
  2. 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 Math

Problem: Starting at 0, draw uniform points 1..maxPts until reaching at least k. Return the probability of ending with at most n points.

Example 1
Input: n = 10, k = 1, maxPts = 10
Output: 1.00000
Explanation: Alice gets a single card, then stops.
Example 2
Input: n = 6, k = 1, maxPts = 10
Output: 0.60000
Explanation: Alice gets a single card, then stops. In 6 out of 10 possibilities, she is at or below 6 points.

Constraints: 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:

  1. If k == 0 or n ≥ k − 1 + maxPts, return 1.
  2. P[0] = 1, window = 1.
  3. For x = 1..n: P[x] = window / maxPts; add P[x] to the window if x < k; drop P[x − maxPts] if it was in the window.
  4. 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:

  1. Only totals below k keep drawing, so only they feed later totals.
  2. 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 Array

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.

Example 1
Input: items = [[6,2],[2,6],[3,4]], budget = 9
Output: 4
Explanation: You can buy 2 copies of item 0 and 1 copy of item 2 for a total cost of 2 * 2 + 4 = 8, which is not greater than budget = 9. Buying item 2 gives 1 free copy of item 0, because factor2 = 3 divides factor0 = 6. You leave with 3 purchased copies and 1 free copy, for a total of 4 item copies.
Example 2
Input: items = [[2,4],[3,2],[4,1],[6,4],[12,4]], budget = 8
Output: 10
Explanation: You can buy 1 copy of item 0, 1 copy of item 1, and 2 copies of item 2 for a total cost of 4 + 2 + 2 * 1 = 8. Buying item 0 gives 1 free copy of items 2, 3, and 4. Buying item 1 gives 1 free copy of items 3 and 4. Buying item 2 gives 1 free copy of item 4. Thus, you receive 6 free copies. You leave with 4 purchased copies and 6 free copies, for a total of 10 item copies.

Constraints: 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:

  1. bonus_i = number of j ≠ i with factor_i | factor_j.
  2. Knapsack dp[b] = best value of first copies costing exactly b.
  3. Answer: max(dp[b] + (budget − b) // min_price) over reachable b.
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:

  1. Free copies depend only on which types are bought, not how many copies.
  2. Any additional purchased copy is worth exactly 1, so the cheapest item is the best extra (whether or not it was a first copy).
  3. 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 String

Problem: Select the most non-overlapping palindromic substrings, each of length at least k.

Example 1
Input: s = "abaccdbbd", k = 3
Output: 2
Explanation: We can select the substrings underlined in s = "abaccdbbd". Both "aba" and "dbbd" are palindromes and have a length of at least k = 3. It can be shown that we cannot find a selection with more than two valid substrings.
Example 2
Input: s = "adbcda", k = 2
Output: 0
Explanation: There is no palindrome substring of length at least 2 in the string.

Constraints: 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:

  1. dp[0] = 0; dp[i] = dp[i − 1] (skip a character).
  2. If s[i−k:i] is a palindrome: dp[i] = max(dp[i], dp[i − k] + 1); same for length k + 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:

  1. Replacing a long palindrome by the shorter one in its middle frees characters and keeps the count.
  2. 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 StringGraph

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.

Example 1
Input: 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]
Output: 28
Explanation: To convert "abcd" to "acbe", do the following operations: - Change substring source[1..1] from "b" to "c" at a cost of 5. - Change substring source[2..2] from "c" to "e" at a cost of 1. - Change substring source[2..2] from "e" to "b" at a cost of 2. - Change substring source[3..3] from "d" to "e" at a cost of 20. The total cost incurred is 5 + 1 + 2 + 20 = 28. It can be shown that this is the minimum possible cost.
Example 2
Input: source = "abcdefgh", target = "acdeeghh", original = ["bcd","fgh","thh"], changed = ["cde","thh","ghh"], cost = [1,3,5]
Output: 9
Explanation: To convert "abcdefgh" to "acdeeghh", do the following operations: - Change substring source[1..3] from "bcd" to "cde" at a cost of 1. - Change substring source[5..7] from "fgh" to "thh" at a cost of 3. We can do this operation because indices [5,7] are disjoint with indices picked in the first operation. - Change substring source[5..7] from "thh" to "ghh" at a cost of 5. We can do this operation because indices [5,7] are disjoint with indices picked in the first operation, and identical with indices picked in the second operation. The total cost incurred is 1 + 3 + 5 = 9. It can be shown that this is the minimum possible cost.

Constraints: 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:

  1. Give each distinct string an id; dist[a][b] = min rule cost; Floyd–Warshall (only same-length strings connect).
  2. dp[0] = 0; if source[i] == target[i], dp[i + 1] = dp[i].
  3. For each rule length L: if both slices have ids and dist is 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:

  1. 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.
  2. Different blocks never overlap, so the string splits into converted blocks and untouched equal characters: the prefix DP.
  3. 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 Matrix

Problem: A knight makes k random moves (each of 8 directions equally likely). Return the probability it is still on the board.

Example 1
Input: n = 3, k = 2, row = 0, column = 0
Output: 0.06250
Explanation: There are two moves (to (1,2), (2,1)) that will keep the knight on the board. From each of those positions, there are also two moves that will keep the knight on the board. The total probability the knight stays on the board is 0.0625.
Example 2
Input: n = 1, k = 0, row = 0, column = 0
Output: 1.00000

Constraints: 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:

  1. prob[row][column] = 1.
  2. Repeat k times: new grid, each cell spreads p / 8 to its on-board knight targets.
  3. 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:

  1. Each move multiplies by 1/8 per direction; probability leaving the board never returns.
  2. 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.