skip to content

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

level: seniorimportance: should knowfreq 26%

answer

  1. each step consumes one of each
  2. count what the mask already tells you
  3. filled shifts versus processed workers
  4. a dimension that can never disagree
  5. the index is derivable, so drop it

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.

solid answer

~50 s

The invariant is the point: `dp[mask]` means "the first k workers have been assigned, and the filled shifts are exactly `mask`", where k is the number of set bits in `mask`. Because every step assigns exactly one worker to exactly one shift, the count of filled shifts *is* the count of processed workers — so an explicit worker index would always equal that count and can never disagree with it. Keeping it would give 2^n·n entries where 2^n suffice, and would create a large region of states that are unreachable by construction. The transition is `dp[mask] = min over shifts s in mask of dp[mask without s] + cost[k-1][s]`, so the whole thing is 2^n states with O(n) work each. The shortcut is conditional, though: the moment a worker may be skipped, or take two shifts, the count stops determining the index and the dimension has to come back.

code

pseudocode · 9 lines
pseudocode
dp[0] = 0
for mask = 1 to 2^n - 1:
    k = number of set bits in mask       // workers already placed
    dp[mask] = INFINITY
    for s in 0..n-1:
        if bit s of mask is set:
            prev = mask with bit s cleared
            dp[mask] = min(dp[mask], dp[prev] + cost[k-1][s])
answer = dp[2^n - 1]

go deeper

for a junior

Know that a subset can be an index into a table and that assigning people to slots one at a time is the classic use. Focus on reading the recurrence rather than deriving it.

for a middle

Explain the invariant in one sentence — filled slots equal processed items — and derive from it that the table is 2^n rather than 2^n·n. Be able to write the transition and justify the mask ordering.

for a senior

Demonstrate that you spot a redundant dimension and can name its real costs: wasted memory near the feasibility wall and a swathe of unreachable states that mask bugs. State the exact condition under which the shortcut is valid.

for a principal

Be ready to argue about clarity versus footprint on a team: the compact formulation is cheaper but encodes an invariant that a future maintainer can silently break, so decide when it must be asserted in code and covered by tests.

## The setup n workers must each be given exactly one of n shifts, one worker per shift, with a cost matrix `cost[w][s]` giving what it costs to put worker w on shift s (overtime premium, travel, skill mismatch). Minimise total cost. There are n! assignments; at n = 15 that is over a trillion. Subset DP brings it to 2^n states. ## The state and its invariant Define `dp[mask]` = the minimum cost of assigning workers 0..k−1 to exactly the shifts whose bits are set in `mask`, where **k = the number of set bits in mask**. That sentence contains the entire insight. Workers are processed in a fixed order — worker 0, then worker 1, and so on — and each transition consumes one worker *and* one shift. The two counts therefore move in lockstep and can never diverge. Whose turn it is is not extra information; it is derivable from the state you already have. ## The recurrence ``` dp[0] = 0 for mask = 1 to 2^n - 1: k = number of set bits in mask // shifts filled = workers placed dp[mask] = INFINITY for s in 0..n-1: if bit s of mask is set: prev = mask with bit s cleared dp[mask] = min(dp[mask], dp[prev] + cost[k-1][s]) answer = dp[2^n - 1] ``` Read the last worker placed as `k−1` because `mask` already includes their shift. Increasing numeric order over masks is a valid evaluation order: clearing a bit always yields a smaller number, so `dp[prev]` is final when read. Complexity is 2^n states × O(n) transitions = O(2^n·n) time and O(2^n) memory — an n-fold saving over the naive `dp[mask][worker]` formulation in both. ## Why the redundant dimension is not harmless Engineers often shrug at an extra index: "it is only a factor of n, and it makes the code clearer." Three reasons to care. 1. **Memory is the binding constraint in subset DP.** A factor of n at n = 22 is more than four million extra entries per mask-layer's worth of table — the difference between fitting and not, and it costs you roughly four elements of headroom against a wall you are already close to. 2. **It manufactures unreachable states.** In `dp[mask][w]`, every pair where `w` is not the set-bit count of `mask` is meaningless. They will be initialised, iterated and cache-missed. Worse, a bug that writes to one of them will produce an answer that looks plausible and is wrong, because nothing in the loop structure declares those states illegal. 3. **It hides the invariant.** The clean formulation forces you to state "filled shifts = processed workers", which is the property a reviewer or an interviewer actually wants to hear. Carrying both lets you avoid noticing that they are the same number. ## When the shortcut breaks — the part that matters The count implies the index only when **each transition advances both counters by exactly one**. Break that coupling and the shortcut is wrong: - **Optional items.** If a worker may be left unassigned (fewer shifts than workers, or a "no suitable shift" option), then a mask with three bits could correspond to having considered three, four or five workers. The index is no longer a function of the mask and must be stored. - **Multiplicity.** If one worker can cover two shifts, two set bits can belong to one worker. Same failure. - **Order-dependent cost.** If the cost of placing a worker depends on *which* shift the previous worker took — a handover, a shared vehicle, a travel leg between locations — then even with the counters coupled you need the previous choice in the state, which is the routing shape with its extra endpoint dimension. So the two archetypes are mirror images. In a routing DP the extra dimension is mandatory because steps are coupled through the last choice. In a plain assignment DP it is redundant because steps are coupled only through *how many* have been made. Being able to say which situation you are in, and why, is what separates a memorised template from understanding the state design. ## Practical notes Initialise unreached entries to infinity, not zero, or partial garbage propagates into the answer as a suspiciously cheap schedule. To recover the actual roster, either store the winning shift per mask or walk backwards from the full mask, at each step finding the `s` whose removal satisfies the equality in the recurrence. And if the matrix is rectangular — more shifts than workers — index the mask over the *smaller* dimension; the exponent is where all your cost lives, so it should sit on whichever side is smaller.

  • What goes wrong if some workers may be left unassigned?
    The coupling breaks. With optional workers, a mask holding three filled shifts might mean three, four or five workers were considered, so the set-bit count no longer identifies whose turn it is. You must restore the explicit index — `dp[mask][w]` — and pay the n-fold memory cost, or reformulate so that skipping is itself a modelled choice that consumes a worker.
  • How would you recover the actual roster rather than just its cost?
    Store the chosen shift alongside each `dp[mask]`, or reconstruct backwards: from the full mask with k = n, find the shift `s` whose removal satisfies `dp[mask] == dp[mask without s] + cost[k-1][s]`, record that pairing for worker k−1, clear the bit and continue. Reconstruction is O(n^2) and does not affect the complexity.
  • The matrix is rectangular — 20 shifts and 8 workers. Which side gets the mask?
    The smaller one. Cost is exponential in whatever the mask enumerates, so masking the 8 workers gives 256 states while masking the 20 shifts gives a million. Iterate shifts in index order and let the mask track which workers are already placed; the same set-bit invariant applies with the roles swapped.

saying these in an interview costs you the question

  • Insists every subset DP needs a second position index
  • Says the redundant dimension is free because it is only a factor
  • Applies the count shortcut where elements may be skipped
  • Confuses this with routing DP, where the last choice matters
  • Iterates masks in an order that reads unfinished entries
  • Initialises unreached states to zero instead of infinity

context