问题本质 :计算 [1, N] 范围内至少有一位重复数字 的正整数个数。直接计算较难,可转化为计算没有重复数字 的数的个数(即各位数字互不相同的数),然后用总数 N 减去该值 。
核心思路 :计算 [1, N] 范围内数位互不相同的数的个数。
- 数位 DP 状态设计 :
dp[pos][mask][isLimit][isNum]pos: 当前处理到数字的第几位(从高位到低位)。mask: 一个位掩码(10位),记录0~9哪些数字已被使用过。isLimit: 当前位是否受到N对应位的限制(即之前所有位都与N相同)。isNum: 当前是否已经开始构造数字(避免前导零被误判为使用数字0)。
- 记忆化搜索 :从最高位开始递归填充每一位,在
!isLimit && isNum时缓存结果。 - 最终答案 :
N - countUnique(N)。
C++ 代码实现:
cpp
class Solution {
public:
int numDupDigitsAtMostN(int n) {
string s = to_string(n);
int m = s.length();
// dp[pos][mask] 记忆化,isLimit=false 且 isNum=true 时才缓存 vector<vector<int>> memo(m, vector<int>(1 << 10, -1));
// 返回从 pos 开始构造,已使用数字掩码为 mask,能构造出的无重复数字的数的个数
// isLimit: 当前位是否受 n 的限制 // isNum:是否已经开始构造数字(即是否跳过了前导零)
function<int(int, int, bool, bool)> dfs = [&](int pos, int mask, bool isLimit, bool isNum) -> int {
if (pos == m) {
// 构造完成,如果已经开始构造数字(isNum=true),则算一个有效数字 return isNum ? 1 : 0;
}
// 记忆化:只有在不受限制且已经开始构造数字时,结果才是可复用的 if (!isLimit && isNum && memo[pos][mask] != -1) {
return memo[pos][mask];
}
int res = 0;
// 如果之前还没有开始构造数字,这一位可以选择继续跳过(即仍然不构造数字)
if (!isNum) {
// 跳过当前位,即这一位不填数字,mask 不变,isLimit=false(因为跳过了,相当于填了0,但0不计入数字,所以不受原数限制)
res += dfs(pos + 1, mask, false, false);
}
// 确定当前位可以填的数字范围
int low = isNum ? 0 : 1; // 如果还没开始构造,不能填0(避免前导零)
int high = isLimit ? s[pos] - '0' : 9;
// 枚举当前位可以填的数字
for (int d = low; d <= high; ++d) {
// 检查数字 d 是否已经被使用过 if ((mask >> d) & 1) continue; // 被使用过,跳过 // 填入数字 d,更新 mask,更新 isLimit(如果之前受限且当前位填的是上限值,则后续仍受限)
res += dfs(pos + 1, mask | (1 << d), isLimit && (d == high), true);
}
// 缓存结果:只有不受限制且已经开始构造数字的状态才缓存 if (!isLimit && isNum) {
memo[pos][mask] = res;
}
return res;
};
// 总共有 n 个数,减去没有重复数字的数的个数,即为至少有一位重复的数字的个数 return n - dfs(0, 0, true, false);
}
};
复杂度分析:
- 时间复杂度 :O(m * 2^10 *10),其中
m是N的位数(最大为10)。状态数为m * 2^10,每个状态最多枚举 10 个数字。 - 空间复杂度:O(m * 2^10),用于记忆化存储。