给定一个正整数 n,返回长度为 n 的所有可被视为可奖励的出勤记录的数量。 答案可能非常大,你只需返回结果mod 109 + 7的值。
学生出勤记录是只包含以下三个字符的字符串:
‘A’ : Absent,缺勤
‘L’ : Late,迟到
‘P’ : Present,到场
如果记录不包含多于一个’A’(缺勤)或超过两个连续的’L’(迟到),则该记录被视为可奖励的。
示例 1:
输入: n = 2
输出: 8
解释:
有8个长度为2的记录将被视为可奖励:
"PP" , "AP", "PA", "LP", "PL", "AL", "LA", "LL"
只有"AA"不会被视为可奖励,因为缺勤次数超过一次。
注意:n 的值不会超过100000。
根据题目的意思,其实可以理解为:在三个不同的字符中,有放回的抽取 n 个字符,求满足设定规则的可能性。(这么理解是方便举一反三,解决思路和这种理解关系不大)。按照动态规划的思想,可以设定三个数组 P、 A、 L ,分别存储以 P、A、L结尾的可能性,具体点:P[i] 就是当抽取序列长度为 i 时,以 P 结尾的序列的种数,A,L同理。因为这样做可以得到P、A、L的递推公式,就达到了动态规划的目的。
第一个P,因为P接在任何字符后边都符合规则,可以给奖励,所以就是 i -1 长度三种字符结尾的和
第二个L,首先,L接在A或者P后都可以,所以前两项相加,接在L后边时,需要保证这个L前边不是 L,也就是 i - 2 长度的序列不能以 L 结尾,也就是 A 和 P 结尾,所以再加上后两项。
第三个A 比较复杂,因为只能有一个A,所以 i 长度序列要以A结尾,需要保证 i - 1 长度的序列中没有 A
先引入 naP 和 naL数组分别表示没有A,并且分别以P、L结尾的序列的可能次数。
具体来说,naP[i] 表示 i 长度序列以 P 结尾,并且不包含字符 A,naL同理。
所以: A[i] = naP[i - 1] + naL[i - 1]
接着来看 naP[i] 的递推 ,naP[i] = naP[i - 1] + naL[i -1] 。(因为是不含A的可能性和,所以只有 naP[i - 1] + naL[i - 1])
可以看到这个递推和 A[i] 的递推一毛一样,因为可能次数一样,可以等价 naP[ i -1] = A[i - 1]
所以 A[i] = naP[i - 1] + naL[i - 1] 可以写成 A[i] = A[i - 1] + naL[i - 1]
现在只剩下 naL[i] 的递推,naL[i] = naP[i - 1] + naP[ i - 2]
(因为不含A,所以没有A[i - 1],又因为L不能连续,与上边 L[i] 的递推同理,所以后半部分只剩 naP[i - 2] 这一项)
继续:那么 naL[i - 1] = naP[i - 2] + naP[i - 3],naP[i - 2] 和 naP[i - 3] 等价 A[i - 2] 和 A[i - 3]
最后: A[i] = A[i-1] + A[i-2] + A[i-3]
采用三个数组的递推公式,得到所求长度的所有合法可能性,naP 和 naL只是中间用来推导,程序中不涉及。
因为数组从 0 开始,所以 A[0] 对应 序列长度为 1 的可能性,最后结果就是序列长度为 n 的三种字符结尾的所有可能性之和,也就是 A[n - 1] + P [n - 1] + L[n -1]
由于数据可能比较大,所以对 10^9 + 7 求模,中间值也比较大,所以在求每个数组值时就求模
c++ 语言实现,思路理解后,语言无所谓。
#include
#include
using namespace std;
// 学生出勤记录II,题目是dp的问题,在 3 个字符里有放回的抽 n 次,抽出来的序列满足一定规则的方式
// 思路:根据题目的限制规则 3 个字符分别建立三个数组,存储以他们结尾的可能数, P[i] 存储 长度 i 的序列以P结尾的可能数
// 因为这么设计可以构造出 dp 的形式,即递推的形式。
// P[i] = P[i-1] + A[i-1] + L[i-1]
// L[i] = P[i-1] + A[i-1] + A[i-2] + p[i-2]
// A[i] = A[i-1] + A[i-2] + A[i-3]
class Solution {
public:
int checkRecord(int n)
{
// 判断特殊与边界
if (0 == n)
{
return 0;
}
if (1 == n)
{
return 3;
}
if (2 == n)
{
return 8;
}
long M = 1000000007;
vector P(n), A(n), L(n);
// 这里 A[0] 表示 长度为 1
// 不可以推导求的先计算出来
A[0] = 1; A[1] = 2; A[2] = 4;
P[0] = 1;
L[0] = 1;
L[1] = 3;
for (int i = 1; i < n; i++)
{
P[i] = (P[i - 1] % M + A[i - 1] % M + L[i - 1] % M)% M;
if (i > 1)
{
L[i] = (P[i - 1] % M + A[i - 1] % M + P[i - 2] % M + A[i - 2] % M) % M;
}
if (i > 2)
{
A[i] = (A[i - 1] % M + A[i - 2] % M + A[i - 3] % M) % M;
}
}
return (A[n - 1] % M + P[n - 1] % M + L[n - 1] % M) % M;
}
};
上边这种解法属于O(n),时间复杂度其实挺高,但是思路比较好理解。还有几种更简单的方法附在参考链接。
还有一种解法非常简单,是leetcode上战胜100%的,20ms,代码如下。(还没理解,等理解了加思路)
class Solution {
public:
int checkRecord(int n) {
static const long long M = 1000000007;
long long a0l0 = 1, a0l1 = 0, a0l2 = 0, a1l0 = 0, a1l1 = 0, a1l2 = 0;
for (int i = 0; i <= n; ++i) {
auto new_a0l0 = (a0l0 + a0l1 + a0l2) % M;
a0l2 = a0l1;
a0l1 = a0l0;
a0l0 = new_a0l0;
auto new_a1l0 = (a0l0 + a1l0 + a1l1 + a1l2) % M;
a1l2 = a1l1;
a1l1 = a1l0;
a1l0 = new_a1l0;
}
return static_cast(a1l0);
}
};
o(logn):https://leetcode.com/problems/student-attendance-record-ii/discuss/101633/improving-the-runtime-from-on-to-olog-n
O (n):https://leetcode.com/problems/student-attendance-record-ii/discuss/101638/simple-java-on-solution
有问题望指出