552. 学生出勤记录 II

n 个位置,每个位置可以放 P/A/L 三个字符之一,A 不能超过 1 个,不能有连续的三个 L。

DFS 记录 A 的数量、L 的连续次数,不是 L 时就把 L 的次数清 0。

方法一、DFS(超时)

class Solution:
    def checkRecord(self, n: int) -> int:
        @lru_cache(None)
        def dfs(day, absent, late):
            if day == n:  return 1
            ans = 0
            # 1. 放置 Present,late 清零
            ans = (ans + dfs(day + 1, absent, 0))  % 1000000007    
            # 2. 最多放一个 Absent,late 清零
            if absent < 1:                
                ans = (ans + dfs(day + 1, 1, 0))  % 1000000007    
            # 3. 最多连续放 2 个 Late
            if late < 2:          
                ans = (ans + dfs(day + 1, absent, late + 1)) % 1000000007    

            return ans % 1000000007 

        return dfs(0, 0, 0) % 1000000007        

方法二、DFS + 记忆(记忆化搜索)

状态:[day, absent, late]

class Solution:
    def checkRecord(self, n: int) -> int:
        memo = [[[0]*3 for _ in [0,1]] for _ in range(n)]

        def dfs(day, absent, late):
            if day >= n:  return 1
            if memo[day][absent][late]:
                return memo[day][absent][late]
            ans = 0
            ans += dfs(day + 1, absent, 0)
            if absent < 1:
                ans += dfs(day + 1, 1, 0)
            if late < 2:
                ans += dfs(day + 1, absent, late + 1)

            memo[day][absent][late] = ans % 1000000007
            return ans % 1000000007

        return dfs(0, 0, 0) 

方法三、动态规划

定义 dp[i][j][k] 表示前 i 天有 j 个 A 且 结尾有连续 k 个 L 的方案数。
边界 dp[0][0][0] = 1
状态转移:
dp[i][][] 的值从 dp[i−1][][] 的值转移得到,计算每个状态的值需要考虑第 i 天的出勤记录:

  • 如果第 i 天是 P,则前 i 天和前 i−1 天的相比,A 的数量不变,结尾连续 L 的数量清零,因此对 0 ≤ j ≤ 1,有
    dp[i][j][0] := dp[i][j][0] + ∑ k = 0 2 \sum_{k=0}^2 k=02 dp[i−1][j][k]
  • 如果第 i 天是 A,则前 i 天和前 i−1 天的相比,A 的数量加 1,结尾连续 L 的数量清零,此时要求前 i−1 天的记录中的 A 的数量必须为 0,因此有
    dp[i][1][0]:=dp[i][1][0] + ∑ k = 0 2 \sum_{k=0}^2 k=02 dp[i−1][0][k]
  • 如果第 i 天是 L,则前 i 天和前 i−1 天的相比,A 的数量不变,结尾连续 L 的数量加 1,此时要求前 i−1 天的记录中的结尾连续 L 的数量不超过 2,因此对 0 ≤ j ≤ 1 和 1 ≤ k ≤ 2,有
    dp[i][j][k] := dp[i][j][k] + dp[i−1][j][k−1]

上述状态转移方程对于i=1 也适用。

计算长度为 n 的所有可奖励的出勤记录的数量,即为计算dp[n][][] 的所有元素之和。计算过程中需要将结果对 109+7 取模。

class Solution:
    def checkRecord(self, n: int) -> int:
        MOD = 1000000007
        dp = [[[0, 0, 0], [0, 0, 0]] for _ in range(n+1)]
        dp[0][0][0] = 1
        for i in range(1, n+1):
            # P
            # for j in [0,1]:
            #     for k in [0,1,2]:
            #         dp[i][j][0] = (dp[i][j][0] + dp[i - 1][j][k]) % MOD
            # A
            # for k in [0,1,2]:
            #     dp[i][1][0] = (dp[i][1][0] + dp[i - 1][0][k]) % MOD
            # L
            # for j in [0,1]:
            #     for k in [1,2]:
            #         dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j][k - 1]) % MOD
 			
 			# 其实只有六种状态 
            # 放置 P, A = 0, L 清 0 <= A = 0, L = 0, 1, 2  
            dp[i][0][0] = (dp[i-1][0][0] + dp[i-1][0][1] + dp[i-1][0][2]) % MOD
            # 放置 P, A = 1, L 清 0 <= A = 1, L = 0, 1, 2 
            # 放置 A, L 清 0 <= A = 0, L = 0, 1, 2 
            dp[i][1][0] = (dp[i-1][1][0] + dp[i-1][1][1] + dp[i-1][1][2] + dp[i-1][0][0] + dp[i-1][0][1] + dp[i-1][0][2]) % MOD
            # 放置 L <= A = 0, 1, L = 0, 1 
            dp[i][0][1] = dp[i-1][0][0] % MOD
            dp[i][0][2] = dp[i-1][0][1] % MOD
            dp[i][1][1] = dp[i-1][1][0] % MOD
            dp[i][1][2] = dp[i-1][1][1] % MOD

        return (sum(dp[n][0]) + sum(dp[n][1])) % MOD

方法四、动态规划 + 降维

dp[i][][] 只会从dp[i−1][][] 转移得到。因此可以将 dp 中的总天数的维度省略。

class Solution:
    def checkRecord(self, n: int) -> int:
        MOD = 1000000007
        dp = [[1, 0, 0], [0, 0, 0]]        

        for i in range(1, n+1):
            newdp = [[0, 0, 0], [0, 0, 0]]   
            # # P
            # for j in [0,1]:
            #     for k in [0,1,2]:
            #         newdp[j][0] += dp[j][k]
            #         newdp[j][0] %= MOD
            # # A
            # for k in [0,1,2]:
            #     newdp[1][0] += dp[0][k]
            #     newdp[1][0] %= MOD
            # # L
            # for j in [0,1]:
            #     for k in [1,2]:
            #         newdp[j][k] += dp[j][k-1]
            #         newdp[j][k] %= MOD

            newdp[0][0] = (dp[0][0] + dp[0][1] + dp[0][2]) % MOD
            newdp[0][1] = dp[0][0] % MOD
            newdp[0][2] = dp[0][1] % MOD
            newdp[1][0] = (dp[1][0] + dp[1][1] + dp[1][2] + dp[0][0] + dp[0][1] + dp[0][2]) % MOD
            newdp[1][1] = dp[1][0] % MOD
            newdp[1][2] = dp[1][1] % MOD

            dp = newdp  
                  
        return (sum(map(sum,dp))) % MOD

方法五、动态规划 + 状态机

A 有两种状态 0 or 1 连续的 L 有三种状态 0 or 1 or 2 共六种组合,即共有 6 种状态,所以可以只声明一个一维数组。

二维降一维的计算公式为:index = i * cols + j,其中,cols 表示列数。

初始状态就是字符数为 0,此时 6 个状态对应的字符串数量为 1,0,0,0,0,0

class Solution:
    def checkRecord(self, n: int) -> int:
        MOD = 1000000007
        # 考虑 A 的数量,结尾处连续 L 的数量,一共 6 种状态
        #(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)
        a, b, c, d, e, f = 1, 0, 0, 0, 0, 0
        for i in range(n):
            a, b, c, d, e, f = (a+b+c) % MOD, a % MOD, b % MOD, (a+b+c+d+e+f) % MOD, d % MOD, e % MOD 
         
        return (a+b+c+d+e+f) % MOD