Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Planning DP  ›  Lesson

DP on Grids & the Zone Crossing

DP on Grids 32 minWhen moves only go right and down, the grid is its own table
You're building a piece ofMeridian — the day planner
This piece — cheapest_crossing(): Finds the cheapest way across a grid of service zones, around the closed ones.
Scenario Meridian divides the city into a grid of service zones, each with a congestion toll, and roadworks close some zones for the day. The zone crossing finds the cheapest way for a van to get from the north-west depot to the south-east one, and says which way to turn at every junction.
Your task
Build cheapest_crossing(grid). grid[r][c] is the toll for zone (r, c), or -1 for a closed zone. A van starts in the top-left zone and ends in the bottom-right one, moving only right or down, and pays the toll of every zone it enters, including the first and the last. Return {"cost": lowest_total_toll, "path": moves}, where moves is a string of "R" and "D". Return None if every route is blocked. For ties, fill cost[r][c] for every zone, then walk back from the bottom-right corner: step to the cell above when its cost is less than or equal to the cell on the left, otherwise step left. Example: cheapest_crossing([[1, 3, 1], [1, 5, 1], [4, 2, 1]]) → {"cost": 7, "path": "RRDD"}.

DP on Grids & the Zone Crossing

In short: on a grid where the van only moves right or down, every zone's answer depends only on the zone above it and the zone to its left, and both are already finished by the time you get there. So the grid doubles as the table: no separate structure, no recursion, just a fill order where "above" and "left" come first. Swap + for min and the same shape counts routes or finds the cheapest one; closed zones become 0 routes or infinite cost so the combine step quietly ignores them.

![A 3x3 cost grid for [[1,3,1],[1,5,1],[4,2,1]], each cell showing its running cost with arrows from above and left into the bottom-right corner 7, and the winning RRDD path traced in green](/api/assets/dsa/assets/fdab48196afb6ce5.png)

Meridian divides the city into a grid of service zones. Each zone has a congestion toll, and on any given morning a few are closed for roadworks. A van leaving the north-west depot for the south-east one works its way across the grid, and dispatch wants two numbers: how many routes are there, and which one is cheapest?

A grid needs no separate table. If the van only ever moves right or down, every zone can be reached only from the zone above it or the zone to its left, and those two zones already have their answers when you get there. The map is the table, and the only question left is the order you fill it in.

Counting routes

Every route into a zone arrives from above or from the left, and no route does both. So the counts simply add:

ways[r][c] = ways[r - 1][c] + ways[r][c - 1]

A closed zone has zero routes into it, and every count downstream quietly loses the routes that would have used it. Watch a 4 × 5 district fill with two zones closed (X):

watch it move · grid

Every cell on one anti-diagonal (r + c constant) depends only on the diagonal before it, so the answers sweep across the grid as a wavefront from the start corner.

The start has one route, the empty one. The two closed zones will stay at zero routes. 4 by 5 table. At row 0, column 0 — value 1. 3 of 20 cells filled.

1 / 8

The wavefront shows why the fill order is safe: a cell only ever reads cells on the diagonal before it. Row by row is one order that respects this, and column by column is another:

What does this print?
R, C = 3, 3
ways = [[1] * C for _ in range(R)]
for c in range(1, C):          # columns outside,
    for r in range(1, R):      # rows inside
        ways[r][c] = ways[r - 1][c] + ways[r][c - 1]
print(ways[R - 1][C - 1])

The counts grow fast. Guess before you run it:

What does this print?
ways = [[1] * 10 for _ in range(10)]
for r in range(1, 10):
    for c in range(1, 10):
        ways[r][c] = ways[r - 1][c] + ways[r][c - 1]
print(ways[9][9])

You only ever read the row above, so one row is enough. Step through the rolling version, where each row[c] already holds "above" and gains "left":

step through it
1row = [1, 1, 1]
2for r in range(1, 3):
3 for c in range(1, 3):
4 row[c] += row[c - 1]
5print(row)

The cheapest crossing

Swap + for min and add the zone's own toll, and the counting table becomes a cost table:

