state 30 of 32 · 6 min
The errors that do not crash
Dynamic programming fails quietly. The table fills, numbers appear, and the answer is wrong. These are the specific ways, collected.
Wrong loop direction
for c in range(w, C + 1): # upward: items reused
for c in range(C, w - 1, -1): # downward: each item onceSolves a different problem. Symptom: answers too high on the bounded version.
Base cases left at zero
A minimum over a table containing false zeroes selects them. Symptom: answers far too low, and plausible.
For a maximum, zero claims unreachable states are achievable with value zero. Use negative infinity for unreachable, infinity for an unfilled minimum.
The answer read from the wrong cell
"Best up to i" is monotone, so the answer is the last cell. "Best ending exactly at i" is not, so it is the maximum anywhere. Returning the last cell from an "ending exactly here" table passes small tests, because on short inputs the best chain often does end last.
The memo key is not the whole state
@lru_cache(maxsize=None)
def go(i): # but the answer also depends on remaining capacityReturns a value computed for a different situation, silently. Test: can two different situations produce the same key?
Recurrence permits what the rule forbids
best[i] = max(best[i-1], value[i] + best[i-1]) # allows two adjacent
best[i] = max(best[i-1], value[i] + best[i-2]) # correctSymptom: the answer is larger than any legal selection. Check the take branch excludes what it should.
Counting duplicates
Loops in the wrong nesting counts permutations where combinations were asked for. Test on a tiny input where you can enumerate by hand: coins 1 and 2, amount 3, is the answer 2 or 3?
Modifying while iterating, or sharing a row
table = [[0] * m] * n # n references to ONE list
table = [[0] * m for _ in range(n)] # n separate listsThe first is a Python specific trap and every row changes together.
The checklist
| Check | Question |
|---|---|
| State | could two situations share this key? |
| Base cases | is each one a sentence I can defend? |
| Direction | is every cell filled after what it reads? |
| Combine | counting or optimising? |
| Answer cell | does my definition put it last, or anywhere? |
| Reachability | is unreachable distinguishable from zero? |
The habit that catches all of them
Write a brute force version, then compare both on a few hundred random small inputs.
for _ in range(500):
case = random_small_case()
assert brute(case) == fast(case), caseIt takes ten minutes, it finds every error above, and it prints the smallest failing case for you. This is worth more than any amount of staring at the table.