leetcode:902. 最大为 N 的数字组合

给定一个按 非递减顺序 排列的数字数组 digits 。你可以用任意次数 digits[i] 来写的数字。例如,如果 digits = [‘1’,‘3’,‘5’],我们可以写数字,如 ‘13’, ‘551’, 和 ‘1351315’。

返回 可以生成的小于或等于给定整数 n 的正整数的个数 。

示例 1:

输入:digits = [“1”,“3”,“5”,“7”], n = 100
输出:20

解释:
可写出的 20 个数字是:
1, 3, 5, 7, 11, 13, 15, 17, 31, 33, 35, 37, 51, 53, 55, 57, 71, 73, 75, 77.

示例 2:

输入:digits = [“1”,“4”,“9”], n = 1000000000
输出:29523

解释:
我们可以写 3 个一位数字,9 个两位数字,27 个三位数字,
81 个四位数字,243 个五位数字,729 个六位数字,
2187 个七位数字,6561 个八位数字和 19683 个九位数字。
总共,可以使用D中的数字写出 29523 个整数。
示例 3:

输入:digits = [“7”], n = 8
输出:1

提示:

1 <= digits.length <= 9
digits[i].length == 1
digits[i] 是从 ‘1’ 到 ‘9’ 的数
digits 中的所有值都 不同
digits 按 非递减顺序 排列
1 <= n <= 109

思路

数位 DP 思想:
一个数第 i 位( s[i] )会受到第 i-1 位( s[i-1] )的约束
若 (x代表nums数组中任意一位数)

1> i-1 位数与 nums[x] 相等 则第i位要小于等于第s[i]
举例:
123 => 如果第二位确定是2 则第三位只能是 0,1,2,3

2> i-1 位数小于 nums[x] 则第i位取值范围是0-9
举例
123 => 第二位取到1 则第三位0-9都可

3> i-1 位数大于 nums[x] 直接break

利用dp[ ]数组 进行记忆化dp

解释: 没有任何限制条件下面,第i到n位可以产生多少个数字,因为如果没有任何限制条件,他们的计算代码是一样的,所以可以记下来,避免重复运算。就比如n = 789,如果百位选1,那么十位就都可以选任意的数,个位也可以选任意的数,如果百位选择2,那么十位个位也都可以随便选,这种情况就重复计算了,所以说就把百位在没有任何约束下能够生成的数字记录下来,之后如果百位选择3,4,5,6就可以直接拿来用。

将 n 转换成字符串 s,定义 f(i,isLimit,isNum) 表示构造从左往右第 i 位及其之后数位的合法方案数
其中:

  • isLimit表示当前是否收到n的约束 . 若为真,则第i位填入的数字至多为 s[i] , 否则至多为 9 (前面有解释) . 如果在受到约束的情况下填了 s[i] , 那么后续填入的数字依旧受到 n 的约束
  • isNum 表示前面的数位是否填了数字 . 若为假 , 则表示当前数位可以跳过(不填数字) , 或者填入的数字至少为 1 ; 若为真 , 则必须填入数字( 范围是 0-9 ), 这样我们可以控制构造出来一位/两位/三位数等… , 本题上可以填入的数字需要从 digits 中选择
class Solution {
    private String[] digits;
    private char s[];
    private int dp[];

    public int atMostNGivenDigitSet(String[] digits, int n) {
        this.digits = digits;
        s = Integer.toString(n).toCharArray();
        dp = new int[s.length];
        Arrays.fill(dp, -1); // dp[i] = -1 表示 i 这个状态还没被计算出来
        return f(0, true, false);
    }

    private int f(int i, boolean isLimit, boolean isNum) {
        if (i == s.length) return isNum ? 1 : 0; // 如果填了数字,则为 1 种合法方案
        if (!isLimit && isNum && dp[i] >= 0) return dp[i]; // 在不受到任何约束的情况下,返回记录的结果,避免重复运算
        var res = 0;
        if (!isNum) // 前面不填数字,那么可以跳过当前数位,也不填数字
            // isLimit 改为 false,因为没有填数字,位数都比 n 要短,自然不会受到 n 的约束
            // isNum 仍然为 false,因为没有填任何数字
            res = f(i + 1, false, false);
        var up = isLimit ? s[i] : '9'; // 根据是否受到约束,决定可以填的数字的上限
        // 注意:对于一般的题目而言,如果此时 isNum 为 false,则必须从 1 开始枚举,由于本题 digits 没有 0,所以无需处理这种情况
        for (var d : digits) { // 枚举要填入的数字 d
            if (d.charAt(0) > up) break; // d 超过上限,由于 digits 是有序的,后面的 d 都会超过上限,故退出循环
            // isLimit:如果当前受到 n 的约束,且填的数字等于上限,那么后面仍然会受到 n 的约束
            // isNum 为 true,因为填了数字
            res += f(i + 1, isLimit && d.charAt(0) == up, true);
        }
        if (!isLimit && isNum) dp[i] = res; // 在不受到任何约束的情况下,记录结果
        return res;
    }
}

作者:endlesscheng
链接:https://leetcode.cn/problems/numbers-at-most-n-given-digit-set/solution/shu-wei-dp-tong-yong-mo-ban-xiang-xi-zhu-e5dg/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结

这道题真的是第一遍读题就直接找题解, 因为没有接触过数形dp的概念
总结一下数型dp:
核心就是去合并相同类型的数组,减少遍历次数,也就是本题中dp[]数组的作用!!!
789 => 如果百位是1,可以计算出所有符合条件的个数,因为限制是7,所以 2,3,4,5,6都是相同的计算结果,这时如果我们把它记下来 , **可以大幅度减少循环遍历次数!!!**这就是记忆化数位dp的思想

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值