</> MAANG.io
coding interview · 301

Advanced

Advanced coding interview preparation covering complex algorithms, system design, and optimization techniques for senior-level positions.

0/95 solved 0% complete

Grid and Position DP

What is this?

Each of these indexes by a position and needs exactly one extra coordinate to close the recurrence. A grid cell needs nothing more — the strictly-increasing rule makes the graph acyclic on its own. Typing needs to know where the other finger is. Shelving needs to know where the current row started. Scheduling needs to know the last job that finished in time, which is a binary search rather than a loop.

flowchart TD A["state = position + ?"] --> B["Longest Increasing Path
nothing — strict increase = acyclic"] A --> C["Two Fingers
+ the other finger's position"] A --> D["Bookcase Shelves
+ where this shelf begins"] A --> E["Job Scheduling
+ last compatible job (binary search)"] B --> B1["memoise on the cell"] C --> C1["only ONE finger is at word[i−1]"] D --> D1["walk backwards until width is exceeded"] E --> E1["sort by end, bisect on start"]

💡 Fun fact: Minimum Distance to Type a Word Using Two Fingers looks like it needs a state of (index, finger1, finger2) — 27 × 27 positions at every index. It does not. After typing word[i−1], one of the fingers is necessarily on word[i−1], which is already known from the index. So only the other finger's position needs storing, and the state collapses to (index, other). Recognising that part of a state is implied by the rest is one of the most reliable ways to turn a DP that will not fit into one that does.

🔓 The 4 problems in this chapter are free. Sign in with Google or Microsoft to start solving.


The one-line idea: write down the smallest thing you must remember besides the position. Anything the index already implies is not state — and removing it is usually the difference between passing and timing out.


1. When the rule makes it acyclic

@lru_cache(None)
def go(r, c):
    best = 1
    for nr, nc in neighbours(r, c):
        if mat[nr][nc] > mat[r][c]:            # strictly greater → no cycles possible
            best = max(best, 1 + go(nr, nc))
    return best
return max(go(r, c) for r in range(m) for c in range(n))

No visited set is needed, and that is the point worth saying aloud: because every step strictly increases, a path can never return to a cell it has left. The memo makes each cell computed once, giving O(mn).

The alternative is a topological sort by value, which is the same computation arranged differently and useful to mention.

2. When part of the state is implied

@lru_cache(None)
def go(i, other):                              # other = the finger NOT on word[i-1]
    if i == len(word): return 0
    prev = word[i-1] if i else None
    return min(dist(prev,  word[i]) + go(i+1, other),   # same finger continues
               dist(other, word[i]) + go(i+1, prev))    # the other finger moves

Two choices per character, and None — no finger placed yet — costs zero to move from, which handles the first two characters without a special case. Distance is Manhattan on the 6-wide keyboard grid: divmod(ord(ch) - ord('A'), 6).

3. When the choice is where the group begins

for i in range(1, n + 1):
    w = h = 0
    for j in range(i, 0, -1):                  # books j..i share the last shelf
        w += books[j-1][0]
        if w > shelf_width: break              # cannot widen further
        h = max(h, books[j-1][1])
        dp[i] = min(dp[i], dp[j-1] + h)

Walking backwards from i is what lets the width accumulate incrementally and the break fire as soon as the shelf is full. Books must stay in order, which is why "which books share the last shelf" is always a contiguous suffix — and that is the fact that makes the whole DP work.

4. When the transition is a binary search

jobs = sorted(zip(end, start, profit))
ends, dp = [0], [0]                            # both strictly increasing
for e, s, p in jobs:
    i = bisect_right(ends, s) - 1              # last job finishing at or before s
    if dp[i] + p > dp[-1]:
        ends.append(e); dp.append(dp[i] + p)
return dp[-1]

Sorting by end time is what makes ends increasing and therefore searchable. dp is kept monotonic by appending only when the new total beats the current best, which means dp[-1] is always the answer so far and no final scan is needed.


5. A 30-second worked example (two fingers)

word = "CAKE" on a 6-wide keyboard: A is (0,0), C is (0,2), E is (0,4), K is (1,4).
Distances: C→A = 2, C→K = 3, A→K = 5, K→E = 1.

the greedy — always make the cheapest move now:
  C   place a finger there                                  0
  A   same finger C→A = 2   vs   place the other finger = 0  → 0
  K   fingers on C and A:  C→K = 3   vs   A→K = 5            → 3
  E   fingers on K and A:  K→E = 1   vs   A→E = 4            → 1
                                                    total    4

the optimum — split the word between the fingers:
  finger 1 types C then A        C→A = 2
  finger 2 types K then E        K→E = 1
                                                    total    3

The greedy's free move at step 2 is what costs it. Placing the second finger on A looks like a saving of 2 and leaves both fingers stranded on the left of the keyboard, while K and E are both on the right. Paying the 2 to keep a finger free is worth it — and no rule about the current character can see that, which is exactly why the problem is a DP.


6. Where you'll actually meet this

  • Text layout engines. Filling shelves is the same recurrence as breaking a paragraph into lines under a width limit.
  • Revenue management. Weighted job scheduling underlies ad slots, meeting rooms and rental bookings.
  • Terrain and hydrology. Longest increasing path is water flow over a height map.
  • Input and accessibility research. Two-thumb typing cost is a genuine keyboard-layout metric.
  • Resource planning. "Group consecutive items under a capacity limit" appears in shipping, batching and pagination.

7. Problems in this chapter

▶ Longest Increasing Path in a Matrix

Longest strictly increasing path. Memoised DFS — the increase rule makes cycles impossible, so no visited set is needed.
Pattern: memoised DFS on an implicit DAG. Target: O(mn) time and space.

▶ Minimum Distance to Type a Word Using Two Fingers

Least total finger travel. State is (index, other finger) because one finger is always on the previous character.
Pattern: DP with an implied state component. Target: O(n · 27) time.

▶ Filling Bookcase Shelves

Least total height with books kept in order. For each prefix, try every contiguous suffix as the last shelf.
Pattern: prefix DP over contiguous groups. Target: O(n²) time, O(n) space.

▶ Maximum Profit in Job Scheduling

Maximum profit from non-overlapping jobs. Sort by end time and binary-search the last compatible job.
Pattern: DP + binary search. Target: O(n log n) time, O(n) space.


8. Common pitfalls 🚫

  • Adding a visited set to the increasing path. Strict increase already forbids revisiting, and the set breaks the memo.
  • Storing both finger positions. One of them is implied by the index.
  • Walking shelves forward. Backwards accumulation is what lets the width check break early.
  • Forgetting books must stay in order, which is what makes the last shelf a contiguous suffix.
  • Sorting jobs by start time. End time is what makes the binary search possible.
  • Using bisect_left on the ends array, which admits a job that ends exactly when the next begins — or excludes one that should qualify. Check the boundary against the problem's definition.
  • Memoising on mutable state. Keys must be the position and the small extra coordinate, nothing else.

9. Key takeaways

  1. State = position + the smallest thing you must remember. Anything implied is not state.
  2. A strict-increase rule gives you a DAG for free — no visited set, no cycle handling.
  3. Contiguity is a gift. "Books stay in order" is what makes the shelf a suffix.
  4. Sort to make a transition searchable. By end time, then bisect.
  5. Keeping a DP array monotonic means the answer is always its last element.
  6. None as an initial position removes the special case for the first move.
  7. Why interviewers like it: the naive state is always writable and always too big, so collapsing it is the visible skill.

Order: Longest Increasing Path in a Matrix → Minimum Distance to Type a Word Using Two Fingers → Filling Bookcase Shelves → Maximum Profit in Job Scheduling.

Maximum Profit in Job Scheduling

Core idea: Each job is an interval [start, end] worth some profit, and you may only run jobs that don't overlap in time. Sort all jobs by end time. Then build a DP table where dp[i] is the best profit achievable using only the first i jobs (in end-sorted order). For job i, you face a clean binary choice: skip it and keep dp[i-1], or take it and earn profit_i plus the best you could have made from jobs that finish at or before this job's start. That last quantity is dp[p(i)], where p(i) is the index of the last job whose end ≤ start_i — found by binary search because the jobs are sorted by end. So dp[i] = max(dp[i-1], profit_i + dp[p(i)]). Sorting is O(n log n), and each of the n jobs does one O(log n) binary search, giving O(n log n) overall. This is the textbook weighted interval scheduling algorithm.


The problem, rephrased

You run a single freelance workstation that can handle exactly one job at a time. A pile of job offers lands on your desk; each offer says "start at hour s, finish at hour e, pay p." You can accept any subset of offers as long as no two you accept overlap — once you start a job you must see it through to its end before the next begins. Some offers pay little but free you up quickly; some pay a fortune but block out a huge chunk of your calendar. You want the subset of non-overlapping offers whose total pay is the largest possible.

Formally: given three equal-length arrays startTime, endTime, and profit, where job k occupies the half-open interval [startTime[k], endTime[k]) and pays profit[k], choose a set of jobs no two of which overlap so that the sum of their profits is maximized. Return that maximum profit. A job that starts exactly when another ends is allowed (the intervals are half-open, so they don't conflict).

This is LeetCode 1235 — Maximum Profit in Job Scheduling.

Input Means Output
start=[1,2,3,3], end=[3,4,5,6], profit=[50,10,40,70] take job 0 (1→3) then job 3 (3→6) 120
start=[1,2,3,4,6], end=[3,5,10,6,9], profit=[20,20,100,70,60] take jobs 0,3,4 → 20+70+60 = 150 150
start=[1,1,1], end=[2,3,4], profit=[5,6,4] all overlap → take the single best 6

Note the greedy trap in row 2: the single juiciest job (profit 100, spanning 3→10) would block jobs 3 and 4, whose combined 70+60+20=150 beats it. Maximum profit is not "grab the biggest first" — it needs DP.


Sign in to continue reading

The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.

Sign in to read the full lesson

Sign in to MAANG.io

Use your Google or Microsoft account — no password to remember.

Continue with Google Continue with Microsoft

Please accept the terms above to continue.