1. 题目来源
2. 题目解析
一道正常的线性dp 问题吧,状态定义和转移都不太难。分类讨论三种情况就行了。看到 y 总写了一个根据当前状态向前进行状态转移的方法,感觉还是值得记录一下的。避免某些情况下比较难写。可以对比下常规的状态转移方式和本次的状态转移方式,体会一下区别。
思路:
- 状态定义:f[i][j][k] 定义为长度为 i 的情况下,总共有 j 个 A,且末尾有连续个 L 的有效出勤方案数构成。
- 状态转移:f[i][j][k] 向前转移的方式,看看末尾添加这三种方式时,下一个状态可以变化到什么,然后再将状态转移过去即可。
- 填A:当 j 没有填过时,可以进行状态转移。状态将变为 f[i+1][j+1][0]。因为此时末尾已经变为 A,而非 L。
- 填L:当 k 没有连续填过两次时,可以进行状态转移。状态将变为 f[i+1][j][k+1]。因为此时末尾已经变为L,它不影响 A 的数量。
- 填P:这个没有什么限制,状态直接转移即可。状态将变为 f[i+1][j][0]
这个状态转移就是从当前位置推下一个位置是哪些,而非寻常的从当前位置,看看前一个位置是哪些。比较巧妙。
包括这个代码实现也是,细节点在于 i<n 的,因为在 i=n-1 时,会递推出来 i+1=n 的这一项状态值,所以也能算出结果。
另外也贴一下普通的状态转移代码,体会一下两者不同之处。
- 常规的转移方式,需要枚举每一个位置的前一位可以填什么,实际上大差不差。
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( 1 ) O(1) O(1)
class Solution {
public:
int checkRecord(int n) {
const int MOD = 1e9+7, N = 1e5+5;
int f[N][2][3];
memset(f, 0, sizeof(f));
f[0][0][0] = 1;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < 2; j ++ )
for (int k = 0; k < 3; k ++ ) {
if (j < 1) f[i + 1][j + 1][0] = (f[i + 1][j + 1][0] + f[i][j][k]) % MOD; // 填A, 总数中不能出现两次 A
if (k + 1 <= 2) f[i + 1][j][k + 1] = (f[i + 1][j][k + 1] + f[i][j][k]) % MOD; // 填L, 连续的不能出现三次 L
f[i + 1][j][0] = (f[i + 1][j][0] + f[i][j][k]) % MOD; // 填P,这个肯定能转移,最后一位连续L 置为0 即可
}
int res = 0;
for (int j = 0; j < 2; j ++ )
for (int k = 0; k < 3; k ++ )
res = (res + f[n][j][k]) % MOD;
return res;
}
};
常规转移:
class Solution {
public:
int checkRecord(int n) {
const int MOD = 1e9+7, N = 1e5+5;
int f[N][2][3];
memset(f, 0, sizeof(f));
f[0][0][0] = 1;
for (int i = 1; i <= n; i ++ ) {
// 末尾填 p
for (int j = 0; j < 2; j ++ ) {
for (int k = 0; k < 3; k ++ ) {
f[i][j][0] = (f[i][j][0] + f[i - 1][j][k]) % MOD;
}
}
// 末尾填 A
for (int k = 0; k < 3; k ++ ) {
f[i][1][0] = (f[i][1][0] + f[i - 1][0][k]) % MOD;
}
// 末尾填 L
for (int j = 0; j < 2; j ++ ) {
for (int k = 1; k < 3; k ++ ) {
f[i][j][k] = (f[i][j][k] + f[i - 1][j][k - 1]) % MOD;
}
}
}
int res = 0;
for (int j = 0; j < 2; j ++ )
for (int k = 0; k < 3; k ++ )
res = (res + f[n][j][k]) % MOD;
return res;
}
};