Facebook Pixel

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:

  1. Less than 2 total absences: The student can have at most 1 'A' in their entire record
  2. 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 built
  • j: 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)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 to n-1
  • Absence count is either 0 or 1 (once we hit 1, we can't add more)
  • Consecutive late count ranges from 0 to 2 (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 with j = 1 and reset k = 0
  • If k < 2, we can add an 'L' and continue with k + 1 consecutive lates
  • We can always add a 'P' and reset k = 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:

  1. 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 reset k to 0 (consecutive late count resets)
  2. 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 increment k
  3. Add a Present ('P'):

    • Always possible
    • Transition: dfs(i + 1, j, 0)
    • We keep j the same and reset k 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 Evaluator

Example 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:

  1. Add 'A': dfs(1, 1, 0) - move to position 1, used 1 absence, reset consecutive lates
  2. Add 'L': dfs(1, 0, 1) - move to position 1, no absences, 1 consecutive late
  3. 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'):

  1. Add 'A': Not allowed (j = 1, already used our one absence)
  2. Add 'L': dfs(2, 1, 1) - creates "AL_"
  3. Add 'P': dfs(2, 1, 0) - creates "AP_"

Following "AL" path → dfs(2, 1, 1):

Position 2 choices (after 'AL'):

  1. Add 'A': Not allowed (already used absence)
  2. Add 'L': dfs(3, 1, 2) - creates "ALL"
  3. 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 from 0 to n (n possible values)
  • j: can only be 0 or 1 (2 possible values for absence count)
  • k: can be 0, 1, or 2 (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 (when i goes from 0 to n), contributing O(n) space.
  • Cache storage: The memoization cache stores at most 6n states, each taking constant space, contributing O(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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More