算法第四题:学生出勤记录情况统计 2021-08-19
一、问题描述
可以用字符串表示一个学生的出勤记录,其中的每个字符用来标记当天的出勤情况(缺勤、迟到、到场)。记录中只含下面三种字符:
‘A’:Absent,缺勤
‘L’:Late,迟到
‘P’:Present,到场
如果学生能够 同时 满足下面两个条件,则可以获得出勤奖励:
按总出勤 计,学生缺勤(‘A’)严格 少于两天。
学生不会存在 连续 3 天或 连续 3 天以上的迟到(‘L’)记录。
给你一个整数 n ,表示出勤记录的长度(次数)。请你返回记录长度为 n 时,可能获得出勤奖励的记录情况 数量 。答案可能很大,所以返回对 10的9次方加7取余的结果。
二、问题分析
自己想用数学分析的方法做,很明显自己推了很多,但是情况还是很复杂,特别是当A和连续的L在一起的时候,问题变得很复杂。
后来又想到用分治,结果是一样得,子问题划分上不好划分,并且原问题不能单纯由子问题构成。
三、记忆化搜索
枚举所有方案得暴力搜索DFS(深度优先搜索)
设计变量
u | 当前还剩下多少位需要决策 ,其实可以看成还有几天的出勤情况没算 |
---|---|
acnt | 当前方案中A的出现总次数 ,acnt小于2的时候,是可能拿到奖金的 |
lant | 当前方案中结尾L的总出现次数,lant连续出现0,1,2,是有可能拿到奖金 |
cur | 当前方案,看成当前出勤天数下的出勤情况 |
ans | 结果集,当前的出勤天数下的能拿到到奖金的情况数 |
最开始的笨方法当然是暴力搜索,不过要采用回溯的思路,每次加一天的出勤情况,有三种可能,分别判断,如果满足能拿奖金的情况,那么情况数就增加;这样一直加直到满足n,u其实是每次都在减少的。
回溯的解决问题的一般步骤是:
1.针对给定问题,定义问题的解空间,它至少包含问题的一个解
在本题中,拿将金就是一个解情况,满足拿奖金的条件设计就是在定义问题的解空间。
2.确定易于搜索的解空间结构,使得回溯法能够方便地搜索整个解空间
然后每次加一天,然后看看能拿到奖金的情况
3.以深度优先的方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索
比如当添加一天后不能拿到奖金了,那么这种情况直接就抛弃了,后面从新从能拿将金的情况里选一个继续进行。
设计函数
1.如果新的一天是P,那么可以直接方进来,并把这种情况方入解空间
2.如果新的一天是A,那么只有之前天里A没出现,即acnt<1时,可以把这种情况方入解空间
3.如果新的一天是L,那么L只能连续出现两个,上一天是P或A都是可以的,即lant<2
dfs(int day,int n,int absent,int late)
其中day表示第几天,n表示一共的天数,absent表示当天的出勤情况,late表示最多的连续迟到天数。
//初始化输入
public int checkRecord(int n){return dfs(0,n,0,0)
}
//函数主题逻辑
private int dfs(int day,int n,int absent,int late){//退出条件if(day>=n){return 1;}//回溯开始int ans=0;//1.新的一天是Pans=ans+dfs(day+1,n,absent,0)//2.新的一天是Aif(absent<1)ans=ans+dfs(day+1,n,1,0)//3.新的一天是Lif(late<2)ans=ans+dfs(day+1,n,absent,late+1)return ans;
}
//最终返回的是n天的情况数
时间复杂度:O(3^n),n个位置,每个位置有3个选择,就是33…*3
空间复杂度:O(n),递归层数为 n。
实际上,我们不需要枚举所有的方案数,因此我们只需要保留函数签名中的前三个参数即可。
同时由于我们计算某个(u,acnt,lant)的方案数时,其依赖的状态可能会被重复使用,考虑加入记忆化,将结果缓存起来。其实就是记住前一天的情况,通过分析把收索次数降下来。
例如:我们要计算 day=2, absent=1, late=0,它有可能从哪些状态而来呢?
absent=1可能是第 0 天填入的;
absent=1可能是第 1 天填入的;
所以,以上两种情况,到第 2 天的时候的状态是完全一样的,也就产生了重复计算,所以,我们可以声明一个缓存,记录每个状态下计算得到的值,下次再遇到相同的状态,直接返回即可。
看代码
public int checkRecord(int n){int[][][]memo=new int[n][2][3];return dfs(0,n,0,0,memo);
}
private int dfs(int day, int n, int absent, int late, int[][][] memo) {if (day >= n) {return 1;}// 相同的状态计算过了直接返回if (memo[day][absent][late] != 0) {return memo[day][absent][late];}// 回溯开始int ans = 0;// 1. Present随便放ans = ans + dfs(day + 1, n, absent, 0, memo);// 2. Absent最多只能放一个if (absent < 1) {ans = ans + dfs(day + 1, n, 1, 0, memo);}// 3. Late最多连续放2个if (late < 2) {ans = ans + dfs(day + 1, n, absent, late + 1, memo);}// 记录每一个状态的计算结果memo[day][absent][late] = ans;return ans;}
时间复杂度:O(n),通过memo可以看到有 n * 2 * 3种状态,每个状态只会计算一遍,所以是 6n,时间复杂度为 O(n)。
空间复杂度:O(n),递归层数为 n,memo数组占用 n * 2 * 3 = 6n的空间,两者空间复杂度都是 O(n)。
四、动态规划
好了,有了记忆化,转 DP 就非常简单了,只要把 memo 改成 dp 即可,我们这样定义动态规划:
状态定义:dp[i][j][k]表示第 i 天、在 A 为 j 次、连续的 L 为 k 次的方案数。
状态转移:所有的状态都是从前一天,即 i-1,转移而来,但是对于 j 和 k,
要分三种情况来讨论:
当前填入的是 P,i-1 天的任何状态都能转移过来;
当前填入的是 A,i-1 天即之前肯定没填过 A,同时所有的 late 状态都可以转移到过来。
当前填入的是 L,i-1 天最多只能有一个连续的 L,其他的状态依次转移过来。
为了方便计算,我们把第 0 天的值初始化。
好了,请看代码,加了详细注释:
class Solution {int MOD = 1000000007;public int checkRecord(int n) {long[][][] dp = new long[n][2][3];// 初始值dp[0][0][0] = 1;dp[0][1][0] = 1;dp[0][0][1] = 1;for (int i = 1; i < n; i++) {// 本次填入P,分成前一天累计了0个A和1个A两种情况dp[i][0][0] = (dp[i - 1][0][0] + dp[i - 1][0][1] + dp[i - 1][0][2]) % MOD;dp[i][1][0] = (dp[i - 1][1][0] + dp[i - 1][1][1] + dp[i - 1][1][2]) % MOD;// 本次填入A,前一天没有累计A都能转移过来// 这行可以与上面一行合并计算,为了方便理解,我们分开,下面会合并dp[i][1][0] = (dp[i][1][0] + dp[i - 1][0][0] + dp[i - 1][0][1] + dp[i - 1][0][2]) % MOD;// 本次填入L,前一天最多只有一个连续的L,分成四种情况dp[i][0][1] = dp[i - 1][0][0];dp[i][0][2] = dp[i - 1][0][1];dp[i][1][1] = dp[i - 1][1][0];dp[i][1][2] = dp[i - 1][1][1];}// 计算结果,即最后一天的所有状态相加long ans = 0;for (int i = 0; i < 2; i++) {for (int j = 0; j < 3; j++) {ans = (ans + dp[n - 1][i][j]) % MOD;}}return (int) ans;}
}
五、矩阵快速幂
六、总结
这个题最开始想到的是用回溯的思想,递归的深度优先遍历求解答案,但是递归的次数过多,然后有很多情况没有加以利用,效率很慢。
然后就用记忆化的方法,保存每次的状态,如果后续能用到这种状态,直接调用,就不再递归了,这样一下就把时间复杂度降低了。
现在看代码还是有点迷糊,还是先写倒这吧。
算法第四题:学生出勤记录情况统计 2021-08-19相关推荐
- Leetcode每日一题-学生出勤记录 II(Student Attendance Record II)
可以用字符串表示一个学生的出勤记录,其中的每个字符用来标记当天的出勤情况(缺勤.迟到.到场).记录中只含下面三种字符: 'A':Absent,缺勤 'L':Late,迟到 'P':Present,到场 ...
- 字符串题目:学生出勤记录 I
文章目录 题目 标题和出处 难度 题目描述 要求 示例 数据范围 解法 思路和算法 代码 复杂度分析 题目 标题和出处 标题:学生出勤记录 I 出处:551. 学生出勤记录 I 难度 2 级 题目描述 ...
- C++Python描述 LeetCode 551. 学生出勤记录 I
C++&Python描述 LeetCode 551. 学生出勤记录 I 大家好,我是亓官劼(qí guān jié ),在公众号.CSDN.GitHub.B站.华为开发者论坛等平台分享一些 ...
- 552. 学生出勤记录 II
552. 学生出勤记录 II 可以用字符串表示一个学生的出勤记录,其中的每个字符用来标记当天的出勤情况(缺勤.迟到.到场).记录中只含下面三种字符: 'A':Absent,缺勤 'L':Late,迟到 ...
- 551. 学生出勤记录
551. 学生出勤记录 I 给你一个字符串 s 表示一个学生的出勤记录,其中的每个字符用来标记当天的出勤情况(缺勤.迟到.到场).记录中只含下面三种字符: 'A':Absent,缺勤 'L':Late ...
- LeetCode 552. 学生出勤记录 II(动态规划)
文章目录 1. 题目 2. 解题 1. 题目 给定一个正整数 n,返回长度为 n 的所有可被视为可奖励的出勤记录的数量. 答案可能非常大,你只需返回结果mod 10^9 + 7的值. 学生出勤记录是只 ...
- 力扣 -- 551. 学生出勤记录 I 、 552. 学生出勤记录 II
目录 551. 学生出勤记录 一 .题目描述 二. 实现思路以及代码 552. 学生出勤记录 II 一 .题目描述 二. 实现思路以及代码 551. 学生出勤记录 一 .题目描述 给你一个字符串 s ...
- LeetCode——552. 学生出勤记录 II(Student Attendance Record II)[困难]——分析及代码(Java)
LeetCode--552. 学生出勤记录 II[Student Attendance Record II][困难]--分析及代码[Java] 一.题目 二.分析及代码 1. 动态规划 (1)思路 ( ...
- leetcode: 552. 学生出勤记录 II
552. 学生出勤记录 II 来源:力扣(LeetCode) 链接: https://leetcode.cn/problems/student-attendance-record-ii/ 可以用字符串 ...
最新文章
- Linux下vi和vim模式相互切换
- tf.argsort
- 智能车的转弯部分_江西智能搬运平板车铁路轨道运输车-厂家直销
- java 开发银行支付、对账时证书相关的操作总结
- photorec_如何在Linux / Ubuntu中使用PhotoRec恢复已删除的文件
- linux redis-连接命令
- 【地震数据处理】GAN网络基础知识
- 傅里叶变换 FFT 频谱泄漏?
- L2-016 愿天下有情人都是失散多年的兄妹 (25 分) fill函数、bfs
- C#:Winform 打字测速程序 Typer
- Landscape Photography: Winter 风景摄影:冬天 Lynda课程中文字幕
- mysqladmin 管理命令详细应用
- 无法使用备份文件 'D:\20160512.bak',因为原先格式化该文件时所用扇区大小为 512,而目前所在设备的扇区大小为 4096...
- KITTI数据集去除camera生成bag包(kitti2bag)
- 论斯芬克司吃人的合理性。
- 【字符串比较函数】strcmp,stricmp,strcmpi区别
- 用友修改了变量不能连接到服务器,用友T3客户端登录提示 运行时错误91:未设置对象变量...
- PLC实现入栈出栈功能
- 浏览器内核WebKit编年史
- nginx实现ip端口转发_nginx 怎么做端口转发
热门文章
- 信号量与生产者消费者问题
- C++STL常用操作之prev、next篇
- android 电话回音消除,智能门铃中可视对讲的回音消除
- 服务器 硬盘 2.5改3.5,教你如何把2.5寸硬盘当3.5寸硬盘使用
- 2019年自考计算机应用基础(实践),2019年自考计算机应用基础试题练习(九)
- 算法设计与分析——动态规划(一)矩阵连乘
- html天气插件iframe,HTML_利用iframe在网页中显示天气附效果截图,css: 复制代码代码如下: *{margi - phpStudy...
- 华为云 承诺重于泰山!
- ffmpeg 设置网络代理_MAC下使用SSH设置代理的办法
- 英语学习的方法(1)