cost[r][c] = grid[r][c] + min(cost[r - 1][c], cost[r][c - 1])

A closed zone gets infinity, and min then ignores it. Using 0 for a closed zone is a trap, and so is reusing the -1 from the map as if it were a toll:

broken — fix it

The centre zone is closed, so the only legal routes go round the edge, and the cheaper one costs 15. This prints 9 because it drives straight through the roadworks and gets paid for it. Make closed zones unreachable.

Now fill the cost table yourself. Every frame shows the two cells a zone compares. Change the tolls, or close a different zone with -1, and run it again:

your code, drawn · grid

Your cost table. Each zone reads only its upper and left neighbours, both already final, and closed zones stay at infinity so min passes them over.

Reading the route back

The table holds costs, not moves, so you recover the route by walking back from the corner. At each zone, ask which neighbour the minimum came from, and step there. When the two neighbours tie, you need a rule, or two correct programs will print different routes. The zone crossing uses "came from above" on a tie, which makes the returned route hug the top edge for as long as it can:

python

When one number per cell isn't enough

Some questions can't be answered from "the cheapest way to reach this zone". Here's one, written the way an interviewer would hand it to you:

Interview-style problem: the articulated truck. A truck crosses a toll grid from the top-left zone to the bottom-right one, moving right or down. It pays every zone's toll, plus turn_fee every time it switches between moving right and moving down. Return the lowest total.

Constraints: 1 to 30 rows and columns, tolls 0 to 100, turn_fee 0 to 1,000.

Examples: [[1, 1, 1], [5, 5, 1], [5, 5, 1]] with fee 10 → 15 (right, right, down, down: one turn). The same grid with fee 0 → 5, the plain cheapest route.

The cheapest arrival at a zone might have come in moving down, while a slightly dearer one came in moving right and can carry on without paying for a turn. Keep one number per cell and you have to throw one of them away, and it may be the one you needed. The fix is the most important idea in this section: when the future depends on something, put it in the state. Here that's the last direction, so each zone carries two numbers. In the capstone exercise, a driver with emergency permits carries one number per permits-used count. The grid stays the same, each cell holds a little more, and the fill order doesn't change because every dependency still points up or left.

❓ Cross-question: "What if the van can also move up or left?" Then two neighbouring zones depend on each other, and no fill order finishes one before the other. It stops being a table problem and becomes a shortest-path problem on a graph of zones, and Dijkstra settles zones in cost order, which is exactly the order the table no longer gives you.

How fast do routes pile up?

The number of right/down routes across an n by n block of moves is C(2n, n), while the table has only (n + 1)² cells:

n routes table cells
5 252 36
10 184,756 121
12 2,704,156 169
15 155,117,520 256

Listing routes is exponential and filling the table is quadratic, which is why nobody enumerates routes to find the cheapest one.

Every cell does one + or min of its two finished neighbours, and there are R · C cells, so the plain grid is O(R · C) time. The table itself is O(R · C) space; since a cell only ever reads the row above and the row it's building, one rolling row is enough and that drops space to O(C) — the same trade as knapsack's rolling row, and for the same reason: you can fill the number this way, but you lose the ability to walk the route back.

Grid question Cell holds Combine Time
how many routes a count + O(R · C)
cheapest route a cost min + toll O(R · C)
largest free square a side length 1 + min of three O(R · C)
with a turn fee a cost per last direction min over 2 O(R · C · 2)
with k permits a cost per permits used min per layer O(R · C · k)

Idioms & real-world patterns

Guard the edges, don't trust negative indices

cost[r - 1][c] at r = 0 is cost[-1][c], the bottom row, and Python reads it without complaint. On a counting grid it's usually still 0 and the bug hides. On a cost grid it's usually infinity and the bug hides too, until someone prefills the table differently. Write if r > 0 and if c > 0 explicitly, or pad the table with a sentinel row and column.

⚡ Advanced: the anti-diagonal is parallel

