552. Student Attendance Record II - Explanation

Problem Link



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

  1. Define a recursive function dfs(i, cntA, cntL) where i is the remaining length, cntA is the number of absences used, and cntL is the current consecutive late streak.
  2. Base case: when i == 0, we have built a valid record, so return 1.
  3. For each position, we have three choices:
    • Place 'P': always valid, resets consecutive late count to 0.
    • Place 'A': only valid if cntA == 0 (no absence used yet), resets late count to 0.
    • Place 'L': only valid if cntL < 2 (fewer than 2 consecutive lates), increments late count.
  4. Sum all valid transitions and cache the result.
  5. 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: O(n)O(n)
  • Space complexity: O(n)O(n)

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

  1. For the base case n == 1, manually define the counts for each (A, L) state.
  2. For larger n, recursively get the state counts for n - 1.
  3. Build the new state counts by considering what character we append:
    • Appending 'P' sums all states with the same absence count (resets late to 0).
    • Appending 'L' shifts the late count up by 1.
    • Appending 'A' moves from absence count 0 to 1 (resets late to 0).
  4. Cache results to avoid recomputation.
  5. Sum all six final state values modulo 109+710^9 + 7.
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()) % MOD

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

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

  1. Initialize dp[0][0][0] = 1 (one way to have an empty record with no absences and no lates).
  2. For each position from 1 to n, for each state (cntA, cntL):
    • Adding 'P': transitions from any late count to late count 0 with the same absence count.
    • Adding 'A': if cntA > 0, transitions from absence count cntA - 1 to cntA with late count 0.
    • Adding 'L': if cntL > 0, transitions from late count cntL - 1 to cntL with the same absence count.
  3. Sum all states at position n for 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)) % MOD

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

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

  1. Initialize the base case for length 1: set counts for each (A, L) state.
  2. For each additional position (from length 2 to n):
    • Compute new state counts based on appending 'P', 'L', or 'A'.
    • 'P' sums all late counts for the same absence count (resets late to 0).
    • 'L' shifts late count forward.
    • 'A' transfers from absence 0 to absence 1.
  3. 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()) % MOD

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(1)O(1)

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

  1. Initialize dp[0][0] = 1 (empty record with no absences and no consecutive lates).
  2. For each position from 1 to n:
    • 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) if cntA > 0.
      • 'L': adds to state (cntA, cntL) from (cntA, cntL - 1) if cntL > 0.
    • Swap arrays and continue.
  3. 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)) % MOD

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(1)O(1)

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.