1、递归+动态规划
我们可以使用数组
d
p
[
i
]
dp[i]
dp[i]来记录到字符串的第
i
i
i位位置,前几位产生满足条件的情况数总和。显然比起寻找至少有一位重复的数字,更简单的方法是用总数减去每一位都不重复的数字。因此我们定义函数f(i,mask,isLimit,isNum)
,其中:
mask
表示已经被选中的数字集合,0 表示未选中,1表示选中;isLimit
表示当前的情况是否会受到n
的约束,若为真则说明在当前情况中,我们最多只能遍历到s[i]
;isNum
表示当前位置是否可以从 0 开始,真表示可以。
在递归过程中:
- 若
isNum
为假说明当前为第一位,后面递归中我们的isLimit
可以置为真,因为不管多大都不会超过n
; - 若
isNum
为真说明当前数字不为第一位,我们需要根据isNum
和isLimit
判断当前可以填入数字的范围。
当i
指向字符串的末尾时,说明我们已经遍历完当前字符串。若isNum
为真说明当前合法,返回 1 ,否则返回 0 。
class Solution {
public:
int numDupDigitsAtMostN(int n) {
auto s = to_string(n);
int m = s.length(), dp[m][1 << 10];
memset(dp, -1, sizeof(dp)); // -1 表示没有计算过
function<int(int, int, bool, bool)> f = [&](int i, int mask, bool is_limit, bool is_num) -> int {
if (i == m)
return is_num; // is_num 为 true 表示得到了一个合法数字
if (!is_limit && is_num && dp[i][mask] != -1)
return dp[i][mask];
int res = 0;
if (!is_num) // 可以跳过当前数位
res = f(i + 1, mask, false, false);
int up = is_limit ? s[i] - '0' : 9; // 如果前面填的数字都和 n 的一样,那么这一位至多填数字 s[i](否则就超过 n 啦)
for (int d = 1 - is_num; d <= up; ++d) // 枚举要填入的数字 d
if ((mask >> d & 1) == 0) // d 不在 mask 中
res += f(i + 1, mask | (1 << d), is_limit && d == up, true);
if (!is_limit && is_num)
dp[i][mask] = res;
return res;
};
return n - f(0, 0, true, false);
}
};