算法题:动态规划之学生出勤记录II

题目

给定一个正整数 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[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]

第一个P,因为P接在任何字符后边都符合规则,可以给奖励,所以就是 i -1 长度三种字符结尾的和

第二个L,首先,L接在A或者P后都可以,所以前两项相加,接在L后边时,需要保证这个L前边不是 L,也就是 i - 2 长度的序列不能以 L 结尾,也就是 A 和 P 结尾,所以再加上后两项。

第三个A 比较复杂,因为只能有一个A,所以 i 长度序列要以A结尾,需要保证 i - 1 长度的序列中没有 A

推导A:

先引入 naPnaL数组分别表示没有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

有问题望指出

你可能感兴趣的:(AL练习)