1012. 至少有 1 位重复的数字

难度困难

给定正整数 n ,返回在 [1, n] 范围内具有 至少 1 位 重复数字的正整数的个数。

示例 1:

输入:n = 20

输出:1

解释:具有至少 1 位重复数字的正数(<= 20)只有 11 。

示例 2:

输入:n = 100

输出:10

解释:具有至少 1 位重复数字的正数(<= 100)有 11,22,33,44,55,66,77,88,99 和 100 。

示例 3:

输入:n = 1000

输出:262

提示:

  • 1 <= n <= 10^9

# 数位 dp

我们可以转化为求解无重复数字的个数。最后答案等于 nn 减去无重复数字的个数

定义 dfs(i,mask,limit,isNum)dfs(i,mask,limit,isNum) 为第 ii 位及以后的无重复数字的个数

  • ii:当前位

  • maskmask:前面选过的数字集合,0 ~ 9, 一共 10 位

  • limitlimit:是否收到了 nn 的约束

    例如:n=126n = 126,若 i=0i = 0 填入了 11,则 i=1i = 1 最多只能填入 22;若 i=1i=1 填入了 11,则 i=2i = 2 最多可以填入 99;若 i=1i = 1 填入了 22,则 i=2i = 2 最多可以填入 66

  • isNumisNumii 前面的数位是否填写了数字

    例如: 010 ,不能有前导零,若 ii 前面没有任何数字,当前 ii 位要么跳过,要么至少从 1 开始填。

    n=123n=123,在 i=0i = 0 跳过,相当于构造 99 以内的数字,若 i=1i = 1 不跳过,相当于构造 10 ~ 99 以内的数字,若 i=1i = 1 跳过,相当于构造 9 以内的数字

此处可以只记忆化 iimaskmask,因为 limitlimitisNumisNum 只有一种情况

  • 例如:3 2 8

    ​ 3 2 i,当前位 ii 只能由 3, 2 由来,只有一种情况。

不能只记忆化 ii,因为对于 maskmask 可能由相同数字集合不同顺序而来

  • 例如:123 与 321
class Solution {
    public int numDupDigitsAtMostN(int n) {
        nums = String.valueOf(n).toCharArray();
        memo = new int[nums.length][1 << 10];
        for (int i = 0; i < nums.length; i++) {
            Arrays.fill(memo[i], -1);
        }
        return n - dfs(0, 0, true, false);
    }
    char[] nums;
    int[][] memo;
    /**
     * @param i     当前位
     * @param mask  前面选过的数字集合,0 ~ 9, 一共 10 位
     * @param limit 是否收到了 n 的约束
     * @param isNum i 前面的数位是否填了数字
     *              例如 : 010, 不能有前导 0, 若 i 前面没有任何数字,当前 i 位要么跳过,要么至少从 1 开始填
     * @return 统计不重复的数字个数
     */
    public int dfs(int i, int mask, boolean limit, boolean isNum) {
        if (i == nums.length) {
            return isNum ? 1 : 0;
        }
        // 受到限制或者前面没有数字,只有一种情况,没有必要记忆化
        // 例如 : 3 2 8
        //        3 2 i, 当前位 i 只能由 3, 2 由来只有一种情况
        // if (memo[i][mask] != -1) {
        //      这样是不行的
        //      3 2 8 
        //      2 1 i | 1 2 i
        if (!limit && isNum && memo[i][mask] != -1) {
            return memo[i][mask];
        }
        // 当前可以选择的最大值
        // 例如 : 2 9 8
        //           i , i - 1 选择了 9, 当前最高只能选择 8
        int up = limit ? nums[i] - '0' : 9;
        int ans = 0;
        // 只有前面没有任何数字,当前位数可以跳过,可以不跳过
        // 前面有数字,当前位不能跳过,若跳过当前位,会导致重复
        //  例如 : 2 3 3  (t 表示跳过)
        //        t 2 1
        //        2 t 1 | 2 1 t
        // 只有前面没有数字,才可以跳过,且保证不重复
        if (!isNum) {
            // 跳过当前数字可以随便选,没有限制
            ans = dfs(i + 1, mask, false, isNum);
        }
        // 防止前导零
        //  例如 : 010, 不能有前导 0, 若 i 前面没有任何数字,当前 i 位要么跳过,要么至少从 1 开始填
        int d = isNum ? 0 : 1;
        for (; d <= up; d++) {
            // 没有被使用过
            if ((mask >> d & 1) == 0) {
                ans += dfs(i + 1, mask | (1 << d), limit && d == up, true);
            }
        }
        if (!limit && isNum) {
            memo[i][mask] = ans;
        }
        return ans;
    }
}

# 复杂度分析

  • 时间复杂度:O(Dlog(n)2D)O(Dlog(n)2^D),其中 D=10D =10,由于每个状态只会计算一次,因为动态规划的时间复杂度 = 状态个数 ×\times 单个状态计算的时间。本题状态个数为 O(log(n)2D)O(log(n)2^D),单个状态的计算时间为 O(D)O(D)
  • 空间复杂度:O(log(n)2D)O(log(n)2^D)