|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[902. 最大为 N 的数字组合](https://leetcode-cn.com/problems/numbers-at-most-n-given-digit-set/solution/by-ac_oier-8k27/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「动态规划」、「二分」、「数位 DP」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给定一个按 非递减顺序 排列的数字数组 `digits`。你可以用任意次数 $digits[i]$ 来写的数字。例如,如果 $digits = [1,3,5]$,我们可以写数字,如 `'13'`, `'551'`, 和 `'1351315'`。 |
| 10 | + |
| 11 | +返回 **可以生成的小于或等于给定整数** $n$ 的正整数的个数 。 |
| 12 | + |
| 13 | +示例 1: |
| 14 | +``` |
| 15 | +输入:digits = ["1","3","5","7"], n = 100 |
| 16 | +
|
| 17 | +输出:20 |
| 18 | +
|
| 19 | +解释: |
| 20 | +可写出的 20 个数字是: |
| 21 | +1, 3, 5, 7, 11, 13, 15, 17, 31, 33, 35, 37, 51, 53, 55, 57, 71, 73, 75, 77. |
| 22 | +``` |
| 23 | +示例 2: |
| 24 | +``` |
| 25 | +输入:digits = ["1","4","9"], n = 1000000000 |
| 26 | +
|
| 27 | +输出:29523 |
| 28 | +
|
| 29 | +解释: |
| 30 | +我们可以写 3 个一位数字,9 个两位数字,27 个三位数字, |
| 31 | +81 个四位数字,243 个五位数字,729 个六位数字, |
| 32 | +2187 个七位数字,6561 个八位数字和 19683 个九位数字。 |
| 33 | +总共,可以使用D中的数字写出 29523 个整数。 |
| 34 | +``` |
| 35 | +示例 3: |
| 36 | +``` |
| 37 | +输入:digits = ["7"], n = 8 |
| 38 | +
|
| 39 | +输出:1 |
| 40 | +``` |
| 41 | + |
| 42 | +提示: |
| 43 | +* $1 <= digits.length <= 9$ |
| 44 | +* $digits[i].length == 1$ |
| 45 | +* $digits[i]$ 是从 `'1'` 到 `'9'` 的数 |
| 46 | +* `digits` 中的所有值都 不同 |
| 47 | +* `digits` 按 非递减顺序 排列 |
| 48 | +* $1 <= n <= 10^9$ |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +### 数位 DP + 二分 |
| 53 | + |
| 54 | +这是一道「数位 DP」的经典运用题。 |
| 55 | + |
| 56 | +由于题目给定的 `digits` 不包含 $0$,因此相当于只需要回答使用 `digits` 的数值能够覆盖 $[1, x]$ 范围内的多少个数字。 |
| 57 | + |
| 58 | +起始先将字符串数组 `digits` 转为数字数组 `nums`,假定 `nums` 的长度为 $m$,然后考虑如何求得 $[1, x]$ 范围内合法数字的个数。 |
| 59 | + |
| 60 | +假定我们存在函数 `int dp(int x)` 函数,能够返回区间 $[1, x]$ 内合法数的个数,那么配合「容斥原理」我们便能够回答任意区间合法数的查询: |
| 61 | +$$ |
| 62 | +ans_{(l, r)} = dp(r) - dp(l - 1) |
| 63 | +$$ |
| 64 | +对于本题,查询区间的左端点固定为 $1$,同时 $dp(0) = 0$,因此答案为 $dp(x)$。 |
| 65 | + |
| 66 | +然后考虑如何实现 `int dp(int x)` 函数,我们将组成 $[1, x]$ 的合法数分成三类: |
| 67 | +* 位数和 $x$ 相同,且最高位比 $x$ 最高位要小的,这部分统计为 `res1`; |
| 68 | +* 位数和 $x$ 相同,且最高位与 $x$ 最高位相同的,这部分统计为 `res2`; |
| 69 | +* 位数比 $x$ 少,这部分统计为 `res3`。 |
| 70 | + |
| 71 | +其中 `res1` 和 `res3` 求解相对简单,重点落在如何求解 `res2` 上。 |
| 72 | + |
| 73 | +**对 $x$ 进行「从高到低」的处理(假定 $x$ 数位为 $n$),对于第 $k$ 位而言($k$ 不为最高位),假设在 $x$ 中第 $k$ 位为 $cur$,那么为了满足「大小限制」关系,我们只能在 $[1, cur - 1]$ 范围内取数,同时为了满足「数字只能取自 `nums`」的限制,因此我们可以利用 `nums` 本身有序,对其进行二分,找到满足 `nums[mid] <= cur` 的最大下标 $r$,根据 $nums[r]$ 与 $cur$ 的关系进行分情况讨论:** |
| 74 | + |
| 75 | +* $nums[r] = cur$: 此时位置 $k$ 共有 $r$ 种选择,而后面的每个位置由于 $nums[i]$ 可以使用多次,每个位置都有 $m$ 种选择,共有 $n - p$ 个位置,因此该分支往后共有 $r * m^{n - p}$ 种合法方案。且由于 $nums[r] = cur$,往后还有分支可决策(需要统计),因此需要继续处理; |
| 76 | +* $nums[r] < cur$:此时算上 $nums[r]$,位置 $k$ 共有 $r + 1$ 种选择,而后面的每个位置由于 $nums[i]$ 可以使用多次,每个位置都有 $m$ 种选择,共有 $n - p$ 个位置,因此该分支共有 $(r + 1) * m^{n - p}$ 种合法方案,由于 $nums[r] < cur$,往后的方案数(均满足小于关系)已经在这次被统计完成,累加后进行 `break`; |
| 77 | +* $nums[r] > cur$:该分支往后不再满足「大小限制」要求,合法方案数为 $0$,直接 `break`。 |
| 78 | + |
| 79 | +其他细节:实际上,我们可以将 `res1` 和 `res2` 两种情况进行合并处理。 |
| 80 | + |
| 81 | +代码: |
| 82 | + |
| 83 | +```Java |
| 84 | +class Solution { |
| 85 | + int[] nums; |
| 86 | + int dp(int x) { |
| 87 | + List<Integer> list = new ArrayList<>(); |
| 88 | + while (x != 0) { |
| 89 | + list.add(x % 10); |
| 90 | + x /= 10; |
| 91 | + } |
| 92 | + int n = list.size(), m = nums.length, ans = 0; |
| 93 | + // 位数和 x 相同 |
| 94 | + for (int i = n - 1, p = 1; i >= 0; i--, p++) { |
| 95 | + int cur = list.get(i); |
| 96 | + int l = 0, r = m - 1; |
| 97 | + while (l < r) { |
| 98 | + int mid = l + r + 1 >> 1; |
| 99 | + if (nums[mid] <= cur) l = mid; |
| 100 | + else r = mid - 1; |
| 101 | + } |
| 102 | + if (nums[r] > cur) { |
| 103 | + break; |
| 104 | + } else if (nums[r] == cur) { |
| 105 | + ans += r * (int) Math.pow(m, (n - p)); |
| 106 | + if (i == 0) ans++; |
| 107 | + } else if (nums[r] < cur) { |
| 108 | + ans += (r + 1) * (int) Math.pow(m, (n - p)); |
| 109 | + break; |
| 110 | + } |
| 111 | + } |
| 112 | + // 位数比 x 少的 |
| 113 | + for (int i = 1, last = 1; i < n; i++) { |
| 114 | + int cur = last * m; |
| 115 | + ans += cur; last = cur; |
| 116 | + } |
| 117 | + return ans; |
| 118 | + } |
| 119 | + public int atMostNGivenDigitSet(String[] digits, int max) { |
| 120 | + int n = digits.length; |
| 121 | + nums = new int[n]; |
| 122 | + for (int i = 0; i < n; i++) nums[i] = Integer.parseInt(digits[i]); |
| 123 | + return dp(max); |
| 124 | + } |
| 125 | +} |
| 126 | +``` |
| 127 | +* 时间复杂度:由于 `digits` 最多存在 $9$ 个元素,因此二分的复杂度可以忽略,整体复杂度为 $O(\log{n})$ |
| 128 | +* 空间复杂度:$O(C)$ |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +### 最后 |
| 133 | + |
| 134 | +这是我们「刷穿 LeetCode」系列文章的第 `No.902` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 135 | + |
| 136 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 137 | + |
| 138 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 139 | + |
| 140 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 141 | + |
0 commit comments