Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 96dae8b

Browse files
✨update: Modify 357
1 parent 15c5b3f commit 96dae8b

File tree

1 file changed

+83
-2
lines changed

1 file changed

+83
-2
lines changed

‎LeetCode/351-360/357. 统计各位数字都不同的数字个数(中等).md‎

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /