来源:力扣(LeetCode)
描述:
给定正整数 n
,返回在 [1, n]
范围内具有 至少 1 位 重复数字的正整数的个数。
示例 1:
输入:n = 20
输出:1
解释:具有至少 1 位重复数字的正数(<= 20)只有 11 。
示例 2:
输入:n = 100
输出:10
解释:具有至少 1 位重复数字的正数(<= 100)有 11,22,33,44,55,66,77,88,99 和 100 。
示例 3:
输入:n = 1000
输出:262
提示:
- 1 <= n <= 109
方法一:记忆化搜索
由互斥原理可知,至少有 1 位重复的数字的正整数个数等于总个数减去没有重复数字的正整数个数。为了方便计算,我们首先求出在整数区间 [0, n] 之间的没有重复数字的正整数个数 x,那么结果等于 n + 1 − x。
我们从最高位开始填入各个数字,使用整数掩码 mask 记录前面已经填入过的数字(注意前缀 0 不计入已填入的数字)。假设当前填入第 i 位,如果前面填入的数字与 n 对应位置的数字相同,那么可选的填入数字小于等于 n 在第 i 位的数字,否则可填入全部数字。
记可填入的最大数字为 t,依次尝试填入数字 k ∈ [0, t],如果 k 已经出现在 mask 中,那么说明填入数字 k 不合法,否则说明可以填入数字 k,那么尝试填入第 i + 1 位的数字。
在填入第 i + 1 位的数字时,如果掩码 maski = 0 且 k = 0 成立,那么说明前面都是前缀 0,掩码 maski+1 为 0,否则 maski+1 等于 maski 在第 k 位设为一后的值。如果在填入第 i 位时,前面填入的数字与 n 对应位置的数字相同,且在第 i 位填入的数字为 t,那么填入第 i + 1 位时,前面填入的数字也与 n 对应位置的数字相同。
注意到,假设当前需要填入第 i 位,且前面填入的数字与 n 对应位置的数字不相同,那么需要求得的不重复数字的正整数个数只与 mask 相关,我们可以使用备忘录 dp 记录该结果,避免重复计算。
代码:
class Solution {
public:
vector<vector<int>> dp;
int f(int mask, const string &sn, int i, bool same) {
if (i == sn.size()) {
return 1;
}
if (!same && dp[i][mask] >= 0) {
return dp[i][mask];
}
int res = 0, t = same ? (sn[i] - '0') : 9;
for (int k = 0; k <= t; k++) {
if (mask & (1 << k)) {
continue;
}
res += f(mask == 0 && k == 0 ? mask : mask | (1 << k), sn, i + 1, same && k == t);
}
if (!same) {
dp[i][mask] = res;
}
return res;
}
int numDupDigitsAtMostN(int n) {
string sn = to_string(n);
dp.resize(sn.size(), vector<int>(1 << 10, -1));
return n + 1 - f(0, sn, 0, true);
}
};
执行用时:8 ms, 在所有 C++ 提交中击败了74.55%的用户
内存消耗:9.8 MB, 在所有 C++ 提交中击败了14.54%的用户
复杂度分析
时间复杂度:O(m×2w),其中 m 是整数 n 的十进制位数,w = 10 表示十进制数的数字类型数目。最多计算 m×2w 个状态,单个状态需要 O(w) 的时间。
空间复杂度:O(m×2w)。保存 dp 需要 O(m×2w) 的空间。
方法二:组合数学
方法一只有两种情况需要继续进入搜索:
- 前面填入的数字与 n 对应位置的数字相同,且当前填入的数字为 t;
- 前面填入的数字都是 0,即前缀 0,并且当前填入的数字也为 0。
其他情况可以直接利用组合数学进行计算,当前填入第 i 位,那么剩余 m − 1 − i 位待填入,记已经填入的数字数目为 c,那么可选的数字数目为 10 − c,那么剩余的不重复数字的正整数个数等于组合数 A 10 − c c A_{10-c}^c A10−cc(n 的数据范围保证了组合数合法)。
代码:
class Solution {
public:
int A(int x, int y) {
int res = 1;
for (int i = 0; i < x; i++) {
res *= y--;
}
return res;
}
int f(int mask, const string &sn, int i, bool same) {
if (i == sn.size()) {
return 1;
}
int t = same ? sn[i] - '0' : 9, res = 0, c = __builtin_popcount(mask) + 1;
for (int k = 0; k <= t; k++) {
if (mask & (1 << k)) {
continue;
}
if (same && k == t) {
res += f(mask | (1 << t), sn, i + 1, true);
} else if (mask == 0 && k == 0) {
res += f(0, sn, i + 1, false);
} else {
res += A(sn.size() - 1 - i, 10 - c);
}
}
return res;
}
int numDupDigitsAtMostN(int n) {
string sn = to_string(n);
return n + 1 - f(0, sn, 0, true);
}
};
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:5.7 MB, 在所有 C++ 提交中击败了98.18%的用户
复杂度分析
时间复杂度:O(m×w2),其中 m 是整数 n 的十进制位数,w=10 表示十进制数的数字类型数目。计算组合数需要 O(w) 的时间,而前面提到的两种情况互斥,因此搜索过程最多只出现一种情况,搜索层数为 m 层,因此总时间复杂度为 O(m×w2)。
空间复杂度:O(m)。递归需要 O(m) 的栈空间。
author:LeetCode-Solution