|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[1012. 至少有 1 位重复的数字](https://leetcode-cn.com/problems/numbers-with-repeated-digits/solution/by-ac_oier-2szj/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「动态规划」、「数位 DP」、「容斥原理」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给定正整数 $n,ドル返回在 $[1, n]$ 范围内具有 至少 1 位 重复数字的正整数的个数。 |
| 10 | + |
| 11 | +示例 1: |
| 12 | +``` |
| 13 | +输入:n = 20 |
| 14 | + |
| 15 | +输出:1 |
| 16 | + |
| 17 | +解释:具有至少 1 位重复数字的正数(<= 20)只有 11 。 |
| 18 | +``` |
| 19 | +示例 2: |
| 20 | +``` |
| 21 | +输入:n = 100 |
| 22 | + |
| 23 | +输出:10 |
| 24 | + |
| 25 | +解释:具有至少 1 位重复数字的正数(<= 100)有 11,22,33,44,55,66,77,88,99 和 100 。 |
| 26 | +``` |
| 27 | +示例 3: |
| 28 | +``` |
| 29 | +输入:n = 1000 |
| 30 | + |
| 31 | +输出:262 |
| 32 | +``` |
| 33 | + |
| 34 | +提示: |
| 35 | +* 1ドル <= n <= 10^9$ |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +### 数位 DP |
| 40 | + |
| 41 | +**首先 $[1, n]$ 共有 $n$ 个数,而求解 $[1, n]$ 范围内有多少个「至少有 1ドル$ 位重复数字」的数的个数,等价于「总数 $n$」减去「没有重复数的数的个数」。** |
| 42 | + |
| 43 | +于是问题转换为:**如何求解「没有重复数」的数的个数,同时为了更具一般性,我们将求解 $[1, n]$ 范围内「没有重复数」的数的个数,修改为求解 $[0, n]$ 范围内「没有重复数」的数的个数**。 |
| 44 | + |
| 45 | +>即将问题转换为 [(题解) 357. 统计各位数字都不同的数字个数](https://leetcode-cn.com/problems/count-numbers-with-unique-digits/solution/by-ac_oier-6tfl/) 中的进阶部分。 |
| 46 | + |
| 47 | +假定我们存在函数 `int dp(int x)` 函数,能够返回区间 $[0, x]$ 内合法数的个数,那么配合「容斥原理」我们便能够回答任意区间合法数的查询: |
| 48 | + |
| 49 | +$$ |
| 50 | +ans_{(l, r)} = dp(r) - dp(l - 1) |
| 51 | +$$ |
| 52 | + |
| 53 | +然后考虑如何实现 `int dp(int x)` 函数,我们将组成 $[0, x]$ 的合法数分成三类: |
| 54 | +* 位数和 $x$ 相同,且最高位比 $x$ 最高位要小的,这部分统计为 `res1`; |
| 55 | +* 位数和 $x$ 相同,且最高位与 $x$ 最高位相同的,这部分统计为 `res2`; |
| 56 | +* 位数比 $x$ 少,这部分统计为 `res3`。 |
| 57 | + |
| 58 | +其中 `res1` 和 `res3` 求解相对简单,重点落在如何求解 `res2` 上。 |
| 59 | + |
| 60 | +**对 $x$ 进行「从高到低」的处理(假定 $x$ 数位为 $n$),对于第 $k$ 位而言($k$ 不为最高位),假设在 $x$ 中第 $k$ 位为 $cur,ドル那么为了满足「大小限制」关系,我们只能在 $[0, cur - 1]$ 范围内取数,同时为了满足「相同数字只能使用一次」的限制,我们需要使用一个 `int` 变量 $s$ 来记录使用情况(用 $s$ 的低十位来代指数字 $[0, 9]$ 是否被使用),统计 $[0, cur - 1]$ 范围内同时符合两个限制条件的数的个数,记为 $cnt$。** |
| 61 | + |
| 62 | +**当第 $k$ 位有 $cnt$ 种合法选择之后,后面的位数可以在满足「相同数字只能使用一次」的限制条件下任意选择(因为大小关系已经由第 $k$ 位保证),为了快速知道剩下的 $n - k$ 位有多少种方案,我们还需要预处理乘积数组,其中 $f[l][r]$ 代表 $l * (l + 1) * ... * (j - 1) * j$ 的乘积之和。** |
| 63 | + |
| 64 | +> 上述讲解若是觉得抽象,我们可以举个 🌰,假设 $x = 678,ドル我们该如何求解 `res2`:由于限定了 `res2` 为「位数和 $x$ 相同,且最高位与 $x$ 最高位相同的」的合法数个数,因此最高位没有选,只能是 6ドル,ドル然后考虑处理次高位,次高位在 $x$ 中为 7ドル,ドル为了满足大小关系,我们只能在 $[0, 6]$ 范围内做限制,同时由于 6ドル$ 已用过,因此次高位实际只有 $[0, 5],ドル共 6ドル$ 种选择,当确定次高位后,后面的位数任意取,由于前面已经填充了 $p = 2$ 位(即消耗了 $p$ 个不同数字),因此从后面的位数开始应该是 $a = 10 - p$ 开始往后自减累乘到 $b = (10 - p) - (n - p) + 1$ 为止,即此时方案数为 $cnt * f[b][a]$(当前位不是最低位)或者 $cnt$(当前位是最低位)。按照此逻辑循环处理所有位数即可,直到遇到重复数值或正常结束。 |
| 65 | + |
| 66 | +需要说明的是,上述的举例部分只是为方便大家理解过程,看懂了举例部分不代表理解了数位 DP 做法成立的内在条件,阅读的重点还是要放在前面加粗字体部分,只会使用样例理解算法永远不是科学的做法。 |
| 67 | + |
| 68 | +其他细节:乘积数组的预处理与样例无关,我们可以使用 `static` 进行打表优化,同时可以将 `res1` 和 `res2` 两种情况进行合并。 |
| 69 | + |
| 70 | +代码: |
| 71 | +```Java |
| 72 | +class Solution { |
| 73 | + // f[l][r] 代表 i * (i + 1) * ... * (j - 1) * j |
| 74 | + static int[][] f = new int[10][10]; |
| 75 | + static { |
| 76 | + for (int i = 1; i < 10; i++) { |
| 77 | + for (int j = i; j < 10; j++) { |
| 78 | + int cur = 1; |
| 79 | + for (int k = i; k <= j; k++) cur *= k; |
| 80 | + f[i][j] = cur; |
| 81 | + } |
| 82 | + } |
| 83 | + } |
| 84 | + int dp(int x) { |
| 85 | + int t = x; |
| 86 | + List<Integer> nums = new ArrayList<>(); |
| 87 | + while (t != 0) { |
| 88 | + nums.add(t % 10); |
| 89 | + t /= 10; |
| 90 | + } |
| 91 | + int n = nums.size(); |
| 92 | + if (n <= 1) return x + 1; // [0, 9] |
| 93 | + // 位数和 x 相同(res1 + res2) |
| 94 | + int ans = 0; |
| 95 | + for (int i = n - 1, p = 1, s = 0; i >= 0; i--, p++) { |
| 96 | + int cur = nums.get(i), cnt = 0; |
| 97 | + for (int j = cur - 1; j >= 0; j--) { |
| 98 | + if (i == n - 1 && j == 0) continue; |
| 99 | + if (((s >> j) & 1) == 0) cnt++; |
| 100 | + } |
| 101 | + int a = 10 - p, b = a - (n - p) + 1; |
| 102 | + ans += b <= a ? cnt * f[b][a] : cnt; |
| 103 | + if (((s >> cur) & 1) == 1) break; |
| 104 | + s |= (1 << cur); |
| 105 | + if (i == 0) ans++; |
| 106 | + } |
| 107 | + // 位数比 x 少(res3) |
| 108 | + ans += 10; |
| 109 | + for (int i = 2, last = 9; i < n; i++) { |
| 110 | + int cur = last * (10 - i + 1); |
| 111 | + ans += cur; last = cur; |
| 112 | + } |
| 113 | + return ans; |
| 114 | + } |
| 115 | + public int numDupDigitsAtMostN(int n) { |
| 116 | + return (n + 1) - dp(n); |
| 117 | + } |
| 118 | +} |
| 119 | +``` |
| 120 | +* 时间复杂度:$O(\log{n})$ |
| 121 | +* 空间复杂度:$O(1)$ |
| 122 | + |
| 123 | +--- |
| 124 | + |
| 125 | +### 最后 |
| 126 | + |
| 127 | +这是我们「刷穿 LeetCode」系列文章的第 `No.1012` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 128 | + |
| 129 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 130 | + |
| 131 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 132 | + |
| 133 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 134 | + |
0 commit comments