Every cell on one anti-diagonal is independent of the others, so a GPU or a vectorised library can compute a whole diagonal at once. Sequence alignment in bioinformatics uses the same wavefront to fill enormous grids in parallel.


Quick practice

What does this print?
R, C = 3, 3
closed = {(1, 1)}
ways = [[0] * C for _ in range(R)]
for r in range(R):
    for c in range(C):
        if (r, c) in closed:
            continue
        if r == 0 and c == 0:
            ways[r][c] = 1
        else:
            up = ways[r - 1][c] if r > 0 else 0
            left = ways[r][c - 1] if c > 0 else 0
            ways[r][c] = up + left
print(ways[2][2])
broken — fix it

The cheapest crossing of this grid costs 7, but this prints 3 because row 0 reads `cost[-1][c]`, the bottom row, instead of having no "above" at all. Guard the read with `r > 0`.

🎯 Your turn

Write cheapest_crossing(grid). grid[r][c] is a zone's toll, or -1 for closed. The van moves right or down from the top-left zone to the bottom-right one and pays every toll it enters, including the first and last. Return {"cost": total, "path": "RRDD…"}, or None if every route is blocked.

  • cheapest_crossing([[1, 3, 1], [1, 5, 1], [4, 2, 1]]) → {"cost": 7, "path": "RRDD"}

Ties: walk back from the corner and step up whenever the cell above costs less than or equal to the cell on the left.

Then press ▶ Run, tap the Live App try chips, and hit ✓ Check. Green means the zone crossing is built. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upHow many routes

A van crosses a block of rows × cols zones from the top-left zone to the bottom-right one, moving only right or down. Write count_routes(rows, cols) returning how many different routes there are. count_routes(2, 3) → 3; count_routes(1, 5) → 1.

grid-dptabulation
DrillRoutes around roadworks

Now some zones are closed for roadworks: grid[r][c] is 0 for open and 1 for closed. Write routes_around(grid) returning the number of right/down routes from the top-left zone to the bottom-right zone that only pass through open zones (0 if the start or end is closed). routes_around([[0, 0, 0], [0, 1, 0], [0, 0, 0]]) → 2.

grid-dptabulation
BuildLargest clear site

Meridian wants to build a square depot on a map of city blocks, where grid[r][c] is 0 for a free block and 1 for a built one. Write largest_clear_square(grid) returning the side length of the largest square made only of free blocks (0 if there are none). largest_clear_square([[0, 0, 1], [0, 0, 0], [1, 0, 0]]) → 2.

grid-dpdp
BossEvery turn costs

Articulated trucks lose time on every turn. The truck crosses a toll grid from the top-left to the bottom-right moving right or down, pays each zone's toll (including the first and last), and pays turn_fee extra every time it switches between moving right and moving down. Write cheapest_with_turns(grid, turn_fee) returning the lowest total. All tolls are non-negative. cheapest_with_turns([[1, 1, 1], [5, 5, 1], [5, 5, 1]], 10) → 15 (straight right, then straight down: one turn).

Constraints: 1 to 30 rows and columns, tolls 0 to 100, turn_fee 0 to 1,000.

grid-dpdp
CapstoneCrossing with permits

Closed zones (-1) can be crossed with an emergency permit, and a driver carries permits of them. A permitted zone charges no toll. Moving only right or down from the top-left to the bottom-right, write cheapest_with_permits(grid, permits) returning the lowest total toll using at most permits permits, or -1 if even that isn't enough. The start and end zones may be closed too. cheapest_with_permits([[1, -1, 1], [9, 9, 1]], 1) → 3.

Constraints: up to 20 × 20 zones, tolls 0 to 100, permits 0 to 5.

grid-dpdp
ExplainWhy only right and down?

Every grid in this lesson allows only right and down moves. A colleague wants the zone crossing to allow all four directions and plans to keep the same row-by-row table. Explain why that breaks the DP, and what kind of algorithm the four-direction version needs instead.

cheapest_crossing.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
cheapest_crossing(grid) → dictThe cheapest right/down crossing: its total toll and the moves as a string of R and D, or None.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.