1. 明确需求和约束
- 输入:一个正整数 ( n )。
- 输出:从 1 到 ( n ) 之间的“好数”的数量。
- 限制条件:
- 数字不能包含前导零,但由于输入是正整数且范围从 1 到 ( n ),这一点天然满足。
- 数字的数位中最多包含两种不同的数字。
- 关键点:如何有效判断一个数字的数位是否符合“好数”的定义。
2. 分解问题
要解决此问题,可以将问题分解为以下几个步骤:
- 遍历从 1 到 ( n ) 的每个数字。
- 对每个数字,提取其数位,并判断该数字是否是“好数”:
- 将数字转换为字符串。
- 遍历字符串中的每一位,并记录这些数位。
- 使用集合(
Set)存储数位的独特值。
- 如果集合中不超过 2 个不同的数位,则这个数字是“好数”,将计数器加 1。
- 最后输出计数结果。
3. 算法设计
通过上述分析,可以设计出暴力枚举的方法。具体步骤如下:
- 初始化计数器:定义一个变量
count来记录“好数”的数量。 - 遍历每个数字:从 1 到 ( n ) 的所有数字逐个检查。
- 判断是否是“好数”:
- 将数字转换为字符串,提取其数位。
- 利用集合(
Set)自动去重的特性,统计该数字中不同数位的数量。 - 判断集合的大小是否小于等于 2。
- 计数:如果集合大小符合条件,则将计数器加 1。
- 输出结果:最后返回计数器的值。
4. 代码实现
以下是基于上述设计的 Java 代码实现:
import java.util.HashSet;
import java.util.Set;
public class Main {
// 方法:判断从1到n之间的好数数量
public static int solution(int n) {
int count = 0; // 初始化计数器
// 遍历从1到n的所有数字
for (int i = 1; i <= n; i++) {
// 将数字转换为字符串
String numStr = Integer.toString(i);
// 使用集合记录不同的数位
Set<Character> digits = new HashSet<>();
for (char c : numStr.toCharArray()) {
digits.add(c);
}
// 判断集合的大小是否小于等于2
if (digits.size() <= 2) {
count++; // 满足条件的数字计数器加1
}
}
return count; // 返回好数的总数量
}
// 主方法:测试示例
public static void main(String[] args) {
// 测试用例
System.out.println(solution(110)); // 输出102
System.out.println(solution(1000)); // 输出352
System.out.println(solution(1)); // 输出1
}
}
5. 算法复杂度分析
时间复杂度
- 外层循环遍历从 1 到 ( n ),需要 ( O(n) ) 的时间。
- 对于每个数字,将其转换为字符串,并遍历其数位,时间复杂度为 ( O(d) ),其中 ( d ) 是数字的位数。对于最大数字 ( n ),其位数为 ( \log_{10}(n) )。
- 因此,整体时间复杂度为 ( O(n \cdot \log_{10}(n)) )。
空间复杂度
- 主要是存储数位的集合
Set,其大小最多为 10(数字的范围是 0 到 9)。 - 因此,空间复杂度为 ( O(1) )。
7. 优化思考
虽然上述暴力枚举方法直观且易于实现,但对于非常大的 ( n )(例如 ( 10^9 ))可能会超时。可以考虑以下优化方法:
1. 预计算和记忆化
- 利用动态规划或递归,预先计算出某个范围内“好数”的数量,避免重复计算。
- 通过存储中间结果,减少重复计算的开销。
2. 数位动态规划(Digit DP)
- 使用数位 DP 技术,通过逐位构造数字并统计“好数”的数量。
- 数位 DP 的主要思想是逐位分析数字,判断是否符合条件,同时利用状态压缩来减少计算。
- 数位 DP 的复杂度与数字的位数和状态空间有关,适合处理大范围问题,例如 ( n ) 接近 ( 10^9 )。