统计各位数字都不同的数字个数 图解题解
这道题到底在问什么
- 输入
- n = 2
- 输出
- 91
- 输入
- n = 3
- 输出
- 739
最优解:为什么这么做
一句话答案:LeetCode 357 统计各位数字都不同的数字个数:按位数分组,每组用乘法原理数个数——首位 9 种、之后每位可选数递减,再逐组累加成答案。时间 O(n)、空间 O(1)。
0 到 10 的 n 次方里,哪些数各位不重复
给一个整数 n(0 ≤ n ≤ 8),数一数在 0 ≤ x < 10 的 n 次方这个范围里,有多少个数各位数字互不相同。比如 121 里有两个 1,不算;120 三位全不同,算。题面给的例子 n = 2 答案 91,n = 3 答案 739。要的是满足条件的个数,不是把它们一个个列出来。
为什么把 0 到 10 的 n 次方逐个查会跑不动
最直接的想法是把范围里每个数都取出来,拆成各位数字逐个查有没有重复。可是 n = 8 时上界是一亿,一个个查要上亿次判断,n 再大范围还成十倍地涨,这条路撑不住。同样位数的数,判重复的规律一样——按位数分组,每组个数一个乘法式子就出来,不必逐个枚举。
按位数分组:每组的个数怎么用乘法数出来
把答案按位数拆开:1 位数一组、2 位数一组、3 位数一组,一直到 n 位数,各组个数加起来就是总数。每一组的个数不用一个个数,用乘法原理(每一位的选法数相乘)算。以两位数为例:首位有几种填法乘上个位有几种填法,就是这一组各位不同的数的个数。要点是逐位算清每一位还剩几个数字能填。
首位为什么是 9 种、后面每位为什么递减
两位及以上时,首位不能填 0——填了 0 像 05 其实就缩成一位数了,所以首位只能从 1 到 9 里挑,9 种。轮到第二位,0 到 9 一共 10 个数字,首位已经占掉 1 个,还剩 9 个可选(注意第二位反而能填 0);再往后每多占一位,可选的数字就再少一个,于是是 9、8、7 这样往下递减。把这一组每一位的选法数乘起来就是本组个数,再把各组个数累加,读到第 n 组的累计就是答案。这就是代码里那串不断乘以「越来越小的数」的来历。
拿 n = 3 亲手把这张表填到 739
1 位数:0 到 9 都能填,10 个,累计 10。2 位数:首位 9 种、个位剩 9 种,9 乘 9 等于 81,累计 10 加 81 等于 91,正好对上 n = 2 的答案。3 位数:首位 9 种、十位剩 9 种、个位再少一个剩 8 种,9 乘 9 乘 8 等于 648,累计 91 加 648 等于 739。读到第 3 组的累计 739,就是 n = 3 的答案。
n = 0 为什么单返 1、首位为 0 为什么会重复计数
按位数循环 n 次、每次一次乘法,时间 O(n);只需累计值和当前可选数两个变量,空间 O(1)。两个边界最容易错。一是 n = 0:范围是 0 ≤ x < 1,只含数字 0 一个,直接返回 1,套不进上面的分组公式。二是首位填 0:2 位及以上时首位只能 1 到 9,允许 0 会让位数缩水、和更短的数重复计数。还有一点,答案是各组之和而不是最后一组——小于 10 的 n 次方包含所有更短位数的数,都要算进去。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住口诀:按位数分组,每组用乘法原理数个数,首位 9 种、之后每位少一个,逐组累加。下面每帧都在套它。
- 4先搭一张表:每一行是一个「位数组」,列出首位选法、其余位选法、本组个数,最后累加成答案。我们一行行把它填满。
- 5从最简单的 1 位数开始。0 到 9 这 10 个数,每个都只有一位数字,谈不上重复,所以全部都算。
- 6一位数没有前导零的顾虑,这一位 0 到 9 都能填,所以首位选法填 10。
- 7一位数后面没有别的位置,其余位这一格就写「无」,跳过它。
- 8把首位的 10 种选法乘起来,后面没有别的位,本组个数就是 10。
- 9累计答案从 0 起步,加上这一组的 10 个,现在累计是 10。第一行填完了。
- 10进入两位数。关键来了:两位数的首位不能是 0,否则像 05 其实就是一位数,所以首位的选法要受限。
- 11首位从 1 到 9 里挑,9 种选法,填 9。这就是「首位不能为 0」带来的限制。
- 12到个位时,0 到 9 一共 10 个数字,首位已经用掉 1 个,剩下 9 个可选。注意这里个位反而可以是 0。
- 13首位 9 种、个位 9 种,乘起来 9 乘 9 等于 81,两位数里各位不同的有 81 个。
- 14把上一行累计的 10,加上这一组的 81,累计变成 91。这正好对上 n = 2 的答案。
- 15进入三位数。规律和两位数一样,只是多了一位,可用的数字会再少一个。
- 16三位数的首位同样不能是 0,1 到 9 共 9 种,填 9。
- 17十位可填的还剩 9 个,个位又被前两位用掉两个、只剩 8 个,9 乘 8 等于 72。每多占一位,选择就少一个。
- 18首位 9 种,乘上其余两位的 72 种,得到 648,三位数里各位不同的有 648 个。
- 19上一行累计 91,加上这一组的 648,累计来到 739。整张表填满了。
- 20看累计答案这一列:10、91、739。n 是几,就读到第几行的累计。n = 3 的答案就是 739。
- 21回头看本质:首位永远是 9 种,从第二位开始,每填一位可用的数字就少一个,9、8、7 这样递减。这就是代码里那个不断乘以「越来越小的数」的来历。
- 22顺手验证一下:如果问的是 n = 2,就读到第二行的累计 91,和题目示例完全一致。这张表一次算好,各种 n 都能查。
- 23只有一个特例要单独记:n = 0 时范围是 0 到 1 之间,只剩数字 0 本身一个,答案直接是 1,不走上面的分组流程。
- 24整体回放一遍:1 位数 10 个,加 2 位数 81 个到 91,再加 3 位数 648 个到 739。按位数分组、乘法原理算个数、逐组累加,这就是全部思路。
⚠️ 容易写错的地方
✗ 错:首位也允许填 0
✓ 对:两位及以上时首位只能 1 到 9
首位填 0 会让位数缩水,变成更短的数,重复计数
✗ 错:n = 0 当成普通情况算
✓ 对:n = 0 单独返回 1
范围 0 ≤ x < 1 只含数字 0,套不进分组公式
✗ 错:各组个数不累加,只取最后一组
✓ 对:答案是各组之和(累计)
小于 10 的 n 次方包含所有更短位数的数,要全算进去
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def countNumbersWithUniqueDigits(self, n: int) -> int:
@cache
def dfs(i: int, mask: int, lead: bool) -> int:
if i < 0:
return 1
ans = 0
for j in range(10):
if mask >> j & 1:
continue
if lead and j == 0:
ans += dfs(i - 1, mask, True)
else:
ans += dfs(i - 1, mask | 1 << j, False)
return ans
return dfs(n - 1, 0, True)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
int countNumbersWithUniqueDigits(int n) {
int f[n + 1][1 << 10];
memset(f, -1, sizeof(f));
function<int(int, int, bool)> dfs = [&]( int i, int mask, bool lead ) -> int {
if (i < 0) {
return 1;
}
if (!lead && f[i][mask] != -1) {
return f[i][mask];
}
int ans = 0;
for (int j = 0; j <= 9; ++j) {
if (mask >> j & 1) {
continue;
}
if (lead && j == 0) {
ans += dfs(i - 1, mask, true);
} else {
ans += dfs(i - 1, mask | (1 << j), false);
}
}
if (!lead) {
f[i][mask] = ans;
}
return ans;
};
return dfs(n - 1, 0, true);
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
private Integer[][] f;
public int countNumbersWithUniqueDigits(int n) {
f = new Integer[n][1 << 10];
return dfs(n - 1, 0, true);
}
private int dfs(int i, int mask, boolean lead) {
if (i < 0) {
return 1;
}
if (!lead && f[i][mask] != null) {
return f[i][mask];
}
int ans = 0;
for (int j = 0; j <= 9; ++j) {
if ((mask >> j & 1) == 1) {
continue;
}
if (lead && j == 0) {
ans += dfs(i - 1, mask, true);
} else {
ans += dfs(i - 1, mask | 1 << j, false);
}
}
if (!lead) {
f[i][mask] = ans;
}
return ans;
}
}复杂度
时间
O(n)
组合写法只需按位数循环 n 次,每次一次乘法
空间
O(1)
只用累计值和当前可选数两个变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 统计各位数字都不同的数字个数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题为什么归到动态规划,明明像数学排列?+
因为它有清晰的「子问题加累加」结构:第 k 位数组的个数可以由更短位数推出,答案是各组的前缀累加。组合写法把这个递推显式算成循环;参考代码的数位 DP 则用记忆化搜索(把算过的结果记下来、下次直接取)表达同一递推,把「还剩几位、哪些数字用过」当状态。两种都是 DP 思想,只是一个写成循环、一个写成带记忆的递归。
首位是 9 种,为什么个位也是 9 种,不该更少吗?+
首位从 1 到 9 选、排除了 0,是 9 种。轮到个位时能用的数字是全部 10 个减去首位已占的 1 个,等于 9 个,而且个位允许填 0,所以恰好也是 9 种。到第三位才因为前两位各占一个、只剩 8 种。首位的 9 是「不能为 0」造成的,个位的 9 是「用掉一个」造成的,来历不同、数值碰巧相等。
如果改成「各位数字都不同且小于某个具体上界 N」呢?+
那就退化成标准的数位 DP 题:从高位到低位逐位填,额外加一个 limit 标记当前是否贴着上界。参考代码里的 mask(用二进制位记哪些数字已用过)加 lead(标记是否还在前导零阶段)框架稍作扩展就能支持,这也是为什么用数位 DP 写法更通用、面试更爱考。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 统计各位数字都不同的数字个数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。