Hard
题目描述
给定一个整数 n,返回在范围 [1, n] 中至少有一位重复数字的正整数的个数。
示例 1:
输入:n = 20
输出:1
解释:只有一个正数(<= 20)至少有 1 位重复数字:11。
示例 2:
输入:n = 100
输出:10
解释:至少有 1 位重复数字的正数(<= 100)有:11, 22, 33, 44, 55, 66, 77, 88, 99, 和 100。
示例 3:
输入:n = 1000
输出:262
约束条件:
1 <= n <= 10^9
提示:
- 有多少个没有重复数字的数?有多少个 K 位数且没有重复数字?
- 有多少个与 N 长度相同的数?有多少个与 N 前缀相同的数?
解题思路
这道题可以使用逆向思维:先计算没有重复数字的数的个数,然后用总数减去它就得到了至少有一位重复数字的数的个数。
核心思路
计算无重复数字的个数:
- 对于位数小于 n 的数字:使用排列组合计算
- 对于位数等于 n 的数字:使用数位 DP 的思想逐位计算
分两部分统计:
- 位数更少的数字:1位数有9个(1-9),2位数第一位有9种选择(1-9),第二位有9种选择(0-9除去第一位),以此类推
- 位数相同的数字:从高位到低位,考虑每一位可以选择的数字个数
具体算法
- 首先将 n 转换为字符串获取各位数字
- 计算位数小于 n 的所有无重复数字的个数
- 计算位数等于 n 且小于等于 n 的无重复数字的个数
- 用 n 减去无重复数字的总个数
时间复杂度主要取决于 n 的位数,空间复杂度为常数级别。这是一道典型的数位 DP 问题的变种。
代码实现
class Solution {
public:
int numDupDigitsAtMostN(int n) {
string s = to_string(n + 1);
int len = s.length();
// 计算无重复数字的个数
int uniqueCount = 0;
// 计算位数小于len的数字个数
for (int i = 1; i < len; i++) {
uniqueCount += 9 * permutation(9, i - 1);
}
// 计算位数等于len且小于n+1的数字个数
unordered_set<int> used;
for (int i = 0; i < len; i++) {
int digit = s[i] - '0';
int smaller = 0;
for (int d = (i == 0 ? 1 : 0); d < digit; d++) {
if (used.find(d) == used.end()) {
smaller++;
}
}
uniqueCount += smaller * permutation(9 - i, len - i - 1);
if (used.find(digit) != used.end()) {
break;
}
used.insert(digit);
}
return n - uniqueCount;
}
private:
int permutation(int n, int r) {
if (r == 0) return 1;
int result = 1;
for (int i = 0; i < r; i++) {
result *= (n - i);
}
return result;
}
};
class Solution:
def numDupDigitsAtMostN(self, n: int) -> int:
s = str(n + 1)
length = len(s)
# 计算无重复数字的个数
unique_count = 0
# 计算位数小于length的数字个数
for i in range(1, length):
unique_count += 9 * self.permutation(9, i - 1)
# 计算位数等于length且小于n+1的数字个数
used = set()
for i in range(length):
digit = int(s[i])
smaller = 0
start = 1 if i == 0 else 0
for d in range(start, digit):
if d not in used:
smaller += 1
unique_count += smaller * self.permutation(9 - i, length - i - 1)
if digit in used:
break
used.add(digit)
return n - unique_count
def permutation(self, n: int, r: int) -> int:
if r == 0:
return 1
result = 1
for i in range(r):
result *= (n - i)
return result
public class Solution {
public int NumDupDigitsAtMostN(int n) {
string s = (n + 1).ToString();
int length = s.Length;
// 计算无重复数字的个数
int uniqueCount = 0;
// 计算位数小于length的数字个数
for (int i = 1; i < length; i++) {
uniqueCount += 9 * Permutation(9, i - 1);
}
// 计算位数等于length且小于n+1的数字个数
HashSet<int> used = new HashSet<int>();
for (int i = 0; i < length; i++) {
int digit = s[i] - '0';
int smaller = 0;
int start = i == 0 ? 1 : 0;
for (int d = start; d < digit; d++) {
if (!used.Contains(d)) {
smaller++;
}
}
uniqueCount += smaller * Permutation(9 - i, length - i - 1);
if (used.Contains(digit)) {
break;
}
used.Add(digit);
}
return n - uniqueCount;
}
private int Permutation(int n, int r) {
if (r == 0) return 1;
int result = 1;
for (int i = 0; i < r; i++) {
result *= (n - i);
}
return result;
}
}
var numDupDigitsAtMostN = function(n) {
function countUniqueDigits(digits, used, tight, pos) {
if (pos === digits.length) return 1;
let result = 0;
let limit = tight ? parseInt(digits[pos]) : 9;
for (let digit = 0; digit <= limit; digit++) {
if (used & (1 << digit)) continue;
let newUsed = used;
let newTight = tight && (digit === limit);
if (used !== 0 || digit !== 0) {
newUsed |= (1 << digit);
}
result += countUniqueDigits(digits, newUsed, newTight, pos + 1);
}
return result;
}
let digits = n.toString();
let totalUnique = countUniqueDigits(digits, 0, true, 0) - 1;
return n - totalUnique;
};
复杂度分析
| 复杂度类型 | 值 |
|---|---|
| 时间复杂度 | O(log n) |
| 空间复杂度 | O(log n) |
说明:
- 时间复杂度:主要取决于数字 n 的位数,为 O(log n)
- 空间复杂度:需要存储已使用的数字集合,最多 10 个数字,实际为 O(log n)