skip to content

questions

12

Why does subset DP over a courier's route need dp[mask][last] rather than dp[mask] alone?

level: middleimportance: must knowfreq 45%

answer

  1. what does the next leg's cost depend on
  2. same set of sites, different place to stand
  3. a state must screen off the past
  4. two partial routes, same mask, not comparable
  5. index the table by endpoint as well

basics

~20 s

The visited set alone does not determine the remaining cost: the next leg's travel time depends on which site the courier is standing at. dp[mask][last] carries both the set of visited sites and the current position.

solid answer

~40 s

A DP state must make the future independent of the path taken to reach it. For a courier who must visit every client site once and return to the depot, two partial routes can cover the *same* set of sites and yet leave the courier in different places — and the cost of everything that follows depends on where they are standing. So the set is not a sufficient state. The fix is a second index: `dp[mask][last]` = cheapest way to have visited exactly the sites in `mask`, finishing at site `last`. The transition extends by one unvisited site: `dp[mask | bit(j)][j] = min(dp[mask][i] + travel[i][j])`. That gives 2^n·n states and O(n) work per state, so O(2^n·n^2) time and O(2^n·n) memory. The answer is the minimum over `last` of `dp[full][last] + travel[last][depot]`.

go deeper

for a junior

Recall the shape: an integer's bits stand for 'visited or not', and the table is indexed by that integer plus the current position. Be able to say why enumerating every ordering is hopeless past a dozen or so items.

for a middle

Explain out loud why the visited set alone fails as a state, write the transition, and derive 2^n·n states with O(n) work each. Interviewers at this level want the recurrence and the cost, not code.

for a senior

Show judgment about when the endpoint dimension is actually required versus redundant, and be ready to discuss memory layout, reconstruction of the route, and what asymmetric costs do and do not change.

for a principal

Own the framing: this is an exact method with an exponential memory wall, so the interesting call is where it belongs in a system at all — on small clusters of stops, as an oracle for testing an approximate planner, or nowhere.

## The problem shape A courier starts at a depot, must visit each of n client sites exactly once, and returns to the depot. Travel times between every pair of locations are known and may be asymmetric (one-way streets, traffic direction). You want the cheapest closed route. Brute force enumerates n! orderings — at n = 18 that is about 6.4 × 10^15, hopeless. Subset DP replaces "which ordering" with "which set, and where am I now", and that collapse is the whole trick. ## What a bitmask is doing here A *bitmask* is an integer used as a set: bit i is 1 when site i has been visited. With n sites there are exactly 2^n such sets, and each is a single integer you can index a table with. That is the only reason subsets are usable as a DP dimension at all — the set is a number, so the table is a plain lookup rather than a map of sets. ## Why the set alone is not a state The defining property of a DP state is that it must **screen off the past**: everything the remaining decisions cost has to be computable from the state, never from how you got there. Suppose the mask says sites {A, B, C} are done. One partial route ended at A, another ended at C. Those two histories have different futures — the very next leg is priced from a different starting point, and so is every leg after it. Storing one number per mask would force you to compare, and discard, routes that are not comparable: the cheaper prefix may end somewhere expensive to leave. Keeping only the cheaper one silently throws away the optimum, and the DP returns a wrong (too small or unachievable) answer rather than a slow one. Adding the endpoint restores the property. Once you know the visited set *and* the current site, the rest of the trip depends on nothing else — the identity of the remaining sites is `complement(mask)`, and the first leg out is priced from `last`. That is optimal substructure recovered. ## The recurrence Fix the depot as the start so it never occupies a bit (fixing the start also removes the n-fold rotational symmetry of a closed tour for free). - Base: `dp[bit(i)][i] = travel[depot][i]` for every site i. - Transition: for each mask, each `i` set in mask with a finite `dp[mask][i]`, and each `j` not in mask: `dp[mask | bit(j)][j] = min(dp[mask | bit(j)][j], dp[mask][i] + travel[i][j])`. - Answer: `min over i of dp[full][i] + travel[i][depot]`, where `full` is the all-ones mask. Evaluation order is free: iterating masks in increasing numeric order works, because `mask` is numerically smaller than `mask | bit(j)` whenever bit j was clear. Every value is therefore final before it is read. ## What it costs | quantity | formula | n = 18 | |---|---|---| | states | 2^n · n | ≈ 4.7 million | | transitions | 2^n · n^2 | ≈ 85 million | | memory (4-byte entries) | 4 · 2^n · n | ≈ 19 MB | The extra dimension multiplies memory by n — real, but a *factor*, not another exponential. People sometimes fear it turns 2^n into something worse; it does not. ## Recovering the route The table holds costs, not orderings. Either store a parent index alongside each entry, or walk backwards at the end: from `(full, last)`, find the predecessor `i` in `mask` with `dp[mask without last][i] + travel[i][last] == dp[mask][last]`, then repeat. Reconstruction is O(n^2) at worst and costs nothing asymptotically. ## When the endpoint dimension is *not* needed The endpoint matters here only because the cost of a step depends on the previous choice. In problems where each step's cost depends only on *which* element is added and *how many* have been added — assigning workers to shifts, for instance — the extra dimension is redundant and should be dropped. Recognising which of the two shapes you are in is the actual skill; adding `last` reflexively wastes an n-fold factor, and omitting it when steps are coupled produces a confidently wrong answer. ## Common traps Forgetting the closing leg back to the depot; initialising unreached states to something other than infinity so garbage propagates; letting the mask include the depot and then wondering why the count is 2^(n+1); and assuming symmetry — with asymmetric travel times you cannot reverse a partial route and reuse its cost.

  • Once the table is filled, how do you recover the actual visiting order?
    Either store a parent index with each entry, or reconstruct backwards: starting from the winning `(full, last)`, look for a predecessor `i` in the mask where `dp[mask without last][i] + travel[i][last]` equals `dp[mask][last]`, move to that state, and repeat until the mask is a single bit. Reconstruction is O(n^2) and does not change the complexity.
  • Why not give the depot its own bit in the mask?
    It is fixed as both start and end, so it carries no decision. Giving it a bit doubles the table and creates masks that are unreachable or meaningless. Fixing the start also collapses the n rotations of the same closed tour into one representative, which is a genuine constant-factor win rather than just tidiness.
  • Roughly what time and memory does 18 sites need?
    About 2^18 · 18 ≈ 4.7 million states, each with up to 18 outgoing transitions, so on the order of 85 million operations — comfortable. Memory is 4.7 million cost entries, roughly 19 MB at four bytes each, plus the same again if you store parent pointers. Both scale by two per extra site.

