Loading

leetcode552. 学生出勤记录 II

题目链接:https://leetcode-cn.com/problems/student-attendance-record-ii/

昨天的题目是直接给出字符串让你判断,而且长度较小,今天的出勤记录的长度变得很大,并且不是直接给你字符串,而是需要你统计在给定出勤记录长度下可以获得奖励的所有排列数量,情况会复杂很多。
因为新一天的排列数由当天的记录和前一天的排列数决定,所以可以用动态规划来解决。

动规五部曲:

1.确定dp数组的定义以及下标的含义
dp[i][j][k]表示第i天时有j个'A'且结尾连续有k个'L'的可获得奖励的排列的数量,其中0<=i<=n,0<=j<=1(缺勤少于两天),0<=k<=2(不能连续迟到三天及以上)

2.递推公式(状态转移方程)
当1≤i≤n 时,dp[i][][] 的值从dp[i−1][][] 的值转移得到,计算每个状态的值需要考虑第i天的出勤记录:

  • 如果第i天的出勤记录为'P',出席是最好的情况,'A'的数量不变,结尾'L'的次数清零,前一天所有'A'的数量小于2并且结尾'L'数量小于3的出勤排列再加上当天的P之后都是可获得奖励的排列,所以前面所有可获得奖励的排列数都可以加上,因此对0<=j<=1,有:
    dp[i][j][0] += (dp[i-1][j][0] + dp[i-1][j][1] + dp[i-1][j][2]);
  • 如果第i天的出勤记录为'A',缺席是不好的情况,'A'的数量+1,结尾'L'的次数清零,而且要求前i-1天的'A'的数量为0,因为如果前面缺勤过一次,今天再缺勤就GG了,所以只能加上前一天缺勤次数为0的和连续迟到少于3天的:
    dp[i][1][0] += (dp[i-1][0][0] + dp[i-1][0][1] + dp[i-1][0][2]);
  • 如果第i天的出勤记录为'L',迟到也是不好的情况,'A'的数量不变,结尾'L'的次数+1,并且要求前i-1天结尾'L'的数量小于2,如果前面连续迟到两天的话,今天再迟到就GG了,所以只能加上'A'的数量为0和1的和连续迟到少于2天的,因此对于0<=j<=1,1<=k<=2,有:
    dp[i][j][k] += (dp[i-1][j][k-1]);

上述状态转移方程对于i=1也适用,因为i=1时,结果为3,分别是出勤dp[1][0][0]、缺勤dp[1][1][0]、迟到dp[1][0][1],它们都会加上dp[0][0][0]也就是1,刚好是想要的结果。

3.dp数组的初始化
i=0时,没有出勤记录,当然也就不会出现'A'和'L',可以获得奖励,所以边界情况是dp[0][0][0] = 1;

4.遍历顺序
顺序当然就是从第1天开始向后遍历。

5.举例推导dp数组
题目所求即是dp[n][][]的所有元素之和。

这里再贴一下评论区里来自龅牙叔的解释,我觉得写得很好:

状态转移方程看得我头皮发麻。在纸上把所有情况枚举了一遍才弄懂:

输入n代表的是天数,我每天去上学,有三种结果:准时P,缺席P,迟到L。如果我n天后没有缺席超过一次,且没有连续超过两天迟到,则我可以拿到奖励。

我在1~n中的任何一天,是否有可能拿到奖励,取决于我过去的日子的上学情况,例如:

例子1:如果今天缺席了,而我过去某一天也缺席了,则我一定无法最终拿到奖励。我今天拿不到奖励都是因为过去的自己没表现好。

例子2:如果今天缺席了,我过去每一天都是好学生,一次缺席也没有,一次迟到也没有,那我今天缺席一次没啥嘛,之后好好表现还是有可能拿到奖励。

这样就是从小问题可以推导出大问题,用动态规划解。

dp[i][j][k]等于第i天,在缺席了j次,以及连续迟到了k次的情况下,还可以拿到奖励的出席排列的数量。
随着天数的增加,这个dp的值肯定是递减的,因为搞不好未来哪一天就缺席或者迟到再也拿不到奖励了。j一共有两种可能,0或者1,对应不能缺席超过1次。k一共有三种可能,0, 1, 2,对应不能连续迟到超过2次。

dp[i][][]一共有六种:

dp[i][0][0]:今天截止0次缺席,今天截止0连续迟到。
dp[i][0][1]:今天截止0次缺席,今天截止1连续迟到。
dp[i][0][2]:今天截止0次缺席,今天截止2连续迟到。
dp[i][1][0]:今天截止1次缺席,今天截止0连续迟到。
dp[i][1][1]:今天截止1次缺席,今天截止1连续迟到。
dp[i][1][2]:今天截止1次缺席,今天截止2连续迟到。
我们依次分析六种情况:

