552. Student Attendance Record II
Problem Description
You need to count how many valid attendance records of length n
can be created for a student to be eligible for an attendance award.
An attendance record is a string of length n
where each character represents the student's status for that day:
'A'
means Absent'L'
means Late'P'
means Present
For a student to be eligible for the attendance award, their record must satisfy both conditions:
- Less than 2 total absences: The student can have at most 1
'A'
in their entire record - No 3 consecutive late days: The student cannot have 3 or more
'L'
s in a row anywhere in the record
Given an integer n
, you need to find the total number of different attendance records of length n
that would make a student eligible for the award. Since this number can be very large, return the result modulo 10^9 + 7
.
For example, if n = 2
, valid records include: "PP"
, "PL"
, "LP"
, "LL"
, "AP"
, "PA"
, "AL"
, "LA"
, "LP"
. Records like "AA"
(2 absences) would be invalid.
The solution uses dynamic programming with memoization where dfs(i, j, k)
represents:
i
: current position in the record being builtj
: number of absences used so far (0 or 1)k
: current consecutive late count
At each position, we try adding:
- An absence
'A'
(if we haven't used one yet) - A late
'L'
(if we haven't had 2 consecutive lates) - A present
'P'
(always allowed, resets consecutive late count)
Intuition
The key insight is that we need to track two critical pieces of information as we build each attendance record: how many absences we've used (since we can have at most 1), and how many consecutive lates we currently have (since we can't have 3 in a row).
Think about building the attendance record day by day. At each day, we have three choices - add 'A'
, 'L'
, or 'P'
. But these choices are constrained by our current state:
- We can only add
'A'
if we haven't already used our one allowed absence - We can only add
'L'
if we don't already have 2 consecutive lates - We can always add
'P'
, and doing so resets our consecutive late counter to 0
This naturally leads to a recursive approach where we explore all valid possibilities. The state space is manageable because:
- Position ranges from
0
ton-1
- Absence count is either
0
or1
(once we hit 1, we can't add more) - Consecutive late count ranges from
0
to2
(once we hit 2, we can't add another'L'
)
So we have at most n × 2 × 3
unique states, making dynamic programming with memoization efficient.
The recursive function dfs(i, j, k)
counts all valid records starting from position i
with j
absences already used and k
consecutive lates ending at position i-1
. At each step:
- If
j == 0
, we can add an'A'
and continue withj = 1
and resetk = 0
- If
k < 2
, we can add an'L'
and continue withk + 1
consecutive lates - We can always add a
'P'
and resetk = 0
The base case is when we've built a complete record of length n
, which counts as 1 valid record. The sum of all paths gives us the total number of valid attendance records.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a top-down dynamic programming approach with memoization using Python's @cache
decorator.
State Definition:
The recursive function dfs(i, j, k)
represents:
i
: Current position in the attendance record (0-indexed)j
: Number of absences used so far (0 or 1)k
: Current count of consecutive lates
Base Case:
When i >= n
, we've successfully built a valid attendance record of length n
, so we return 1
.
Recursive Transitions:
At each position i
, we explore three possible actions:
-
Add an Absence ('A'):
- Only possible if
j == 0
(haven't used our one allowed absence) - Transition:
dfs(i + 1, j + 1, 0)
- We increment
j
to 1 and resetk
to 0 (consecutive late count resets)
- Only possible if
-
Add a Late ('L'):
- Only possible if
k < 2
(don't already have 2 consecutive lates) - Transition:
dfs(i + 1, j, k + 1)
- We keep
j
the same and incrementk
- Only possible if
-
Add a Present ('P'):
- Always possible
- Transition:
dfs(i + 1, j, 0)
- We keep
j
the same and resetk
to 0
Implementation Details:
ans = 0 if j == 0: ans += dfs(i + 1, j + 1, 0) # Add 'A' if k < 2: ans += dfs(i + 1, j, k + 1) # Add 'L' ans += dfs(i + 1, j, 0) # Add 'P' return ans % mod
The modulo operation % (10^9 + 7)
is applied at each step to prevent integer overflow.
Time Complexity: O(n × 2 × 3) = O(n)
since we have at most n
positions, 2 possible values for absences (0 or 1), and 3 possible values for consecutive lates (0, 1, or 2).
Space Complexity: O(n)
for the recursion stack depth and the memoization cache.
The cache_clear()
at the end is used to free up memory after computation, though it's not strictly necessary for correctness.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with n = 3
to illustrate how the solution builds valid attendance records.
Starting with dfs(0, 0, 0)
- position 0, no absences used, no consecutive lates:
Position 0 choices:
- Add 'A':
dfs(1, 1, 0)
- move to position 1, used 1 absence, reset consecutive lates - Add 'L':
dfs(1, 0, 1)
- move to position 1, no absences, 1 consecutive late - Add 'P':
dfs(1, 0, 0)
- move to position 1, no absences, reset consecutive lates
Let's trace one path: Starting with 'A' → dfs(1, 1, 0)
:
Position 1 choices (after 'A'):
Add 'A': Not allowed (j = 1, already used our one absence)- Add 'L':
dfs(2, 1, 1)
- creates "AL_" - Add 'P':
dfs(2, 1, 0)
- creates "AP_"
Following "AL" path → dfs(2, 1, 1)
:
Position 2 choices (after 'AL'):
Add 'A': Not allowed (already used absence)- Add 'L':
dfs(3, 1, 2)
- creates "ALL" - Add 'P':
dfs(3, 1, 0)
- creates "ALP"
Both paths reach dfs(3, _, _)
which returns 1 (base case: i >= n).
So from "AL", we get 2 valid records: "ALL" and "ALP".
Similarly, from "AP" → dfs(2, 1, 0)
:
- Can't add 'A' (used absence)
- Can add 'L' → "APL" ✓
- Can add 'P' → "APP" ✓
This gives us 2 more valid records.
Complete enumeration for n = 3:
Starting with 'A' (4 valid records):
- "ALL", "ALP", "APL", "APP"
Starting with 'L' (7 valid records):
- "LAL", "LAP", "LLA", "LLP", "LPL", "LPP", "LLL" (note: "LLL" is invalid - 3 consecutive lates)
- Actually: "LAL", "LAP", "LLP", "LPL", "LPA", "LPP" (6 valid)
Starting with 'P' (8 valid records):
- "PAL", "PAP", "PLA", "PLL", "PLP", "PPA", "PPL", "PPP"
Wait, let me recalculate more carefully:
Through the recursive process, each path that doesn't violate our constraints (≤1 absence, <3 consecutive lates) contributes 1 to our count. The memoization ensures we don't recompute states like dfs(2, 0, 1)
which could be reached via "PL" or "LL".
The final answer for n = 3 would be 19 valid attendance records, computed by summing all valid paths through our state space.
Solution Implementation
1class Solution:
2 def checkRecord(self, n: int) -> int:
3 """
4 Calculate the number of valid attendance records of length n.
5 A valid record has:
6 - At most 1 absence ('A')
7 - No more than 2 consecutive late days ('L')
8
9 Args:
10 n: Length of the attendance record
11
12 Returns:
13 Number of valid attendance records modulo 10^9 + 7
14 """
15 from functools import cache
16
17 MOD = 10**9 + 7
18
19 @cache
20 def dp(day: int, absence_count: int, consecutive_late: int) -> int:
21 """
22 Dynamic programming function to count valid attendance records.
23
24 Args:
25 day: Current day index (0-indexed)
26 absence_count: Number of absences used so far (0 or 1)
27 consecutive_late: Number of consecutive late days ending at current position
28
29 Returns:
30 Number of valid attendance records from this state
31 """
32 # Base case: reached the end of the record
33 if day >= n:
34 return 1
35
36 result = 0
37
38 # Option 1: Mark as Absent ('A') - only if no absence used yet
39 if absence_count == 0:
40 result += dp(day + 1, absence_count + 1, 0)
41
42 # Option 2: Mark as Late ('L') - only if less than 2 consecutive lates
43 if consecutive_late < 2:
44 result += dp(day + 1, absence_count, consecutive_late + 1)
45
46 # Option 3: Mark as Present ('P') - always allowed, resets consecutive late count
47 result += dp(day + 1, absence_count, 0)
48
49 return result % MOD
50
51 # Calculate the answer starting from day 0 with no absences and no consecutive lates
52 answer = dp(0, 0, 0)
53
54 # Clear the cache to free memory
55 dp.cache_clear()
56
57 return answer
58
1class Solution {
2 private static final int MOD = 1_000_000_007;
3 private int recordLength;
4 private Integer[][][] memo;
5
6 /**
7 * Counts the number of valid attendance records of length n.
8 * A record is valid if it contains:
9 * - At most one 'A' (absent)
10 * - No more than two consecutive 'L' (late)
11 *
12 * @param n the length of the attendance record
13 * @return the number of valid attendance records modulo 10^9 + 7
14 */
15 public int checkRecord(int n) {
16 this.recordLength = n;
17 // memo[day][absenceCount][consecutiveLates]
18 // - day: current position in the record (0 to n-1)
19 // - absenceCount: number of 'A's used so far (0 or 1)
20 // - consecutiveLates: number of consecutive 'L's ending at current position (0, 1, or 2)
21 this.memo = new Integer[n][2][3];
22
23 // Start DFS from day 0 with 0 absences and 0 consecutive lates
24 return dfs(0, 0, 0);
25 }
26
27 /**
28 * Recursively calculates the number of valid attendance records.
29 *
30 * @param currentDay the current day/position in the record
31 * @param absenceCount the number of 'A's used so far (0 or 1)
32 * @param consecutiveLates the number of consecutive 'L's ending at current position
33 * @return the number of valid records from this state
34 */
35 private int dfs(int currentDay, int absenceCount, int consecutiveLates) {
36 // Base case: reached the end of the record successfully
37 if (currentDay >= recordLength) {
38 return 1;
39 }
40
41 // Check if this state has been computed before
42 if (memo[currentDay][absenceCount][consecutiveLates] != null) {
43 return memo[currentDay][absenceCount][consecutiveLates];
44 }
45
46 // Option 1: Mark as 'P' (present) - always allowed
47 // Reset consecutive lates to 0 when marking present
48 int validRecords = dfs(currentDay + 1, absenceCount, 0);
49
50 // Option 2: Mark as 'A' (absent) - only if no absence used yet
51 if (absenceCount == 0) {
52 validRecords = (validRecords + dfs(currentDay + 1, absenceCount + 1, 0)) % MOD;
53 }
54
55 // Option 3: Mark as 'L' (late) - only if less than 2 consecutive lates
56 if (consecutiveLates < 2) {
57 validRecords = (validRecords + dfs(currentDay + 1, absenceCount, consecutiveLates + 1)) % MOD;
58 }
59
60 // Memoize and return the result
61 memo[currentDay][absenceCount][consecutiveLates] = validRecords;
62 return validRecords;
63 }
64}
65
1class Solution {
2public:
3 int checkRecord(int n) {
4 // dp[day][absences][consecutive_lates] = number of valid attendance records
5 // day: current day (0 to n-1)
6 // absences: number of 'A' (absent) records so far (0 or 1)
7 // consecutive_lates: number of consecutive 'L' (late) records ending at current position (0, 1, or 2)
8 int dp[n][2][3];
9 memset(dp, -1, sizeof(dp));
10
11 const int MOD = 1e9 + 7;
12
13 // Recursive function with memoization to count valid attendance records
14 auto dfs = [&](this auto&& dfs, int day, int absenceCount, int consecutiveLates) -> int {
15 // Base case: reached the end of n days
16 if (day >= n) {
17 return 1;
18 }
19
20 // Return memoized result if already computed
21 if (dp[day][absenceCount][consecutiveLates] != -1) {
22 return dp[day][absenceCount][consecutiveLates];
23 }
24
25 // Option 1: Mark as 'P' (present) - always allowed
26 // Reset consecutive late count to 0
27 int result = dfs(day + 1, absenceCount, 0);
28
29 // Option 2: Mark as 'A' (absent) - only if no previous absence
30 if (absenceCount == 0) {
31 result = (result + dfs(day + 1, absenceCount + 1, 0)) % MOD;
32 }
33
34 // Option 3: Mark as 'L' (late) - only if less than 2 consecutive lates
35 if (consecutiveLates < 2) {
36 result = (result + dfs(day + 1, absenceCount, consecutiveLates + 1)) % MOD;
37 }
38
39 // Memoize and return the result
40 return dp[day][absenceCount][consecutiveLates] = result;
41 };
42
43 // Start from day 0 with 0 absences and 0 consecutive lates
44 return dfs(0, 0, 0);
45 }
46};
47
1function checkRecord(n: number): number {
2 // dp[day][absences][consecutiveLates] = number of valid attendance records
3 // day: current day (0 to n-1)
4 // absences: number of 'A' (absent) records so far (0 or 1)
5 // consecutiveLates: number of consecutive 'L' (late) records ending at current position (0, 1, or 2)
6 const dp: number[][][] = Array(n)
7 .fill(null)
8 .map(() => Array(2)
9 .fill(null)
10 .map(() => Array(3).fill(-1))
11 );
12
13 const MOD = 1e9 + 7;
14
15 // Recursive function with memoization to count valid attendance records
16 const dfs = (day: number, absenceCount: number, consecutiveLates: number): number => {
17 // Base case: reached the end of n days
18 if (day >= n) {
19 return 1;
20 }
21
22 // Return memoized result if already computed
23 if (dp[day][absenceCount][consecutiveLates] !== -1) {
24 return dp[day][absenceCount][consecutiveLates];
25 }
26
27 // Option 1: Mark as 'P' (present) - always allowed
28 // Reset consecutive late count to 0
29 let result = dfs(day + 1, absenceCount, 0);
30
31 // Option 2: Mark as 'A' (absent) - only if no previous absence
32 if (absenceCount === 0) {
33 result = (result + dfs(day + 1, absenceCount + 1, 0)) % MOD;
34 }
35
36 // Option 3: Mark as 'L' (late) - only if less than 2 consecutive lates
37 if (consecutiveLates < 2) {
38 result = (result + dfs(day + 1, absenceCount, consecutiveLates + 1)) % MOD;
39 }
40
41 // Memoize and return the result
42 dp[day][absenceCount][consecutiveLates] = result;
43 return result;
44 };
45
46 // Start from day 0 with 0 absences and 0 consecutive lates
47 return dfs(0, 0, 0);
48}
49
Time and Space Complexity
Time Complexity: O(n)
The function uses memoization with @cache
decorator. The state space is defined by three parameters:
i
: ranges from0
ton
(n possible values)j
: can only be0
or1
(2 possible values for absence count)k
: can be0
,1
, or2
(3 possible values for consecutive late count)
Total number of unique states = n × 2 × 3 = 6n
Each state is computed only once due to memoization, and each state computation involves at most 3 recursive calls with constant time operations. Therefore, the time complexity is O(6n) = O(n)
.
Space Complexity: O(n)
The space complexity consists of:
- Recursion stack depth: The maximum recursion depth is
n
(wheni
goes from0
ton
), contributingO(n)
space. - Cache storage: The memoization cache stores at most
6n
states, each taking constant space, contributingO(n)
space.
Overall space complexity is O(n) + O(n) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Apply Modulo at Each Step
Pitfall: A common mistake is only applying the modulo operation at the final return, rather than at each recursive step. This can lead to integer overflow issues for large values of n
.
Incorrect approach:
def dp(day, absence_count, consecutive_late):
if day >= n:
return 1
result = 0
if absence_count == 0:
result += dp(day + 1, absence_count + 1, 0)
if consecutive_late < 2:
result += dp(day + 1, absence_count, consecutive_late + 1)
result += dp(day + 1, absence_count, 0)
return result # Missing modulo here!
# Only applying modulo at the end
return dp(0, 0, 0) % MOD # Too late - overflow may have already occurred
Solution: Apply modulo at each recursive return to keep numbers manageable:
return result % MOD # Apply modulo at each step
2. Incorrect State Transition for Consecutive Lates
Pitfall: Forgetting to reset the consecutive late count when adding a non-late character ('A' or 'P'). This would incorrectly track consecutive lates across non-late days.
Incorrect approach:
# Wrong: Not resetting consecutive_late when adding 'A' if absence_count == 0: result += dp(day + 1, absence_count + 1, consecutive_late) # Should be 0, not consecutive_late # Wrong: Not resetting consecutive_late when adding 'P' result += dp(day + 1, absence_count, consecutive_late) # Should be 0, not consecutive_late
Solution: Always reset consecutive_late
to 0 when adding 'A' or 'P':
if absence_count == 0: result += dp(day + 1, absence_count + 1, 0) # Reset to 0 result += dp(day + 1, absence_count, 0) # Reset to 0
3. Off-by-One Error in Consecutive Late Check
Pitfall: Using the wrong comparison operator for checking consecutive lates. The condition should be consecutive_late < 2
(allowing 0 or 1 previous consecutive lates before adding another), not consecutive_late <= 2
.
Incorrect approach:
# Wrong: This would allow 3 consecutive lates if consecutive_late <= 2: # Incorrect! result += dp(day + 1, absence_count, consecutive_late + 1)
Solution: Use strict less than (<
) to ensure we never have 3 consecutive lates:
if consecutive_late < 2: # Correct - only allows 0 or 1 previous consecutive lates result += dp(day + 1, absence_count, consecutive_late + 1)
4. Not Using Memoization or Using It Incorrectly
Pitfall: Implementing the recursive solution without memoization leads to exponential time complexity due to repeated calculations of the same states.
Incorrect approach:
def dp(day, absence_count, consecutive_late):
# No @cache decorator or manual memoization
if day >= n:
return 1
# ... rest of the logic
Solution: Use Python's @cache
decorator or implement manual memoization:
from functools import cache
@cache # Essential for avoiding repeated calculations
def dp(day, absence_count, consecutive_late):
# ... implementation
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!