1. 题目背景与需求分析
洛谷P1980是一道来自NOIP2013普及组的经典计数问题。题目要求统计在1到n的所有整数中,数字x(0≤x≤9)出现的次数。这类问题在编程竞赛中非常常见,考察选手对数字处理、循环控制和边界条件的把握能力。
实际应用场景包括:
- 数据分析中的数字频率统计
- 密码学中的数字分布研究
- 商业统计中的销售数字分析
注意:题目中n的范围是1≤n≤1,000,000,这意味着算法的时间复杂度必须控制在O(n)以内,否则会因数据量过大导致超时。
2. 解题思路解析
2.1 暴力枚举法(基础解法)
最直观的解法是遍历1到n的每个数字,然后逐位检查是否等于x:
cpp复制int count = 0;
for (int i = 1; i <= n; i++) {
int num = i;
while (num > 0) {
if (num % 10 == x) count++;
num /= 10;
}
}
时间复杂度分析:
- 外层循环n次
- 内层循环取决于数字位数,最大为7次(1,000,000是7位数)
- 总体复杂度O(7n),在n=1e6时约7e6次操作,可以接受
2.2 数学规律法(优化解法)
更高效的解法是利用数字的数学规律,逐位计算x在每个数位上出现的次数。这种方法的时间复杂度是O(logn),适合处理更大的n值。
核心思路:
- 将数字拆分为个位、十位、百位等
- 对每一位,计算x在该位上出现的次数
- 累加所有位上的出现次数
3. 完整代码实现与注释
3.1 暴力解法实现
cpp复制#include <iostream>
using namespace std;
int main() {
int n, x;
cin >> n >> x;
int count = 0;
for (int i = 1; i <= n; i++) {
int num = i;
while (num > 0) {
if (num % 10 == x) count++;
num /= 10;
}
}
cout << count << endl;
return 0;
}
3.2 数学规律解法实现
cpp复制#include <iostream>
#include <cmath>
using namespace std;
int countDigit(int n, int x) {
int cnt = 0, k;
for (int i = 1; (k = n / i) != 0; i *= 10) {
int high = k / 10;
int current = k % 10;
int low = n % i;
if (x == 0) {
if (high) high--;
else break;
}
cnt += high * i;
if (current > x) cnt += i;
else if (current == x) cnt += low + 1;
}
return cnt;
}
int main() {
int n, x;
cin >> n >> x;
cout << countDigit(n, x) << endl;
return 0;
}
4. 关键点解析与注意事项
4.1 边界条件处理
-
当x=0时的特殊处理:
- 0不能出现在数字的最高位
- 需要单独处理高位计算
-
n=0的情况:
- 题目保证n≥1,但实际编程中仍需考虑
-
x的有效范围:
- 确保x在0-9之间
4.2 性能优化技巧
-
循环终止条件:
- 在数学解法中,当k=0时及时终止循环
-
变量复用:
- 尽量减少不必要的变量声明
-
位运算优化:
- 可以用位运算代替部分乘除法
5. 测试用例与验证
5.1 典型测试用例
| 输入(n, x) | 预期输出 | 测试目的 |
|---|---|---|
| 10 1 | 2 | 基本功能 |
| 100 0 | 11 | 0的特殊处理 |
| 1 1 | 1 | 最小值测试 |
| 999999 9 | 600000 | 最大值测试 |
5.2 调试技巧
-
打印中间结果:
- 在数学解法中打印每位计算结果
-
小规模测试:
- 先用小n值验证正确性
-
对比测试:
- 用暴力解法验证优化解法的正确性
6. 算法扩展与变种
6.1 统计多个数字的出现次数
如果需要统计0-9所有数字的出现次数,可以:
- 使用数组存储各数字计数
- 一次遍历完成所有统计
6.2 更大范围的数字统计
当n超过1e9时:
- 暴力解法不再适用
- 必须使用数学规律解法
- 可能需要处理大数问题
6.3 其他进制下的统计
将算法扩展为统计其他进制(如二进制、十六进制)下某数字的出现次数:
- 将基数10改为其他基数
- 调整位权计算方式
7. 常见错误与解决方法
-
死循环问题:
- 原因:循环条件错误
- 解决:检查while循环终止条件
-
结果偏小:
- 原因:边界条件处理不当
- 解决:仔细检查x=0和最高位的处理
-
时间超限:
- 原因:使用了低效算法
- 解决:改用数学规律解法
8. 实际应用中的优化建议
-
预处理技术:
- 对固定范围的查询可以预处理结果
-
记忆化搜索:
- 缓存已计算结果避免重复计算
-
并行计算:
- 对极大范围的统计可采用并行算法
9. 不同语言的实现差异
9.1 Python实现特点
python复制def count_digit(n, x):
cnt = 0
for i in range(1, n+1):
cnt += str(i).count(str(x))
return cnt
优势:
- 代码简洁
- 内置字符串操作
劣势:
- 性能较差
- 大数处理效率低
9.2 Java实现注意事项
java复制import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int x = sc.nextInt();
int count = 0;
for (int i = 1; i <= n; i++) {
int num = i;
while (num > 0) {
if (num % 10 == x) count++;
num /= 10;
}
}
System.out.println(count);
}
}
注意点:
- 输入输出处理较慢
- 需要关闭Scanner
- 使用StringBuilder优化字符串操作
10. 性能对比与算法选择
| 方法 | 时间复杂度 | 适用场景 | 实现难度 |
|---|---|---|---|
| 暴力枚举 | O(nlogn) | 小规模数据(n<1e6) | 简单 |
| 数学规律 | O(logn) | 大规模数据(n≥1e6) | 中等 |
| 预处理 | O(1)查询 | 多次查询固定范围 | 复杂 |
选择建议:
- 比赛场景:优先考虑数学规律解法
- 教学场景:从暴力解法入手理解问题
- 工程场景:根据实际数据规模选择