Student Attendance Record II is a classic counting-DP problem. Instead of checking one record, we must count how many valid records of length n exist. The trick is to realize the validity of a record depends only on a tiny amount of state, so we can group billions of records into just six buckets.
Problem. A record is a string of length n over 'P' (present), 'A' (absent), 'L' (late). It is
rewardable if it has fewer than 2 total absences and never 3 late days in a row. Return the
number of rewardable records of length n, modulo 10^9 + 7.
Example: n = 2 → answer 8. The only invalid length-2 records are AA (two absences) — every other of
the 9 strings is fine, giving 8.
The slow way first
The brute force is to generate all 3^n records and test each one. That is exponential and dies almost immediately — for n = 40 there are over 10^19 strings. We need something that scales linearly with n.
The question to ask: what do I actually need to know about a record so far to decide what I can append next? I do not need the whole string. I only need two facts: how many absences I have used (0 or 1) and how many late days are at the very end (0, 1, or 2). Everything else is irrelevant to the rules.
The idea: count by state
Track six counts, one per (absent, trailingLate) state where absent ∈ {0, 1} and trailingLate ∈ {0, 1, 2}. Start with the empty record: state (0, 0) holds the single empty string. Then for each of the n positions, push every count forward by deciding what character to append:
- Append P: absence unchanged, trailing late resets to 0.
- Append L: absence unchanged, trailing late goes up by one — but only from
late < 2(a 3-run is illegal). - Append A: only from an
absent = 0state, moves it toabsent = 1, trailing late resets to 0.
After processing all n characters, the answer is the sum of all six buckets — every record that survived without breaking a rule.
Walk through it
Step through the animation. The cells across the top place one concrete record character by character, while the six labels below show the count in every (absent, late) bucket. Watch how P dumps everything into the late-0 column, L shifts the late columns rightward, and A lifts a count from the top row into the bottom (absent) row.
Pseudocode
dp[absent][late] = number of valid records in that state
start: dp[0][0] = 1, everything else 0
repeat n times, building a fresh "new" grid each time:
for every state (a, l):
append P: new[a][0] += dp[a][l] # late resets
for every state (a, l) with l < 2:
append L: new[a][l + 1] += dp[a][l] # late grows
for every state (0, l):
append A: new[1][0] += dp[0][l] # use the one allowed absence
take everything mod 1e9+7, then dp = new
answer = sum of all six dp buckets, mod 1e9+7The Python solution
def checkRecord(n):
MOD = 10**9 + 7
dp = [[0, 0, 0], [0, 0, 0]]
dp[0][0] = 1
for _ in range(n):
new = [[0, 0, 0], [0, 0, 0]]
for a in range(2):
for l in range(3):
new[a][0] += dp[a][l]
for a in range(2):
for l in range(2):
new[a][l + 1] += dp[a][l]
new[1][0] += dp[0][0] + dp[0][1] + dp[0][2]
for a in range(2):
for l in range(3):
new[a][l] %= MOD
dp = new
return sum(dp[a][l] for a in range(2) for l in range(3)) % MODdp[a][l]is the number of valid records withaabsences andltrailing late days. We seeddp[0][0] = 1for the empty record.- The P block routes every state into the late-0 column (
new[a][0]), because a present day resets the late run. - The L block shifts each state up one late level (
new[a][l + 1]), and only runs forl < 2, so a 3rd consecutive L is never created. - Line 13 is the A transition: only
absent = 0states may take an absence, and they all land innew[1][0](absence used, late reset). - We apply the modulus every round so the numbers never overflow, and line 19 sums all six buckets for the final count.
Complexity
| Case | Time | Notes |
|---|---|---|
| Brute force (all strings) | O(3^n) (moderate) | generate and test every record |
| State DP (this solution) | O(n) (moderate) | 6 buckets, constant work per character |
O(1) (fast)We do a constant amount of work per character (just shuffling six numbers), so the whole thing is O(n) time and O(1) space. Collapsing an exponential space of strings into a handful of states is the heart of counting DP.
When this pattern shows up
When a problem says "count the number of valid sequences/strings of length n," resist generating them. Ask what minimal state determines whether you can extend the sequence — usually a small tuple — and make a DP bucket per state. The answer is the sum of all reachable end states.
Two easy bugs: forgetting to gate the L transition on l < 2 (which silently allows LLL), and applying
the modulus only at the end (the intermediate sums overflow in languages without big integers). Apply the
mod every round.
Practice
The empty record sits in state (absent 0, late 0). After appending a single L, which bucket holds that record, and what are absent and late?
1. Why are six states enough to count every valid record?
2. What does appending P do to a state?
3. Why does the L transition only run while trailing late is less than 2?
4. What is the time complexity of the state-DP solution?