[LeetCode] 552. Student Attendance Record II 學生出勤記錄之二


 

Given a positive integer n, return the number of all possible attendance records with length n, which will be regarded as rewardable. The answer may be very large, return it after mod 109 + 7.

A student attendance record is a string that only contains the following three characters:

 

  1. 'A' : Absent.
  2. 'L' : Late.
  3. 'P' : Present.

A record is regarded as rewardable if it doesn't contain more than one 'A' (absent) or more than two continuous 'L' (late).

Example 1:

Input: n = 2
Output: 8 
Explanation:
There are 8 records with length 2 will be regarded as rewardable:
"PP" , "AP", "PA", "LP", "PL", "AL", "LA", "LL"
Only "AA" won't be regarded as rewardable owing to more than one absent times. 

Note: The value of n won't exceed 100,000.

 

這道題是之前那道 Student Attendance Record I 的拓展,但是比那道題難度要大的多。從題目中說結果要對一個很大的數取余,說明結果是一個很大很大的數。一般來說這種情況不能用遞歸來求解,可能會爆棧,所以要考慮利用數學方法或者動態規划 Dynamic Programming 來做。其實博主最先看到這題的時候,心想這不就是高中時候學的排列組合的題嗎,於是又在想怎么寫那些A幾幾,C幾幾的式子來求結果,可惜並沒有做出來。現在想想怎么當初高中的自己這么生猛,感覺啥都會的樣子,上知天文下知地理,數理化生樣樣精通的感覺,燃鵝隨着時間的推移,所有的一切都還給了老師。總感覺這題用數學的方法應該也可以解,但是看網上的大神們都是用 DP 做的,沒辦法,那只能用 DP 來做了。下面這種做法來自 大神 lixx2100 的帖子,這里定義一個三維的 dp 數組,其中 dp[i][j][k] 表示數組前i個數字中,最多有j個A,最多有k個連續L的組合方式,那么最終要求的結果就保存在dp[n][1][2]中。然后來考慮如何求 dp[i][j][k] 的狀態轉移方程,首先來取出前一個狀態下的值,就是前 i-1 個數的值 dp[i-1][j][2],即數組前 i-1 個數中,最多有j個A,最多有2個連續L的排列方式,然后如果 j>0,那么再加上 dp[i-1][j-1][2],即加上了最多有j-1個A的情況,並對超大數取余;如果 k>0,則再加上 dp[i-1][j][k-1],即加上了最多有j個A,最多有 k-1 個連續L的排列方式,其實博主並沒有完全理解為什么要這么更新,如果有大神們理解了這么做的含義,請不吝賜教,在下方留言告知博主啊~

 

解法一:

class Solution {
public:
    int checkRecord(int n) {
        int M = 1e9 + 7;
        int dp[n + 1][2][3] = {0};
        for (int j = 0; j < 2; ++j) {
            for (int k = 0; k < 3; ++k) {
                dp[0][j][k] = 1;
            }
        }
        for (int i = 1; i <= n; ++i) {
            for (int j = 0; j < 2; ++j) {
                for (int k = 0; k < 3; ++k) {
                    int val = dp[i - 1][j][2];
                    if (j > 0) val = (val + dp[i - 1][j - 1][2]) % M;
                    if (k > 0) val = (val + dp[i - 1][j][k - 1]) % M;
                    dp[i][j][k] = val;
                }
            }
        }
        return dp[n][1][2];
    }
};

 

下面這種方法來自 大神 KJer 的帖子,大神帖子里面的講解寫的很詳細,很贊,也不難讀懂。定義了三個 DP 數組 P, L, A,其中 P[i] 表示數組 [0,i] 范圍內以P結尾的所有排列方式,L[i] 表示數組 [0,i] 范圍內以L結尾的所有排列方式,A[i] 表示數組 [0,i] 范圍內以A結尾的所有排列方式。那么最終所求的就是 P[n-1] + L[n-1] + A[n-1] 了,難點就是分別求出 P, L, A 數組的遞推公式了。

首先來看P數組的,P字符沒有任何限制條件,可以跟在任何一個字符后面,所以有 P[i] = A[i-1] + P[i-1] + L[i-1]

再來看L數組的,L字符唯一的限制條件是不能有超過兩個連續的L,那么在P和L字符后面可以加1一個L,如果前一個字符是L,要看再前面的一位是什么字符,如果是P或着A的話,可以加L,如果是L的話,就不能再加了,否則就連續3個了,所以有 L[i] = A[i-1] + P[i-1] + A[i-2] + P[i-2]

