持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情
题目描述
2022/9/25 每日一题
0, 1, 和 8 被旋转后仍然是它们自己;2 和 5 可以互相旋转成对方;6 和 9 同理,除了这些以外其他的数字旋转以后都不再是有效的数字。如果一个数的每位数字被旋转以后仍然还是一个数字,且和其本身不同,则这个数是有效的。
输入:给定一个正整数N
输出:计算从 1 到 N 中有多少个数 X 是有效数字。
算法思路
1、首先对有效数字所需的属性进行分析,若一个数是有效数字,则:
必须具有:2,5,6,9
可有可无:0,1,8
禁止出现:3,4,7
2、暴力法:枚举1-n的每一个数字,若为好数则返回值加一,最后返回最终返回值,但是这个方法不是很聪明。
3、数位动态规划:本文需要重点介绍的算法。
数位动态规划
动态规划是一种以空间复杂度来换取时间复杂度的有效方法,一般使用一个多维数组来存储计算过程中产生的中间状态,数组的每个维度代表事件发生状态的一个维度。
数位动态规划算法的思想是分别规划整个数字中的每个数字位,核心思想是“填数”,例如给定数字“xyz”这个三位数,那么百位、十位、个位可能出现的数字为0-9。我们尝试在每个数位上填0-9,同时要考虑题目所给约束条件来作为动态规划算法的边界条件。
以本题为例,给定一个数字,需要判断是否为有效数字的数字,其每位上出现的数字必须分别<=这个数字上的每位数,例如给定“123”,需要判断的数组的百位只能为“1”,十位只能为“1-2”,个位只能为“1-3”。这就是这个数字的边界条件之一。
第二个边界条件为题目本身的约束,即是否出现必须数字2,5,6,9,以及是否出现禁止出现的3,4,7 ,而对于0,1,8可有可无。
数位动态规划的模板为定义一个函数,其格式为dfs(pos, bound, diff),其中
pos为当前填数的位置
bound为边界条件,用于判断是否需要继续遍历
diff为题目本身所给约束,例如本题中对有效数字的定义
本题应用
现在介绍数位动态规划在本题中的具体应用。
本题定义的动态规划函数为int dfs(int pos, int bound, int diff),其中:
pos:当前填数的位置
bound:前pos-1位是否"贴着"输入的正整数,所谓贴着是指某数位是否与输入正整数相等,如果相等则当前位置的填数不能再增大了,否则会超过给定的判断区间
diff:前pos-1位是否出现2,5,6,9,即本题的约束
整个dfs的返回值的含义:从第pos位开始填数,bound,diff为相应状态时,后续填数出现有效数字的个数。bound状态为出边界和不出边界两种,diff为出现必需数字和不出现必须数字两种情况。
memo[pos][bound][diff]:因为在整个填数遍历的过程中会出现重复计算的情况,所以使用一个三维数组记录中间状态以减少重复计算。
代码实现
int[] check = {0, 0, 1, -1, -1, 1, 1, -1, 0, 1};
List<Integer> digits = new ArrayList<>();
//第一维长度不能设为数字位数,因为memo只能为全局变量,而数字位数是在主函数里才能确定的,若设为位数则会被赋值为0
int[][][] memo = new int[5][2][2];
public int rotatedDigits(int n) {
while(n != 0){
digits.add(n % 10);
n /= 10;
}
//反转List
Collections.reverse(digits);
//初始化所有元素为-1
for(int i = 0; i < 5; i++){
for(int j = 0; j < 2; j++){
//Arrays.fill()只能填充一维数组,内部也是执行for循环
Arrays.fill(memo[i][j], -1);
}
}
//从第0位开始考虑,初始时第一位填数受限(不能超出n的第一位),未出现好数
return dfs(0, 1, 0);
}
public int dfs(int pos, int bound, int diff){
//出边界情况:填数位置处于数字外,此时只需判断已填数字是否有必须
if(pos == digits.size()){
return diff;
}
if(memo[pos][bound][diff] != -1){
return memo[pos][bound][diff];
}
int ret = 0;
for(int i = 0; i <= (bound == 1 ? digits.get(pos) : 9); i++){
//若当前位未出现禁止数,则填入当前数,并对下一位进行填数判断
if(check[i] != -1){
ret += dfs(
pos + 1,
bound == 1 && i == digits.get(pos) ? 1 : 0,
diff == 1 || check[i] == 1 ? 1 : 0
);
}
//若当前位出现禁止数,则截枝,不对下一位进行填数,而是更换当前位填数
}
return memo[pos][bound][diff] = ret;
}