dp[i][0][0] = dp[i - 1][0][0] + dp[i - 1][0][1] + dp[i - 1][0][2]
今天截止0次缺席,今天截止0次连续迟到:过去一定也是0次缺席,但过去可以有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]
今天截止1次缺席,今天截止0次连续迟到:分两种情况:

a. 前三项,代表过去有1次缺席,今天没有缺席,但过去可以有0,1,2次连续迟到,因为只要我今天按时出席,过去的迟到记录都一笔勾销。
b. 后三项,代表过去有0次缺席,今天缺席了,但过去可以有0,1,2次连续迟到,因为只要我今天不迟到,过去的迟到记录都一笔勾销。
dp[i][0][1] = dp[i - 1][0][0];
今天截止0次缺席,今天截止1次连续迟到:过去0次缺席,过去0次迟到,今天迟到了。
dp[i][0][2] = dp[i - 1][0][1];
今天截止0次缺席,今天截止2次连续迟到:过去0次缺席,昨天迟到,今天迟到了。
dp[i][1][1] = dp[i - 1][1][0];
今天截止1次缺席,今天截止1次连续迟到:过去1次缺席,过去0次迟到,今天迟到了。这里可以仔细想一下,我今天必须要迟到,所以今天截止的1次缺席必定来自过去。
dp[i][1][2] = dp[i - 1][1][1];
今天截止1次缺席,今天截止2次连续迟到:过去1次缺席,昨天迟到,今天迟到了。
最后,把最后一天的六种情况加和即为答案。

这道题你把状态转移方程写出来了,代码几分钟就写好了。

C++代码

class Solution {
public:
    static constexpr int MOD = 1'000'000'007;

    int checkRecord(int n) {
        vector<vector<vector<int>>> dp(n + 1, vector<vector<int>>(2, vector<int>(3)));  // 长度,A 的数量,结尾连续 L 的数量
        dp[0][0][0] = 1;
        for (int i = 1; i <= n; i++) {
            // 以 P 结尾的数量
            for (int j = 0; j <= 1; j++) {
                for (int k = 0; k <= 2; k++) {
                    dp[i][j][0] = (dp[i][j][0] + dp[i - 1][j][k]) % MOD;
                }
            }
            // 以 A 结尾的数量
            for (int k = 0; k <= 2; k++) {
                dp[i][1][0] = (dp[i][1][0] + dp[i - 1][0][k]) % MOD;
            }
            // 以 L 结尾的数量
            for (int j = 0; j <= 1; j++) {
                for (int k = 1; k <= 2; k++) {
                    dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j][k - 1]) % MOD;
                }
            }
        }
        int sum = 0;
        for (int j = 0; j <= 1; j++) {
            for (int k = 0; k <= 2; k++) {
                sum = (sum + dp[n][j][k]) % MOD;
            }
        }
        return sum;
    }
};

注意到dp[i-1][][]只有在推导dp[i][][]时用到,所以可以把天数的维度省略,

class Solution {
public:
    static constexpr int MOD = 1'000'000'007;

    int checkRecord(int n) {
        int dp[2][3];  // A 的数量,结尾连续 L 的数量
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int dpNew[2][3];  // A 的数量,结尾连续 L 的数量
            memset(dpNew, 0, sizeof(dpNew));
            // 以 P 结尾的数量
            for (int j = 0; j <= 1; j++) {
                for (int k = 0; k <= 2; k++) {
                    dpNew[j][0] = (dpNew[j][0] + dp[j][k]) % MOD;
                }
            }
            // 以 A 结尾的数量
            for (int k = 0; k <= 2; k++) {
                dpNew[1][0] = (dpNew[1][0] + dp[0][k]) % MOD;
            }
            // 以 L 结尾的数量
            for (int j = 0; j <= 1; j++) {
                for (int k = 1; k <= 2; k++) {
                    dpNew[j][k] = (dpNew[j][k] + dp[j][k - 1]) % MOD;
                }
            }
            memcpy(dp, dpNew, sizeof(dp));
        }
        int sum = 0;
        for (int j = 0; j <= 1; j++) {
            for (int k = 0; k <= 2; k++) {
                sum = (sum + dp[j][k]) % MOD;
            }
        }
        return sum;
    }
};
posted @ 2021-08-19 00:21  泠枫Jun  阅读(71)  评论(0编辑  收藏  举报