最后來看A數組的,這個比較麻煩,字符A的限制條件是整個字符串最多只能有1個A,那么當前一個字符是A的話,就不能再加A來,當前一個字符是P或者L的話,要確定之前從沒有A出現過,才能加上A。那么實際上還需要定義兩個數組 P1, L1, 其中 P1[i] 表示數組 [0,i] 范圍內以P結尾的不包含A的所有排列方式,L1[i] 表示數組 [0,i] 范圍內以L結尾的不包含A的所有排列方式,根據前兩種情況不難推出 P1 和 L1 的遞推公式,再加上A的遞推公式如下:

A[i] = P1[i-1] + L1[i-1]

P1[i] = P1[i-1] + L1[i-1]

L1[i] = P1[i-1] + P1[i-2]

將第二第三個等式多次帶入第一個等式,就可以將 P1 和 L1 消掉,可以化簡為:

A[i] = A[i-1] + A[i-2] + A[i-3]

這樣就可以少定義兩個數組了,狀態轉移方程有了,代碼也就不難寫了:

 

解法二:

class Solution {
public:
    int checkRecord(int n) {
        int M = 1e9 + 7;
        vector<int> P(n), L(n), A(n);
        P[0] = 1; L[0] = 1; A[0] = 1;
        if (n > 1) { L[1] = 3; A[1] = 2; }
        if (n > 2) A[2] = 4;
        for (int i = 1; i < n; ++i) {
            P[i] = ((P[i - 1] + L[i - 1]) % M + A[i - 1]) % M;
            if (i > 1) L[i] = ((A[i - 1] + P[i - 1]) % M + (A[i - 2] + P[i - 2]) % M) % M;
            if (i > 2) A[i] = ((A[i - 1] + A[i - 2]) % M + A[i - 3]) % M;
        }
        return ((A[n - 1] + P[n - 1]) % M + L[n - 1]) % M;
    }
};

 

下面這種方法來自 大神 dettier 的帖子,這里面定義了兩個數組P和 PorL,其中 P[i] 表示數組前i個數字中1以P結尾的排列個數,PorL[i] 表示數組前i個數字中已P或者L結尾的排列個數。這個解法的精髓是先不考慮字符A的情況,而是先把定義的這個數組先求出來,由於P字符可以再任意字符后面加上,所以 P[i] = PorL[i-1];而 PorL[i] 由兩部分組成,P[i] + L[i],其中 P[i] 已經更新了,L[i] 只能當前一個字符是P,或者前一個字符是L且再前一個字符是P的時候加上,即為 P[i-1] + P[i-2],所以 PorL[i] = P[i] + P[i-1] + P[i-2]。

那么這里就已經把不包含A的情況求出來了,存在了 PorL[n] 中,下面就是要求包含一個A的情況,那么就得去除一個字符,從而給A留出位置。就相當於在數組的任意一個位置上加上A,數組就被分成左右兩個部分了,而這兩個部分當然就不能再有A了,實際上所有不包含A的情況都已經在數組 PorL 中計算過了,而分成的子數組的長度又不會大於原數組的長度,所以直接在 PorL 中取值就行了,兩個子數組的排列個數相乘,然后再把所有分割的情況累加起來就是最終結果啦,參見代碼如下:

 

解法三:

class Solution {
public:
    int checkRecord(int n) {
        int M = 1e9 + 7;
        vector<long> P(n + 1), PorL(n + 1);
        P[0] = 1; PorL[0] = 1; PorL[1] = 2;
        for (int i = 1; i <= n; ++i) {
            P[i] = PorL[i - 1];
            if (i > 1) PorL[i] = (P[i] + P[i - 1] + P[i - 2]) % M;
        }
        long res = PorL[n];
        for (int i = 0; i < n; ++i) {
            long t = (PorL[i] * PorL[n - 1 - i]) % M;
            res = (res + t) % M;
        }
        return res;
    }
};

 

Github 同步地址:

https://github.com/grandyang/leetcode/issues/552

 

類似題目:

Student Attendance Record I

 

參考資料:

https://leetcode.com/problems/student-attendance-record-ii/

https://leetcode.com/problems/student-attendance-record-ii/discuss/101638/Simple-Java-O(n)-solution

https://leetcode.com/problems/student-attendance-record-ii/discuss/101633/Improving-the-runtime-from-O(n)-to-O(log-n)

https://leetcode.com/problems/student-attendance-record-ii/discuss/101643/Share-my-O(n)-C%2B%2B-DP-solution-with-thinking-process-and-explanation

 

LeetCode All in One 題目講解匯總(持續更新中...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM