1. 程式人生 > >[LeetCode] Student Attendance Record II 學生出勤記錄之二

[LeetCode] 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的拓展,但是比那道題難度要大的多。從題目中說結果要對一個很大的數取餘,說明結果是一個很大很大的數。那麼一般來說這種情況不能用遞迴來求解,可能會爆棧,所以要考慮利用數學方法或者DP來做。其實博主最先看到這題的時候,心想這不就是高中時候學的排列組合的題嗎,於是又在想怎麼寫那些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 = 1000000007;
        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 = 1000000007;
        vector<int> P(n), L(n), A(n);
        P[0] = 1; L[0] = 1; L[1] = 3;
        A[0] = 1; A[1] = 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個數字中已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 = 1000000007;
        vector<long 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 long res = PorL[n];
        for (int i = 0; i < n; ++i) {
            long long t = (PorL[i] * PorL[n - 1 - i]) % M;
            res = (res + t) % M;
        }
        return res;
    }
};

類似題目:

參考資料: