LeetCode788:旋转数字

138 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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;
    }