902. 最大为 N 的数字组合「数学」

思路

由题意可知这是一道组合数字的题目,可以利用数学方法来实现

数学

对于数字n,计它的位数为k

digits的长度为len,表示该数组中有多少个数字可供使用,且digits中的数字时单调递增的。

数字不重复且非递减排序,就是递增排序的。

首先我们知道:

  1. 对于数字n,任意一个位数小于k的数显然都是比n小的。
  2. 当两数字位数相同时,假设有: n = a 0 a 1 . . . a k − 1 n = {a_0}{a_1}...{a_{k-1}} n=a0a1...ak1 n u m = b 0 b 1 . . . b k − 1 num = {b_0}{b_1}...{b_{k-1}} num=b0b1...bk1,又可以分为以下几种情况,从第一位进行分析:
    1. b 0 < a 0 b_0 < a_0 b0<a0,则 b 1 , b 2 , . . . , b k − 1 b_1,b_2,...,b_{k-1} b1,b2,...,bk1可以使用任意数字,都不会影响num < n的结果;
    2. b 0 = a 0 b_0 = a_0 b0=a0,那么需要递归的去继续分析 a 1 . . . a k − 1 {a_1}...{a_{k-1}} a1...ak1 b 1 . . . b k − 1 {b_1}...{b_{k-1}} b1...bk1
    3. b 0 > a 0 b_0 > a_0 b0>a0,那么显然是不存在num > n的数字组合的。

因此可以设计算法:

  1. 首先初始化变量:将digits由字符串数组转换为整形数组、将数字n转换为整形数组,数组中每一个元素是一个一位数
  2. 计算位数小于k的数字组合数量,有: l e n + l e n 2 + . . . + l e n ( k − 1 ) len + len^2 + ... + len^{(k-1)} len+len2+...+len(k1)
  3. 计算位数等于k的数字组合数量, n = a 0 a 1 . . . a k − 1 n = {a_0}{a_1}...{a_{k-1}} n=a0a1...ak1 n u m = b 0 b 1 . . . b k − 1 num = {b_0}{b_1}...{b_{k-1}} num=b0b1...bk1,按下标从低到高进行两种分析,固定下标idx
    1. 首先计算得到digits中第一个大于 a i d x a_{idx} aidx的数下标i,同时i也表示digits数组中元素小于 a i d x a_{idx} aidx的数量,并且此时 b i d x + 1 , b i d x + 2 , . . . , b k − 1 {b_{idx+1}},{b_{idx+2}},...,{b_{k-1}} bidx+1,bidx+2,...,bk1都可以任意选择,此时共有: i ∗ l e n ( k − 1 − i d x ) i*len^{(k-1-idx)} ilen(k1idx)种组合;
    2. d i g i t s [ i ] = = a i d x digits[i] == a_{idx} digits[i]==aidx,则还需要递归的继续分析 a i d x + 1 . . . a k − 1 {a_{idx+1}}...{a_{k-1}} aidx+1...ak1 b i d x + 1 . . . b k − 1 {b_{idx+1}}...{b_{k-1}} bidx+1...bk1,从而得到该情况下的答案;否则可令此时的答案为0。
    3. 将上述两种情况累加后返回。

从上述算法设计中,我们发现:

  1. digits数组是单调递增的,并且需要对该数组进行查询,所以可以利用二分查找加速;
  2. 有多次需要计算len次幂的地方存在,可以打表用于后续查询。

实现代码如下:

class Solution {
    private int[] table;
    
    public int atMostNGivenDigitSet(String[] digits, int n) {
        int ans = 0;
        char[] chars = String.valueOf(n).toCharArray();
        //n是一个k位整数
        int k = chars.length;
        table = new int[k];
        int len = digits.length;

        //将字符串转换为整型,方便进行查找
        List<Integer> digitList = Arrays.stream(digits).map(Integer::parseInt).collect(Collectors.toList());

        //显然任意的 1 到 k-1 位整数都小于n
        //有 len + len^2 + ... + len^(k-1) 个
        int x = len;
        table[0] = 1;
        for (int i = 1; i < k; i++) {
            ans += x;
            table[i] = x;
            x *= len;
        }
        ans += dfs(0, chars, digitList);

        return ans;
    }

    private int dfs(int idx, char[] chars, List<Integer> digitList){
        if (idx == chars.length){
            return 1;
        }
        //分为两种情况,下标idx处小于chars[idx]与等于chars[idx]的两种情况
        int a=0, b=0;
        int c = chars[idx] - '0';
        
        //有i个数小于c
        int i = biSearch(c, digitList);
        a = i * table[chars.length-1-idx];
        
        if (i<digitList.size() && digitList.get(i) == c){
            b = dfs(idx+1, chars, digitList);
        }

        return a+b;
    }
    
    //返回第一个大于等于val的下标
    private int biSearch(int val, List<Integer> list){
        int l = 0, r = list.size() - 1;
        int mid;
        while(l <= r){
            mid = l + (r - l) / 2;
            if (list.get(mid) < val){
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return l;
    }
}

该方法能实现的原因是digits数组中没有0.
若存在0,还需要进行额外的判断,代码会更乱。

动态规划

数位DP?参考官方题解的思路,思路见官方题解

数位DP-OI Wiki

【动态规划の数位 DP】一文详解通用「数位 DP」求解思路

灵佬的题解

class Solution {
    public int atMostNGivenDigitSet(String[] digits, int n) {
        List<Integer> digitList = Arrays.stream(digits).map(Integer::parseInt).collect(Collectors.toList());
        char[] chars = String.valueOf(n).toCharArray();
        //n是一个k位整数
        int k = chars.length;
        int m = digits.length;
        
        
        // dp[i][0]表示由digits构成且小于n的前i位的数字的个数
        // dp[i][1]表示由digits构成且等于n的前i位的数字的个数
        int[][] dp = new int[k+1][2];
        //初始化dp[0][1] = 1
        dp[0][1] = 1;

        for (int i = 1; i <= k; i++) {
            int c = chars[i-1] - '0';  //第i个数字
            for (int j = 0; j < m; j++) {
                //从digits中找满足条件的数
                if(digitList.get(j) == c){
                    dp[i][1] = dp[i-1][1];
                } else if (digitList.get(j) < c) {
                    //前面的数都是相等的,找到有多少个小于的
                    dp[i][0] += dp[i-1][1];  
                } else {
                    break;
                }
            }
            if (i > 1){
                // m * dp[i-1][0] 部分表示 dp[i-1][0]代表的数num * 10 + digits[j] 都是满足条件的
                // + m 表示digits中数字直接作为一个数时显然也是满足条件的
                dp[i][0] += m * dp[i-1][0] + m;
            }
        }
        
        return dp[k][0] + dp[k][1];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值