这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战
leetcode-552-学生出勤记录II
[博客链接]
[题目描述]
给你一个字符串 s 表示一个学生的出勤记录,其中的每个字符用来标记当天的出勤情况(缺勤、迟到、到场)。记录中只含下面三种字符:
- 'A':Absent,缺勤
- 'L':Late,迟到
- 'P':Present,到场
如果学生能够 同时 满足下面两个条件,则可以获得出勤奖励:
按 总出勤 计,学生缺勤('A')严格 少于两天。 学生 不会 存在 连续 3 天或 3 天以上的迟到('L')记录。
如果学生可以获得出勤奖励,返回获得奖励的方案数量 。
示例 1:
输入:n = 2
输出:8
解释:
有 8 种长度为 2 的记录将被视为可奖励:
"PP" , "AP", "PA", "LP", "PL", "AL", "LA", "LL"
只有"AA"不会被视为可奖励,因为缺勤次数为 2 次(需要少于 2 次)。
示例 2:
输入:n = 1
输出:3
示例 3:
输入:n = 10101
输出:183236316
提示:
- 1 <= n <= 10000
[题目链接]
[github地址]
[思路介绍]
思路一:动态规划
- 常规dp的动态规划分析已经在上一篇文章给了出来,大家可以去看一下,这里就不赘述了
- 时间复杂度O(n)
- 空间复杂度O(n)
思路二:dfs+记忆化
- 这道题最开始想到的肯定是dfs,每一次选择三选一
- 然后判断符合条件的话加入结果集
- 去重返回方案数量
- 但是这样面临的也必然是时间复杂度爆炸
- 时间复杂度O
int res = 0, len = 0;
Set<String> set = new HashSet<>();
public int checkRecord(int n) {
len = n;
dfs(0, "", "A");
dfs(0, "", "P");
dfs(0, "", "L");
return res;
}
public void dfs(int idx, String now, String add) {
if (idx == len && check(now) ) {
if (!set.contains(now)) {
res += 1;
set.add(now);
}
return;
}
now = now + add;
if (!check(now)) {
return;
}
dfs(idx + 1, now, "A");
dfs(idx + 1, now, "P");
dfs(idx + 1, now, "L");
}
public boolean check(String s) {
return !(s.indexOf("A") != s.lastIndexOf("A") || s.contains("LLL"));
}
- 时间复杂度O()
- 空间复杂度O(n)
思路三:dfs+记忆化
- 根据思路二可以发现某些状态可以不进行重复计算
- 对上述dfs做记忆化改造
- 其实刷题刷到这里,大家可以发现记忆化dfs和动态规划就非常相似了
int MOD = (int) (1e9 + 7);
int len = 0;
int[][][] cache;
public int checkRecord(int n) {
cache = new int[n][2][3];
len = n;
return dfs(0, 0, 0);
}
public int dfs(int idx, int cntA, int cntL) {
if (idx >= len) {
return 1;
}
// 相同的状态计算过了直接返回
if (cache[idx][cntA][cntL] != 0) {
return cache[idx][cntA][cntL];
}
// 回溯开始
int ans = 0;
ans = (ans + dfs(idx + 1, cntA, 0)) % MOD;
if (cntA < 1) {
ans = (ans + dfs(idx + 1, 1, 0)) % MOD;
}
if (cntL < 2) {
ans = (ans + dfs(idx + 1, cntA, cntL + 1)) % MOD;
}
cache[idx][cntA][cntL] = ans;
return ans;
}
- 时间复杂度O(n)
- 空间复杂度O(n)
思路四:状态压缩
- 前一天的文章已经写了动态规划了
- 这次也可以通过状态压缩的方式对dp方程进行降维处理
- 因为发现dp[i]只和dp[i-1]有关
- 当然也可以通过滚动数组的方案去写
- 我这里就不再写了
public int checkRecord(int n) {
long[][] dp = new long[2][3];
dp[0][0] = 1;
dp[1][0] = 1;
dp[0][1] = 1;
int mod = (int) 1e9 + 7;
for (int i = 1; i < n; i++) {
long[][] temp = new long[2][3];
temp[0][0] = (dp[0][0] + dp[0][1] + dp[0][2]) % mod;
temp[1][0] = (dp[1][0] + dp[1][1] + dp[1][2]) % mod;
temp[0][1] = dp[0][0];
temp[0][2] = dp[0][1];
temp[1][1] = dp[1][0];
temp[1][2] = dp[1][1];
temp[1][0] += (dp[0][0] + dp[0][1] + dp[0][2]) % mod;
dp = temp;
}
return (int) ((dp[0][0] + dp[0][1] + dp[0][2] + dp[1][0] + dp[1][1] + dp[1][2]) % mod);
}
- 时间复杂度O(n)
- 空间复杂度O(1)
思路五:矩阵快速幂
- 不得不说数学是真的有意思
- 三叶大佬yyds
- 矩阵快速幂其实就将上述运算转换成矩阵运算
- idx = 0 cntA = 0 cntL = 0
- idx = 1 cntA = 0 cntL = 2
- idx = 2 cntA = 0 cntL = 1
- idx = 3 cntA = 1 cntL = 0
- idx = 4 cntA = 1 cntL = 1
- idx = 5 cntA = 1 cntL = 2
- 这种也是一种常见的降维思路,将二维坐标转换成一维坐标
- idx = cntA * 3 + cntL
int N = 6;
int mod = (int)1e9+7;
//矩阵乘法
long[][] mul(long[][] a, long[][] b) {
int r = a.length, c = b[0].length, z = b.length;
long[][] ans = new long[r][c];
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
for (int k = 0; k < z; k++) {
ans[i][j] += a[i][k] * b[k][j];
ans[i][j] %= mod;
}
}
}
return ans;
}
public int checkRecord(int n) {
long[][] ans = new long[][]{
{1}, {0}, {0}, {0}, {0}, {0}
};
long[][] mat = new long[][]{
{1, 1, 1, 0, 0, 0},
{1, 0, 0, 0, 0, 0},
{0, 1, 0, 0, 0, 0},
{1, 1, 1, 1, 1, 1},
{0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 1, 0}
};
while (n != 0) {
if ((n & 1) != 0) ans = mul(mat, ans);
mat = mul(mat, mat);
n >>= 1;
}
int res = 0;
for (int i = 0; i < N; i++) {
res += ans[i][0];
res %= mod;
}
return res;
}
- 时间复杂度O(logn)
- 空间复杂度O(1)