552. Student Attendance Record II - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Dynamic Programming - Both memoization (top-down) and tabulation (bottom-up) approaches for counting problems
- State Machine Design - Modeling problems with multiple constraints as transitions between discrete states
- Modular Arithmetic - Applying modulo operations to prevent integer overflow in counting problems
1. Dynamic Programming (Top-Down) - I
Intuition
A valid attendance record has at most 1 absence (A) and no more than 2 consecutive late days (L). We can think of this as a state machine where each state tracks two things: how many absences used so far (0 or 1) and how many consecutive late days at the current position (0, 1, or 2). At each position, we decide whether to place P (present), A (absent), or L (late), and transition to the appropriate next state.
Algorithm
- Define a recursive function
dfs(i, cntA, cntL)whereiis the remaining length,cntAis the number of absences used, andcntLis the current consecutive late streak. - Base case: when
i == 0, we have built a valid record, so return1. - For each position, we have three choices:
- Place
'P': always valid, resets consecutive late count to0. - Place
'A': only valid ifcntA == 0(no absence used yet), resets late count to0. - Place
'L': only valid ifcntL < 2(fewer than2consecutive lates), increments late count.
- Place
- Sum all valid transitions and cache the result.
- Return
dfs(n, 0, 0)as the answer.
class Solution:
def checkRecord(self, n: int) -> int:
MOD = 1000000007
cache = [[[-1 for _ in range(3)] for _ in range(2)] for _ in range(n + 1)]
def dfs(i, cntA, cntL):
if i == 0:
return 1
if cache[i][cntA][cntL] != -1:
return cache[i][cntA][cntL]
res = dfs(i - 1, cntA, 0) % MOD
if cntA == 0:
res = (res + dfs(i - 1, 1, 0)) % MOD
if cntL < 2:
res = (res + dfs(i - 1, cntA, cntL + 1)) % MOD
cache[i][cntA][cntL] = res
return res
return dfs(n, 0, 0)Time & Space Complexity
- Time complexity:
- Space complexity:
2. Dynamic Programming (Top-Down) - II
Intuition
This approach reorganizes the state representation. Instead of tracking individual decisions, we store a 2D map keyed by (absence count, consecutive late count) for each length. The recursive function returns all six possible state counts for strings of length n, and we combine them when extending to longer strings.
Algorithm
- For the base case
n == 1, manually define the counts for each (A, L) state. - For larger
n, recursively get the state counts forn - 1. - Build the new state counts by considering what character we append:
- Appending
'P'sums all states with the same absence count (resets late to0). - Appending
'L'shifts the late count up by1. - Appending
'A'moves from absence count0to1(resets late to0).
- Appending
- Cache results to avoid recomputation.
- Sum all six final state values modulo .
class Solution:
def checkRecord(self, n: int) -> int:
MOD = 10**9 + 7
cache = {}
def count(n):
if n == 1:
# (A, L)
return {
(0, 0): 1, (0, 1): 1, (0, 2): 0,
(1, 0): 1, (1, 1): 0, (1, 2): 0
}
if n in cache:
return cache[n]
tmp = count(n - 1)
res = defaultdict(int)
# Choose P
res[(0, 0)] = ((tmp[(0, 0)] + tmp[(0, 1)]) % MOD + tmp[(0, 2)]) % MOD
res[(1, 0)] = ((tmp[(1, 0)] + tmp[(1, 1)]) % MOD + tmp[(1, 2)]) % MOD
# Choose L
res[(0, 1)] = tmp[(0, 0)]
res[(0, 2)] = tmp[(0, 1)]
res[(1, 1)] = tmp[(1, 0)]
res[(1, 2)] = tmp[(1, 1)]
# Choose A
res[(1, 0)] += ((tmp[(0, 0)] + tmp[(0, 1)]) % MOD + tmp[(0, 2)]) % MOD
cache[n] = res
return res
return sum(count(n).values()) % MODTime & Space Complexity
- Time complexity:
- Space complexity:
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of recursion, we iterate forward and build the DP table from length 0 to n. Each state dp[i][cntA][cntL] represents the number of valid records of length i that have used cntA absences and end with cntL consecutive lates.
Algorithm
- Initialize
dp[0][0][0] = 1(one way to have an empty record with no absences and no lates). - For each position from
1ton, for each state (cntA, cntL):- Adding
'P': transitions from any late count to late count0with the same absence count. - Adding
'A': ifcntA > 0, transitions from absence countcntA - 1tocntAwith late count0. - Adding
'L': ifcntL > 0, transitions from late countcntL - 1tocntLwith the same absence count.
- Adding
- Sum all states at position
nfor the final answer.
class Solution:
def checkRecord(self, n: int) -> int:
MOD = 1000000007
dp = [[[0 for _ in range(3)] for _ in range(2)] for _ in range(n + 1)]
dp[0][0][0] = 1 # Base case
for i in range(1, n + 1):
for cntA in range(2):
for cntL in range(3):
# Choose P
dp[i][cntA][0] = (dp[i][cntA][0] + dp[i - 1][cntA][cntL]) % MOD
# Choose A
if cntA > 0:
dp[i][cntA][0] = (dp[i][cntA][0] + dp[i - 1][cntA - 1][cntL]) % MOD
# Choose L
if cntL > 0:
dp[i][cntA][cntL] = (dp[i][cntA][cntL] + dp[i - 1][cntA][cntL - 1]) % MOD
return sum(dp[n][cntA][cntL] for cntA in range(2) for cntL in range(3)) % MODTime & Space Complexity
- Time complexity:
- Space complexity:
4. Dynamic Programming (Space Optimized) - I
Intuition
Since each position only depends on the previous position, we can reduce space from O(n) to O(1) by keeping only two layers: the current and previous states. We store a 2x3 array representing all six possible (absence count, late count) combinations.
Algorithm
- Initialize the base case for length
1: set counts for each (A, L) state. - For each additional position (from length
2ton):- Compute new state counts based on appending
'P','L', or'A'. 'P'sums all late counts for the same absence count (resets late to0).'L'shifts late count forward.'A'transfers from absence0to absence1.
- Compute new state counts based on appending
- After all iterations, sum the six state values for the answer.
class Solution:
def checkRecord(self, n: int) -> int:
if n == 1:
return 3
MOD = 10**9 + 7
dp = {
(0, 0): 1, (0, 1): 1, (0, 2): 0,
(1, 0): 1, (1, 1): 0, (1, 2): 0
}
for i in range(n - 1):
ndp = defaultdict(int)
# Choose P
ndp[(0, 0)] = ((dp[(0, 0)] + dp[(0, 1)]) % MOD + dp[(0, 2)]) % MOD
ndp[(1, 0)] = ((dp[(1, 0)] + dp[(1, 1)]) % MOD + dp[(1, 2)]) % MOD
# Choose L
ndp[(0, 1)] = dp[(0, 0)]
ndp[(1, 1)] = dp[(1, 0)]
ndp[(0, 2)] = dp[(0, 1)]
ndp[(1, 2)] = dp[(1, 1)]
# Choose A
ndp[(1, 0)] = (ndp[(1, 0)] + (((dp[(0, 0)] + dp[(0, 1)]) % MOD + dp[(0, 2)]) % MOD)) % MOD
dp = ndp
return sum(dp.values()) % MODTime & Space Complexity
- Time complexity:
- Space complexity:
5. Dynamic Programming (Space Optimized) - II
Intuition
This is an alternative space-optimized formulation that iterates forward from the empty state. We maintain a 2x3 DP array and update it in place, swapping between current and next arrays each iteration.
Algorithm
- Initialize
dp[0][0] = 1(empty record with no absences and no consecutive lates). - For each position from
1ton:- Create a fresh next state array.
- For each (cntA, cntL) combination, compute transitions:
'P': adds to state (cntA,0) from any (cntA, cntL).'A': adds to state (cntA,0) from (cntA -1, cntL) ifcntA > 0.'L': adds to state (cntA, cntL) from (cntA, cntL -1) ifcntL > 0.
- Swap arrays and continue.
- Sum all six states at the end for the final count.
class Solution:
def checkRecord(self, n: int) -> int:
MOD = 1000000007
dp = [[0] * 3 for _ in range(2)]
dp[0][0] = 1 # Base case
for i in range(1, n + 1):
next_dp = [[0] * 3 for _ in range(2)]
for cntA in range(2):
for cntL in range(3):
# Choose P
next_dp[cntA][0] = (next_dp[cntA][0] + dp[cntA][cntL]) % MOD
# Choose A
if cntA > 0:
next_dp[cntA][0] = (next_dp[cntA][0] + dp[cntA - 1][cntL]) % MOD
# Choose L
if cntL > 0:
next_dp[cntA][cntL] = (next_dp[cntA][cntL] + dp[cntA][cntL - 1]) % MOD
dp = next_dp
return sum(dp[cntA][cntL] for cntA in range(2) for cntL in range(3)) % MODTime & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Forgetting to Reset Consecutive Late Count
When placing a 'P' (present) or 'A' (absent), the consecutive late count resets to 0. A common mistake is to carry forward the late count, which leads to undercounting valid sequences. Only placing 'L' increments the late counter; all other characters reset it.
Allowing More Than One Absence
The problem states that a valid record has fewer than 2 absences, meaning at most 1 absence is allowed. Tracking the absence count incorrectly or allowing transitions that would result in 2 or more absences will produce an inflated count.
Integer Overflow Without Modular Arithmetic
The number of valid records grows exponentially with n, and the problem requires the result modulo 10^9 + 7. Failing to apply the modulo operation after each addition can cause integer overflow, especially in languages with fixed-size integers like Java or C++.
Incorrect State Transitions for Late Count
The late count can only be 0, 1, or 2. When adding 'L', you transition from late count k to k+1, but only if k < 2. Allowing a transition to late count 3 or mishandling the boundary condition will break the constraint of no more than 2 consecutive lates.
Misunderstanding the State Space
The DP state requires tracking both the absence count (0 or 1) and the consecutive late count (0, 1, or 2), giving 6 total states per position. Collapsing these into fewer states or incorrectly indexing the state array leads to wrong answers.