考过 GESP 一级的人都知道,B 开头的基础题里总有一两道“看起来像送分题”的坑。B4258 四舍五入就是其中之一。题目真的不难,输入一个浮点数,把它四舍五入成整数输出,可偏偏就是这个操作,让不少第一次上考场的同学栽了跟头。后来我去翻去年的通过数据,也发现这种题的错误率远比想象中高。原因倒不复杂:你脑子里想的是“四舍五入”这四个字,但到了代码里,你面对的是浮点数存储、类型转换、格式化输出这些平时不细想就很容易出错的东西。
这篇文章不打算只给你一个能过的代码。我会把这道题从题目背景、数学定义、常见实现、踩坑记录到后续等级里的变形全部拆开讲,尤其会解释为什么有些写法在本地跑得好好的,一提交就 WA。无论你是第一次备考一级,还是已经在准备更高级别,这篇文章都值得花十分钟慢慢看。
1. 题目解读与考点拆解
1.1 GESP 一级到底是什么水平
GESP 全称是 CCF 编程能力等级认证,由中国计算机学会主办,整体分成一到八级。一级属于最基础的认证等级,默认考生已经掌握了最基本的输入输出、变量定义、算术运算、if 分支、for 或 while 循环这些内容。换句话说,一级不会考复杂的算法,更不会考高深的数学建模,它主要验证你“能不能把题目描述翻译成代码”。
在实际考试中,GESP 一级的题目分布里,像 B4258 这种“基础题块”的题目占了相当大的比重。所谓 B 开头,在很多 OJ 平台上代表基础训练区的题目编号,后面跟的数字是题号。这类题往往不涉及某个牛掰的算法,而是把语言基础、边界条件、格式要求这些基本功放在一起考。四舍五入这道题就是一个典型:它看着简单,实际上一头连着数学规则,一头连着浮点数在计算机里的存储方式,中间还夹着输出格式这个很容易被忽略的扣分点。
1.2 题目真正在问什么
“四舍五入”的规则大家都会背:看小数部分的第一位,大于等于 5 就向前进一位,小于 5 就直接舍掉。题目通常会给你一个浮点数,比如 3.4、3.5、3.6,要求你输出对应的整数结果 3、4、4。
这里有个非常容易误判的地方:程序里的整数和数学里的整数不太一样。如果你直接用一个 double 变量接收输入,再像数学书里写的那样“看情况进位”,你会发现代码里根本没有“小数部分第一位”这个概念。你得自己想清楚,到底是通过类型转换截断小数,还是通过加减 0.5 来实现进位,或者干脆把输入当成字符串来处理。不同的处理方式,代码复杂度和出错概率差别很大。
1.3 这道题的隐藏考点
表面上看,四舍五入只考一个“见 5 进 1”的规则。但真正拉高分数的隐藏考点包括这几块:
第一,浮点数不等于数学上的小数。像 0.1、0.2 这种小数在内存里是无限循环的二进制近似值,不是精确值。虽然 0.5 这种值能被精确表示,但当你面对一个复杂的输入时,浮点误差可能会在边界处影响判断。
第二,C++ 里把浮点数强制转换成整数,采用的是截断而不是四舍五入。例如 (int)3.99 结果是 3,这一点和很多人直觉里“强转应该四舍五入”的预期完全相反。如果不理解这个差异,后面写出来的代码会非常诡异。
第三,输出格式。题目要求输出一个整数,那你就不能带小数点,也不能有多余的空格或字符。有些人用了 printf("%f") 结果输出一串小数,还有人输出后忘记换行,这些都属于格式问题,在 OJ 上照样会判错。理解了这三层考点,再回头看你写的代码,就会清楚自己到底踩在哪个坑里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实现思路:从数学规则到编程表达
2.1 四舍五入的数学定义
先回到数学上。对一个非负实数 n,四舍五入到整数可以写成这样:
code复制round(n) = floor(n + 0.5)
也就是说,给 n 加上 0.5,再做一次向下取整。举几个例子:
- n = 3.4,加 0.5 等于 3.9,向下取整是 3。
- n = 3.5,加 0.5 等于 4.0,向下取整是 4。
- n = 3.6,加 0.5 等于 4.1,向下取整是 4。
这个公式干净利落,也正是很多人第一时间写 int ans = (int)(n + 0.5) 的原因。这个写法在 n 是非负数的时候确实有效,因为它把“小数部分是否大于等于 0.5”的判断,转化成了“加 0.5 之后是否越过整数边界”。但要注意,这个转化依赖一个前提:n 是正数。一旦 n 变成负数,这个公式就不一定符合你想要的四舍五入规则了。
2.2 四种常见实现方法
我在教学过程中,见过考生用四种完全不同的方式实现这道题,它们各有各的特点:
| 实现方式 | 核心写法 | 正数情况 | 负数情况 | 精度风险 | 适用场景 |
|---|---|---|---|---|---|
| 加 0.5 再截断 | (int)(n + 0.5) |
正确 | 通常错误 | 中等 | 题目明确是正数时快速写 |
| 标准库 round | round(n) |
正确 | 正确 | 低 | C++11 及以上,推荐 |
| printf 格式化 | printf("%.0f", n) |
正确 | 正确 | 依赖舍入模式 | 只需要输出结果时 |
| 字符串解析 | 读入字符串,判断小数点后第一位 | 正确 | 需额外处理 | 无 | 需要绝对精确时 |
表里每一行单独拿出来都值得聊一聊。前三种是“数值路线”,关键依赖浮点数的精确性;第四种是“字符串路线”,完全绕开浮点数问题,这也是很多竞赛选手在数据严格时更偏爱的方式。
2.3 为什么推荐字符串解析法
从稳妥性的角度,我个人非常推荐字符串解析法,尤其是在 GESP 这种判题环境里。原因很简单:你不需要赌浮点数在边界值上会不会出问题。
很多同学的痛点是,用 double 读入 3.5,round 一下得到 4,觉得挺正常。但当你读入一个像 123456.5 这样的数,或者一个超出 int 范围的大数,再或者带符号的负数,数值路线的代码就要不断打补丁。而字符串解析法从头到尾没有浮点误差,小数点前的整数部分可以直接转成整数,小数点后的第一位拿出来比较大小,判断逻辑和数学定义完全一致。
GESP 一级的数据范围通常不会太大,但养成“能用整数运算就不用浮点运算”的习惯,对后续参加更高级别的考试非常有益。这不是什么高深技巧,这就是竞赛党的基本功。
3. 实操全过程:从读题到提交通过
3.1 答题环境与输入输出模板
GESP 考试无论是在线评测还是本地安装的考试客户端,解法思路都一致。你需要写一个完整的程序,从标准输入读取数据,然后把结果输出到标准输出。考场一般不允许你本地调试太多,所以代码必须一次写对。
先看 C++ 最简单的输入输出骨架:
cpp复制#include <iostream>
using namespace std;
int main() {
// 你的代码
return 0;
}
题目如果输入一个浮点数,你可以用 double n; cin >> n; 读入。但如果我打算用字符串解析法,那对应的读入语句是:
cpp复制string s;
cin >> s;
不要小看这一步。很多考生一看到“浮点数”三个字,条件反射就定义成 double,然后一路用浮点运算走下去。但实际上,题目只要求你输出一个整数,你完全可以把输入看作一串字符,自己来判断小数点在哪、第一位小数是多少。这样既避免了解析边界问题,也让代码在思路上更贴近题目描述。
3.2 完整 C++ 代码
下面这段是我比较推荐的一级考场写法,把正数、负数、整数输入、小数输入全部处理掉了:
cpp复制#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
cin >> s;
// 处理负号
bool neg = false;
if (s[0] == '-') {
neg = true;
s = s.substr(1);
}
// 找小数点
int dot = s.find('.');
if (dot == string::npos) {
// 没有小数点,直接转整数输出
int num = 0;
for (char c : s) {
num = num * 10 + (c - '0');
}
cout << (neg ? -num : num) << endl;
return 0;
}
// 取出整数部分
int num = 0;
for (int i = 0; i < dot; i++) {
num = num * 10 + (s[i] - '0');
}
// 小数点后第一位
int first = (dot + 1 < s.size()) ? (s[dot + 1] - '0') : 0;
// 大于等于5就进位
if (first >= 5) {
num++;
}
if (neg) {
num = -num;
}
cout << num << endl;
return 0;
}
这段代码的判定逻辑是:先把负号摘出来,确保后面只处理正数,这对负数的“四舍五入”规则更清晰。然后找小数点,如果输入本身就是整数,那就直接输出。如果带小数,就把小数点前的部分累加成一个整数,再判断小数点后第一位是否大于等于 5,决定要不要加一。这个逻辑和数学课本上的四舍五入完全一致。
如果你确认题目只给正数,并且不想写这么长,也可以用标准库版本的快速写法:
cpp复制#include <iostream>
#include <cmath>
using namespace std;
int main() {
double n;
cin >> n;
cout << round(n) << endl;
return 0;
}
注意要加 #include <cmath>,并且编译环境需要支持 C++11 或更高版本。GESP 的评测机一般是支持 C++17 的,所以这个写法能过。但如果你担心一些边界问题,还是推荐用前面的字符串解析版本。
3.3 测试用例设计
写完代码不是终点,测试才是。我每次都会让学生至少跑以下几组数据,每组都有不同的目的:
| 输入 | 预期输出 | 测试目的 |
|---|---|---|
| 3.4 | 3 | 常规向下舍去 |
| 3.5 | 4 | 边界进位 |
| 3.6 | 4 | 常规向上进位 |
| 0.4 | 0 | 小数部分小于 5 |
| 0.5 | 1 | 最小的边界进位 |
| 100 | 100 | 无小数情况 |
| 1.9 | 2 | 尽量靠近进位边界 |
| -1.5 | -2 | 负数边界 |
| -1.4 | -1 | 负数舍去 |
如果你用的是字符串解析代码,上面这些用例基本都能通过。如果你用的是 (int)(n + 0.5),-1.5 这组就会出事,因为 -1.5 加 0.5 等于 -1.0,截断后得到 -1,而标准四舍五入的常见理解是 -1.5 应当进到 -2。这也是为什么它是个经典陷阱。
3.4 提交与判题结果
在 OJ 上提交后,你可能会看到几种结果:
- AC(Accepted):代码通过所有测试点,这是唯一健康的结果。
- WA(Wrong Answer):结果有误,最可能的原因是边界值没处理对,比如负数、整数、格式末尾多了空格。
- RE(Runtime Error):运行时错误,这道题里一般不会出现,但如果字符串越界访问则可能报 RE。
- TLE(Time Limit Exceeded):超时。这道题完全不可能超时,除非你写了个死循环。
真到了考场上,遇到 WA 不要慌。先把代码拿回本地,把上面那张测试用例表跑一遍,基本能定位 90% 的问题。剩下的可能出在编译参数或者输入格式上,需要再仔细阅读题目说明。
4. 踩坑实录与常见问题速查
4.1 为什么加 0.5 会出现奇怪的答案
这个坑我在不同学生身上见过无数次。(int)(n + 0.5) 对正数没问题,但有两个隐患:
第一,负数的情况。前面已经说过,-1.5 加 0.5 是 -1.0,强转后是 -1,不是 -2。如果你预期的是“负数也按照远离零的方向进位”,那这段代码就是错的。
第二,大数的情况。如果 n 是很大的浮点数,比如 2147483647.5,加 0.5 之后变成 2147483648.0,这个值已经超出了 int 的最大范围。把它强转成 int 属于未定义行为,实际执行结果可能是负数,也可能是其他乱七八糟的值。GESP 一级的数据可能不会那么大,但平时如果是为了稳,早晚会碰到。
所以我的建议是:不要为了省那几行代码去赌“数据不会刁钻”。参赛这件事,稳比快重要。
4.2 负数到底怎么四舍五入
关于负数,很多初学者一开始是懵的。你看数学课本,四舍五入说的是“小数点后第一位”,但并没有说负数应该往哪个方向“进”。
实际上,不同编程语言、不同数学教材给出的规则并不完全一致。C 标准库的 round() 函数,采取的是“远离零”的舍入方式,也就是 -1.5 会变成 -2,-2.5 会变成 -3。而 Python 里的 round() 默认是“银行家舍入”,-1.5 会变成 -2,但 -0.5 会变成 0,这又和 C 不一样。
GESP 一级如果出现负数,通常题目会写清楚规则,或者评测数据本身不会把负数卡得太死。但从应试角度看,最稳妥的办法是像我在前面字符串解析代码里做的那样:先处理负号,再对正数部分做四舍五入,最后把符号加回去。这样做出来的结果符合“远离零”的规则,也最贴近大家日常手工计算的习惯。
4.3 浮点精度带来的连锁反应
有同学问:我用 double 读入 1.1,然后打印出来怎么成了 1.1000000000000001?这就是浮点数的经典问题。1.1 在二进制里是一个无限循环小数,double 只能存一个近似值。
如果题目数据没有给边界值,浮点误差往往不影响四舍五入的最终结果。但一旦测试点故意放了一个 1.4999999999999999 之类的数,你心里设想的“1.5 进位”就可能不成立。这种极小概率的事件,确实有人遇到过。
所以,处理“四舍五入”这类对边界敏感的题目,字符串解析法是最清净的。它从一开始就不屑于和浮点误差打交道,直接把字符一位一位拿出来判断,结果一定是确定性的。这也是我在文中最推荐的方法。
4.4 常见问题速查表
| 问题 | 原因 | 解决方案 |
|---|---|---|
(int)(n + 0.5) 对负数结果不对 |
负数加 0.5 后截断方向不对 | 先处理负号,或改用 round() |
| 输出带了 .0 或多余空格 | 使用了 %f 或习惯性输出格式不对 |
输出整数,用 cout << ans 或 printf("%d") |
| 输入“12”时程序崩溃 | 代码没考虑没有小数点的情况 | 先 find('.'),无小数点直接转整数 |
本地编译报错 round 不存在 |
编译器标准低于 C++11 | 加 #include <cmath> 并设置 C++11 及以上 |
| 输出结果与实际手算结果差 1 | 把小数部分大于 5 判断成大于等于 5 | 边界条件是 >= 5,不是 > 5 |
| 大数输入时结果乱码 | int 溢出 |
使用 long long 或字符串解析 |
这张表里的最后一条值得多说一句:如果题目数据范围很大,比如 n 可能到 10000000000.5,int 会溢出。建议直接把整数部分累加变量设成 long long。字符串解析代码里把 int num 改成 long long num,其他地方几乎不用动。
5. 一点备考经验与周边延伸
5.1 考场上先看数据范围再选方法
很多同学拿到四舍五入这道题,第一反应就是赶紧写代码。但我建议你先读清楚题目里的数据范围。如果题目明确写着“输入为正浮点数,且只有一位小数”,那你可以选最简单的方式。如果题目没写,或者给了负数和较大的范围,那就老老实实用字符串解析法。
考场上的时间很宝贵,但花了 30 秒审题,可以省下 10 分钟调试。这个习惯一定得养成。
另外,输出的时候记得换行。一些 OJ 不强求最后换行,但 GESP 的评测系统往往要求输出格式严格控制。我会建议无论什么题目,最后都输出一个 endl 或者 \n,这是最保险的。
5.2 从一级到更高级别,四舍五入会怎么变
四舍五入这个考点不会只出现在一级。到了 GESP 二级,题目可能会让你“把结果四舍五入保留两位小数输出”,这时候你就得会用 printf("%.2f", x),或者掌握 iomanip 里的 fixed 和 setprecision。这些本质上都是四舍五入在输出格式上的变形。
到了三级、四级,四舍五入可能不再作为一个单独考点出现,而是嵌在更复杂的问题里。比如浮点数二分、几何计算、动态规划中的小数处理,都会涉及到“最终结果保留几位小数”的要求。这时候,你在一级养成的“优先用整数思维处理小数”的习惯,会帮你避开很多精度坑。
我在准备更高级别题目时,也常遇到像“小猫分鱼 GESP”“巧夺大奖”这种经典题目,它们看起来跟四舍五入不搭边,但内部隐含着同样的道理:把问题从浮点计算转化成整数计算,一切都会变得清爽很多。所以这道一级题,本质上是给你打地基的。
5.3 从这道题延伸出来的一个自测练习
如果你想把这道题吃透,可以试着做一个小扩展:输入任意浮点数,要求四舍五入保留一位小数输出。你能用字符串解析法写出来吗?写完之后,再试试换成保留两位小数,又该怎么改?
这两个小练习看着不起眼,但能检验你是不是真的理解了小数点和字符串的关系。我见过很多学生,做原题没问题,稍微变一下就卡住,原因就是他没有把“四舍五入”从“一个数学规则”升级成“一套可以灵活组合的字符串处理逻辑”。
如果让我给这道题一个定位,我会把它当成 GESP 入门路上的体温计。它不会直接决定你考不考得过,但它能很快告诉你,你对基础类型、输入输出、边界条件的掌握到底是不是真的稳。只要你自己动手把这道题的每一种写法都跑一遍,后面再遇到字符串、浮点数、精度,心里就有底了。
