@@ -44,8 +44,7 @@ class Solution {
4444 int ans = 10 ;
4545 for (int i = 2 , last = 9 ; i <= n; i++ ) {
4646 int cur = last * (10 - i + 1 );
47- ans += cur;
48- last = cur;
47+ ans += cur; last = cur;
4948 }
5049 return ans;
5150 }
@@ -56,6 +55,88 @@ class Solution {
5655
5756---
5857
58+ ### 数位 DP
59+
60+ 一种更为进阶的做法,应当是可以回答任意区间 $[ l, r] $ 内合法数的个数。
61+
62+ 这需要运用「数位 DP」进行求解,假定我们存在函数 ` int dp(int x) ` 函数,能够返回区间 $[ 0, x] $ 内合法数的个数,那么配合「容斥原理」我们便能够回答任意区间合法数的查询:
63+
64+ $$
65+ ans_{(l, r)} = dp(r) - dp(l - 1)
66+ $$
67+
68+ 然后考虑如何实现 ` int dp(int x) ` 函数,我们将组成 $[ 0, x] $ 的合法数分成三类:
69+ * 位数和 $x$ 相同,且最高位比 $x$ 最高位要小的,这部分统计为 ` res1 ` ;
70+ * 位数和 $x$ 相同,且最高位与 $x$ 最高位相同的,这部分统计为 ` res2 ` ;
71+ * 位数比 $x$ 少,这部分统计为 ` res3 ` 。
72+
73+ 其中 ` res1 ` 和 ` res3 ` 求解相对简单,重点落在如何求解 ` res2 ` 上。
74+
75+ ** 对 $x$ 进行「从高到低」的处理(假定 $x$ 数位为 $n$),对于第 $k$ 位而言($k$ 不为最高位),假设在 $x$ 中第 $k$ 位为 $cur$,那么为了满足「大小限制」关系,我们只能在 $[ 0, cur - 1] $ 范围内取数,同时为了满足「相同数字只能使用一次」的限制,我们需要使用一个 ` int ` 变量 $s$ 来记录使用情况(用 $s$ 的低十位来代指数字 $[ 0, 9] $ 是否被使用),统计 $[ 0, cur - 1] $ 范围内同时符合两个限制条件的数的个数,记为 $cnt$。**
76+
77+ ** 当第 $k$ 位有 $cnt$ 种合法选择之后,后面的位数可以在满足「相同数字只能使用一次」的限制条件下任意选择(因为大小关系已经由第 $k$ 位保证),为了快速知道剩下的 $n - k$ 位有多少种方案,我们还需要预处理乘积数组,其中 $f[ l] [ r ] $ 代表 $l * (l + 1) * ... * (j - 1) * j$ 的乘积之和。**
78+
79+ > 上述讲解若是觉得抽象,我们可以举个 🌰,假设 $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$(当前位是最低位)。按照此逻辑循环处理所有位数即可,直到遇到重复数值或正常结束。
80+
81+ 需要说明的是,上述的举例部分只是为方便大家理解过程,看懂了举例部分不代表理解了数位 DP 做法成立的内在条件,阅读的重点还是要放在前面加粗字体部分,只会使用样例理解算法永远不是科学的做法。
82+
83+ 其他细节:乘积数组的预处理与样例无关,我们可以使用 ` static ` 进行打表优化,同时可以将 ` res1 ` 和 ` res2 ` 两种情况进行合并。
84+
85+ 代码:
86+ ``` Java
87+ class Solution {
88+ // f[l][r] 代表 i * (i + 1) * ... * (j - 1) * j
89+ static int [][] f = new int [10 ][10 ];
90+ static {
91+ for (int i = 1 ; i < 10 ; i++ ) {
92+ for (int j = i; j < 10 ; j++ ) {
93+ int cur = 1 ;
94+ for (int k = i; k <= j; k++ ) cur *= k;
95+ f[i][j] = cur;
96+ }
97+ }
98+ }
99+ int dp (int x ) {
100+ int t = x;
101+ List<Integer > nums = new ArrayList<> ();
102+ while (t != 0 ) {
103+ nums. add(t % 10 );
104+ t /= 10 ;
105+ }
106+ int n = nums. size();
107+ if (n <= 1 ) return x + 1 ; // [0, 9]
108+ // 位数和 x 相同(res1 + res2)
109+ int ans = 0 ;
110+ for (int i = n - 1 , p = 1 , s = 0 ; i >= 0 ; i-- , p++ ) {
111+ int cur = nums. get(i), cnt = 0 ;
112+ for (int j = cur - 1 ; j >= 0 ; j-- ) {
113+ if (i == n - 1 && j == 0 ) continue ;
114+ if (((s >> j) & 1 ) == 0 ) cnt++ ;
115+ }
116+ int a = 10 - p, b = a - (n - p) + 1 ;
117+ ans += b <= a ? cnt * f[b][a] : cnt;
118+ if (((s >> cur) & 1 ) == 1 ) break ;
119+ s |= (1 << cur);
120+ if (i == 0 ) ans++ ;
121+ }
122+ // 位数比 x 少(res3)
123+ ans += 10 ;
124+ for (int i = 2 , last = 9 ; i < n; i++ ) {
125+ int cur = last * (10 - i + 1 );
126+ ans += cur; last = cur;
127+ }
128+ return ans;
129+ }
130+ public int countNumbersWithUniqueDigits (int n ) {
131+ return dp((int )Math . pow(10 , n) - 1 );
132+ }
133+ }
134+ ```
135+ * 时间复杂度:$O(n)$
136+ * 空间复杂度:$O(n)$
137+
138+ ---
139+
59140### 最后
60141
61142这是我们「刷穿 LeetCode」系列文章的第 ` No.357 ` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
0 commit comments