这类编程练习题最值得先看的不是题目描述,而是它到底在考察什么、新手容易在哪里卡住、以及怎么写出既正确又清晰的代码。“与7无关的数”是一个经典的入门级筛选与求和问题,它考察的是对整数基本操作、条件判断和循环的掌握。很多初学者一看题目觉得简单,但实际写起来,可能在“与7无关”的条件理解、边界处理,或者求和累加的逻辑上出岔子。这篇文章就从一个写过很多类似代码的角度,拆解这道题,让你不仅能做出答案,更能理解这类问题的通用解决思路和避坑要点。
1. 先拆解“与7无关”到底是什么意思
题目通常这样描述:一个正整数,如果它能被7整除,或者它的十进制表示法中某一位上的数字为7,则称这个数为“与7有关”。反之,则是“与7无关”。现在给定一个范围(比如n < 100),求这个范围内所有“与7无关”的正整数的平方和。
1.1 核心条件判断:三个子条件必须全部不满足
“与7无关”是“与7有关”的反面。所以,我们的核心是判断一个数是否“与7有关”。如果一个数满足以下任意一个条件,它就是有关的:
- 能被7整除:即
i % 7 == 0。 - 个位是7:即
i % 10 == 7。 - 十位是7:即
(i / 10) % 10 == 7。对于n<100的情况,十位就是i / 10。
只有当这三个条件全部不满足时,这个数才是“与7无关”的。很多新手会在这里逻辑混淆,错误地用“且”去连接条件。记住,判断“有关”用“或”,判断“无关”则需要对“有关”的条件取反,或者直接用“且”连接三个不满足的条件。
1.2 为什么是“平方和”而不是简单求和
题目要求的是平方和,这增加了两个考察点:
- 计算:需要在循环体内累加
i * i,而不是i。 - 数据范围:平方和增长很快。例如,当n=99时,最大的无关数可能是96,其平方为9216。100个这样的数平方和可能会超过
int型变量的表示范围(约21亿)。因此,使用long long类型来存储结果通常是更安全的选择,这是一个非常实际的工程细节。
1.3 输入范围与循环边界
题目一般会给出一个上限n(1 <= n < 100)。我们需要处理的是从1到n(包括n)的所有整数。循环的写法通常是for(int i = 1; i <= n; ++i)。这里要注意是i <= n还是i < n,必须根据题意明确。对于“小于n”的描述,就是i < n。
2. 从零开始的完整实现步骤与代码
我们假设题目输入是一个整数n,输出是1到n之间所有与7无关的正整数的平方和。下面我按实际写代码和测试的顺序,一步步来。
2.1 环境与框架准备
你可以在任何支持C/C++的环境里写这道题,比如:
- 本地IDE:Visual Studio, Code::Blocks, Dev-C++等。
- 在线编译器:很多OJ(在线判题系统)都自带。
- 命令行:用g++或clang编译。
代码结构就是一个简单的控制台程序。我建议先在一个单独的.cpp文件里写,方便编译和测试。
2.2 第一种写法:清晰的条件判断
这是最直观的写法,直接翻译题目逻辑。
#include <iostream> using namespace std; int main() { int n; cin >> n; // 读入范围上限n long long sum = 0; // 使用long long防止平方和溢出 for (int i = 1; i <= n; ++i) { // 判断是否与7有关 bool related = false; if (i % 7 == 0) { // 条件1:能被7整除 related = true; } else if (i % 10 == 7) { // 条件2:个位是7 related = true; } else if ((i / 10) % 10 == 7) { // 条件3:十位是7 related = true; } // 如果与7无关,则累加其平方 if (!related) { sum += (long long)i * i; // 注意这里也转为long long计算 } } cout << sum << endl; return 0; }为什么这么写?
- 用一个布尔变量
related来标记,逻辑清晰。 - 使用
else if是因为一旦确定有关,就不需要再判断后续条件,效率稍高。 - 在累加时,将
i强制转换为long long再相乘,是为了避免两个int相乘可能溢出(虽然本题n<100不太会,但这是好习惯)。
2.3 第二种写法:简化的条件表达式
也可以将判断条件写在一个if语句里,更紧凑。
#include <iostream> using namespace std; int main() { int n; cin >> n; long long sum = 0; for (int i = 1; i <= n; ++i) { // 判断是否与7无关:同时不满足三个条件 if ( (i % 7 != 0) && (i % 10 != 7) && ((i / 10) % 10 != 7) ) { sum += (long long)i * i; } } cout << sum << endl; return 0; }这种写法直接判断“无关”,条件是用“且”连接的三个“不等于”。我个人更推荐新手先用第一种,逻辑更直白;熟练后可以用第二种,代码更简洁。
2.4 验证与测试:自己当一回判题机
写完代码不要直接提交,先自己用几个例子验一下。
测试用例设计:
- 小范围验证 (n=10):
- 与7有关的数:7(被7整除)。
- 与7无关的数:1, 2, 3, 4, 5, 6, 8, 9, 10。
- 平方和 = 1+4+9+16+25+36+64+81+100 = 336。
- 运行程序,输入10,看输出是否为336。
- 包含个位和十位是7的数 (n=20):
- 与7有关的数:7, 14, 17。
- 可以手动计算或信任程序。用你的程序跑一遍,再心算几个关键数验证。
- 边界测试 (n=1, n=6):
- n=1时,输出应为1。
- n=6时,输出应为1+4+9+16+25+36=91。
- 稍大的数 (n=30):
- 检查27(个位是7?不,是十位?2不是7,个位是7?等等,27的个位是7!所以27是有关的)。这个例子能帮你确认条件判断是否正确。
在本地运行,输入这些测试用例,对比输出。如果都对了,你的代码逻辑基本就稳了。
3. 深入讨论:如果n的范围扩大怎么办?
原题n<100,所以我们只检查了十位。这是一个重要的边界限制。如果题目没有明确限制n的范围,或者n可以很大(比如n<10000),那么“某一位上是7”这个条件就需要通用化处理。
3.1 通用化的数字位检查算法
我们不能只写死检查十位和个位。需要一个循环来分解整数的每一位。
bool isRelatedToSeven(int num) { // 条件1:能被7整除 if (num % 7 == 0) { return true; } // 条件2:任何一位数字等于7 int temp = num; while (temp > 0) { if (temp % 10 == 7) { // 检查当前个位 return true; } temp /= 10; // 去掉个位 } return false; // 两个条件都不满足,则无关 }在主循环中,调用这个函数:
for (int i = 1; i <= n; ++i) { if (!isRelatedToSeven(i)) { sum += (long long)i * i; } }为什么这样写更好?
- 可扩展性:无论n是几位数,算法都适用。
- 清晰性:将核心判断逻辑封装成函数,主函数更简洁。
- 可维护性:如果判断规则变化,只需修改这个函数。
3.2 思考:性能与优化
对于n<100,两种方法性能无差异。但如果n极大(例如上亿),我们需要考虑:
- 提前终止:上面的函数一旦发现某位是7就返回,避免了不必要的循环。
- 避免重复计算:
i * i可以计算一次存起来。 - 数学方法:本题可能存在更巧妙的数学规律来批量计算“无关数”的平方和,但这超出了入门练习的范畴。对于算法题,优先保证正确性和清晰性。
注意:在入门阶段,不要过早追求极致的优化。先把逻辑写对、写清楚,这是最重要的。
4. 常见错误与排查清单
很多同学提交代码后遇到“答案错误”、“运行超时”或“格式错误”,问题往往出在细节上。下面是我总结的排查顺序:
4.1 答案错误(Wrong Answer)
这是最常出现的。按以下顺序检查:
- 条件逻辑:检查if语句的逻辑运算符。是“或”还是“且”?判断“有关”还是“无关”?最经典的错误就是把判断“无关”的条件
(i%7!=0 && i%10!=7 && (i/10)%10!=7)错写成(i%7!=0 || i%10!=7 || (i/10)%10!=7)。 - 循环边界:确认是
i <= n还是i < n。仔细读题! - 求和变量初始化:
sum是否初始化为0了?未初始化的变量值随机,会导致结果不可预测。 - 整数溢出:
sum是否用了int?当n较大时,平方和很可能超过21亿。改成long long。 - 中间计算溢出:即使
sum是long long,i * i也可能是两个int相乘,在赋值给sum前就已经溢出了。确保计算时类型足够大,如sum += (long long)i * i。 - 输入输出格式:题目要求输出后换行吗?你的
cout << sum;后面有没有加<< endl或\n?
4.2 运行超时(Time Limit Exceeded)
本题n<100,几乎不可能超时。如果超时,99%是代码陷入了死循环。
- 检查循环变量:在
for循环里,你是否错误地修改了循环变量i? - 通用化算法检查:如果你写了通用位检查的
while循环,确保循环条件(temp > 0)正确,并且循环体内有temp /= 10来改变temp的值。
4.3 格式错误(Presentation Error)
通常是因为输出格式与题目要求不完全一致。
- 多输出或少输出:是否只输出了一个结果?有没有多输出提示语句如
“请输入n:”?OJ只接受纯结果数据。 - 空格与换行:结果是单独一行吗?末尾有换行符吗?在大多数OJ中,末尾有无换行符不影响判题,但最好养成输出后换行的习惯 (
cout << sum << endl;)。
5. 举一反三:同类问题的解决模式
“与7无关的数”是一个代表,有一类问题都可以用类似的模式解决。掌握这个模式,你就能快速解决它们。
5.1 问题模式识别
这类问题的共性:给定一个整数范围,按照某种规则筛选出符合条件的数,然后对它们进行某种运算(求和、平方和、计数、求平均值等)。
规则可能包括:
- 数字特性:某位是几、是回文数、是水仙花数、数字之和等。
- 整除特性:能被某个数整除、不能被某个数整除。
- 组合条件:像本题一样,多个条件的组合。
5.2 通用解决框架
- 读入数据:确定范围
n,有时可能是区间[a, b]。 - 初始化结果变量:根据运算是求和、计数还是其他,初始化
sum=0,count=0等。注意数据类型。 - 遍历范围:使用
for循环遍历每个待检查的数i。 - 条件判断:编写一个函数或一段内联代码,判断
i是否满足条件。这是核心。- 如果判断逻辑复杂,强烈建议封装成独立函数,如
bool check(int num)。 - 判断时,注意提取数字的各位(通过
%10和/10循环)。
- 如果判断逻辑复杂,强烈建议封装成独立函数,如
- 累积结果:如果满足条件,就更新结果变量(
sum += i*i,count++)。 - 输出结果:按格式要求输出。
5.3 变式练习
你可以用这个框架尝试解决以下问题,巩固技能:
- 求水仙花数:一个三位数,其各位数字立方和等于该数本身。输出所有水仙花数。
- 求素数之和:求给定区间内所有素数的和。
- 数字统计:统计1到n中,数字k(0-9)出现了多少次。
- 特殊数的和:求1到n中,既能被3整除又能被5整除的数的和。
解决这些问题时,唯一需要变化的就是第4步的“条件判断”函数。遍历和累加的框架是完全一样的。
6. 写给新手的最后几点建议
我见过很多初学者在这类题目上反复出错,不是因为算法难,而是因为一些习惯没养成。
第一,先画流程图或写伪代码,再敲键盘。尤其是条件判断部分,用纸笔列出几个测试数(如7, 14, 17, 27, 71),手动走一遍你的判断逻辑。这能避免最致命的逻辑错误。
第二,重视测试。不要只相信样例输入输出。像第2.4节那样,自己设计几个关键的、边界的测试用例。包括最小的n、最大的n、包含各种“有关”情况的n。自己当自己的第一道关卡。
第三,理解大于记忆。不要只背下这道题的代码。要理解“遍历-判断-累加”这个模式,理解如何用%和/来获取数字的每一位。这样下次遇到“与5无关”或者“包含数字3”的题目,你就能自己写出来。
第四,注意数据类型是基本功。int和long long的选择,中间计算会不会溢出,这些问题在简单题里可能不暴露,但在复杂题目里就是致命的。从一开始就养成根据数据范围选择类型的习惯。
这道题本身不难,但它像一块很好的试金石,能检验你对循环、条件、基本运算和整数处理是否真的掌握了。把它吃透,这一类“数字筛选计算”题你就有了一个可靠的解题模板。