统计区间内的特殊整数
题目描述
如果一个正整数每一个数位上的数字都是 互不相同 的,我们称它为 特殊整数。
给定一个正整数 n
,请你返回区间 [1, n]
之间特殊整数的数目。
示例
示例 1:
输入:n = 20
输出:19
解释:1 到 20 之间所有整数除了 11 以外都是特殊整数。所以总共有 19 个特殊整数。
示例 2:
输入:n = 5
输出:5
解释:1 到 5 所有整数都是特殊整数。
示例 3:
输入:n = 135
输出:110
解释:从 1 到 135 总共有 110 个整数是特殊整数。
不特殊的部分数字为:22 ,114 和 131 。
数据范围
1 <= n <= 2 * 10^9
解决方案
为了统计区间 [1, n]
内的特殊整数数量,我们需要确保每个数字的各个数位上的数字都是互不相同的。由于 n
的范围可以达到 2 * 10^9
,直接遍历所有数字并检查每个数字是否为特殊整数的效率过低。因此,我们需要一种高效的方法来计算符合条件的数字数量。
思路解析
我们可以使用 数字动态规划(Digit DP) 的方法来解决这个问题。数字 DP 是一种专门用于处理与数字的各个数位相关问题的动态规划技巧,特别适用于范围计数问题。
以下是具体的思路步骤:
-
将数字转换为字符串:将
n
转换为字符串s
,方便逐位处理。 -
定义状态:
i
:当前处理的位数(从左到右)。is_limit
:一个布尔值,表示当前位是否受到上界n
的限制。如果is_limit
为true
,则当前位的数字不能超过s[i]
;否则,可以自由选择0
到9
。mask
:一个位掩码,用于记录当前已经使用过的数字。mask
的第c
位为1
表示数字c
已经被使用。is_num
:一个布尔值,表示当前是否已经开始构建数字。如果is_num
为false
,则表示还没有选择任何非零数字,此时可以选择跳过当前位(即选择前导零)。
-
递归函数
dfs
:- 终止条件:当处理到第
m
位(即所有位都处理完)时,如果已经构建了一个有效数字(is_num
为true
),则返回1
,否则返回0
。 - 记忆化:为了避免重复计算,当
is_limit
为false
且已经开始构建数字时,如果当前状态已经计算过,则直接返回记忆化的结果。 - 选择跳过当前位:如果还没有开始构建数字(
is_num
为false
),可以选择跳过当前位,继续处理下一位。 - 选择当前位的数字:根据
is_limit
的状态,确定当前位可以选择的数字范围。如果选择了某个数字c
,需要检查c
是否已经被使用过(通过mask
判断)。如果没有被使用,则更新mask
并递归处理下一位。
- 终止条件:当处理到第
-
记忆化表:使用一个二维数组
memo
来存储中间结果,避免重复计算。
代码解析
以下是基于上述思路实现的代码:
#include <vector>
#include <string>
#include <functional>
using namespace std;
class Solution {
public:
int countSpecialNumbers(int n) {
string s = to_string(n); // 将数字转换为字符串
int m = s.size(); // 数字的位数
// 初始化记忆化表,-1 表示未计算
vector<vector<int>> memo(m, vector<int>(1 << 10, -1));
// 定义递归函数
function<int(int, bool, int, bool)> dfs = [&](int i, bool is_limit, int mask, bool is_num) -> int {
if(i == m) return is_num ? 1 : 0; // 终止条件
if(!is_limit && is_num && memo[i][mask] != -1) return memo[i][mask]; // 记忆化
int res = 0;
if(!is_num) {
// 选择跳过当前位
res += dfs(i + 1, false, mask, false);
}
int up = is_limit ? s[i] - '0' : 9; // 当前位的上限
for(int c = is_num ? 0 : 1; c <= up; c++) { // 如果已经开始构建数字,允许选择 0;否则,从 1 开始
if(!((mask >> c) & 1)) { // 检查数字 c 是否已经被使用
res += dfs(i + 1, is_limit && (c == up), mask | (1 << c), true);
}
}
if(!is_limit && is_num) {
memo[i][mask] = res; // 更新记忆化表
}
return res;
};
return dfs(0, true, 0, false); // 从第 0 位开始递归
}
};
代码细节说明
-
字符串转换:
string s = to_string(n); int m = s.size();
将输入数字
n
转换为字符串s
,便于逐位处理。同时,获取数字的总位数m
。 -
记忆化表:
vector<vector<int>> memo(m, vector<int>(1 << 10, -1));
使用一个二维数组
memo
来存储中间结果。memo[i][mask]
表示在第i
位,当前位数掩码为mask
时,所能构成的特殊整数数量。初始化为-1
,表示尚未计算。 -
递归函数
dfs
:function<int(int, bool, int, bool)> dfs = [&](int i, bool is_limit, int mask, bool is_num) -> int { /* ... */ };
使用 lambda 表达式定义递归函数
dfs
,并捕获所有必要的变量。 -
终止条件:
if(i == m) return is_num ? 1 : 0;
当处理到第
m
位时,如果已经开始构建数字(is_num
为true
),则返回1
,表示找到一个特殊整数;否则返回0
。 -
记忆化查找:
if(!is_limit && is_num && memo[i][mask] != -1) return memo[i][mask];
如果当前状态
i
,mask
已经被计算过,且不受上界限制,并且已经开始构建数字,则直接返回记忆化的结果,避免重复计算。 -
选择跳过当前位:
if(!is_num) { res += dfs(i + 1, false, mask, false); }
如果还没有开始构建数字,可以选择跳过当前位,继续处理下一位。
-
确定当前位的数字范围:
int up = is_limit ? s[i] - '0' : 9; for(int c = is_num ? 0 : 1; c <= up; c++) { /* ... */ }
如果当前位受到上界
n
的限制,则最大可以选择的数字为s[i] - '0'
;否则,可以自由选择0
到9
。如果已经开始构建数字,允许选择0
;否则,从1
开始,避免前导零。 -
检查数字是否重复:
if(!((mask >> c) & 1)) { /* ... */ }
使用位运算检查数字
c
是否已经被使用过。如果未被使用,则继续递归处理下一位,同时更新mask
。 -
更新记忆化表:
if(!is_limit && is_num) { memo[i][mask] = res; }
当当前状态不受上界限制,并且已经开始构建数字时,将结果存入记忆化表。
-
递归调用:
return dfs(0, true, 0, false);
从第
0
位开始,初始状态为受上界限制 (is_limit = true
)、未使用任何数字 (mask = 0
)、且尚未开始构建数字 (is_num = false
)。
时间复杂度分析
由于数字 n
的位数最多为 10
(因为 n <= 2 * 10^9
),递归的状态数为 位数 * 上界限制 * mask * 是否开始构建数字
。具体来说,最多为 10 * 2 * 1024 * 2 = 40960
,远小于 10^7
,因此算法的时间复杂度是可接受的。
总结
通过使用数字动态规划的方法,我们能够高效地计算出区间 [1, n]
内的特殊整数数量。关键在于合理地定义状态并使用记忆化技术避免重复计算。此方法不仅适用于本题,也适用于其他涉及数位限制和组合的计数问题。