Knowing which errands you have already run does not tell you what the rest of the day costs — you also need to know which street corner you are standing on right now.

saying these in an interview costs you the question

  • Says dp[mask] alone is enough because the set is the state
  • Claims the state space is n! since orderings are being searched
  • Thinks the second index makes the DP exponentially worse
  • Forgets the closing leg back to the starting point
  • Assumes travel costs are symmetric and reverses partial routes
  • Leaves unreached states at zero instead of infinity

context

open as a page

Why is interval DP O(n^3) when its dp table holds only O(n^2) entries?

level: middleimportance: must knowfreq 55%

basics

~20 s

Each of the O(n^2) subranges is not filled in constant time: computing dp[i][j] scans every split point between i and j, which is up to O(n) work per entry. States times transitions-per-state gives O(n^2) * O(n) = O(n^3).

open as a page

In a tree DP that forbids picking a node and its parent together, why keep two values per node?

level: middleimportance: must knowfreq 55%

basics

~20 s

Keep two numbers per node: the best subtree total with that node picked, and with it skipped. A parent that picks itself needs its children's skipped totals, so collapsing to one best-per-node throws away exactly the value the parent needs.

open as a page

In a tree DP for the longest path, why does each node combine two child branches but return only one?

level: seniorimportance: must knowfreq 50%

basics

~20 s

A longest path bends at exactly one node, arriving up one branch and leaving down another, so every node tests its two deepest branches against a running global best. It returns only its single deepest branch, because a parent can extend just one.

open as a page

Why does bitmask DP over subsets stay feasible near n = 20 but collapse by n = 30?

level: middleimportance: should knowfreq 42%

basics

~10 s

Each extra element doubles the table. Over 20 elements a dp[mask][last] table holds about 21 million entries; over 30 it holds roughly 32 billion — far past memory before a single transition is counted.

open as a page

In interval DP, why can't the dp[i][j] table be filled in plain row-major order?

level: middleimportance: should knowfreq 45%

basics

~20 s

An interval DP entry dp[i][j] is built from dp[i][k] and dp[k][j] for split points i < k < j, which are always shorter subranges. Row-major order reaches dp[k][j] before that row is written, so fill by increasing interval length instead.

open as a page

Why does a post-order tree DP that combines every child at each node still run in O(n)?

level: middleimportance: should knowfreq 45%

basics

~20 s

Count the work per edge, not per node. Every parent-child link is used exactly once, and a tree on n nodes has n-1 links, so the total combining work is O(n) as long as each node spends constant time per child.

open as a page

In n-worker n-shift assignment DP, why does dp[mask] alone suffice with no worker index?

level: seniorimportance: should knowfreq 26%

basics

~20 s

Processing workers in a fixed index order makes the worker index a function of the mask: the number of set bits is how many workers are already placed. Carrying that index separately multiplies the table by n and adds nothing.

open as a page

In a pipe-cutting model where each cut costs the length of the piece it splits, why does taking the cheapest cut first fail?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A cut's price is set by the piece it lands in, so every choice reprices the remaining cuts and no exchange argument holds. Order is a global optimisation over subranges, solved by interval DP branching on the first cut.

open as a page

Your exact subset-DP route planner tops out near 20 stops, but dispatch now needs 35 — how do you decide what replaces it?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Subset DP's ceiling is exponential memory, so no tuning reaches 35 stops — the method must change. The real decision is what the last few percent of route quality is worth against a planner the team can operate and maintain.

open as a page

Your planner parenthesizes a fixed chain of joins with interval DP — when do you abandon the exact search?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Abandon it when planning cost stops buying execution savings: when quadratic memory or cubic time breaks the per-request budget, or when the size estimates feeding the cost function are so uncertain that the exact optimum is optimal only for a fiction.

open as a page

When is a rerooting tree DP worth its complexity over simply running one traversal per candidate root?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Rerooting answers every node in two linear passes instead of one traversal per node, turning quadratic work into linear. Take it when the measured input size and recompute frequency actually miss the latency budget; below a few thousand nodes the simpler per-root loop usually wins and is far easier to keep correct.

open as a page