ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

信息学奥赛2067:从圆的周长面积学透C++浮点数与格式化输出

信息学奥赛2067:从圆的周长面积学透C++浮点数与格式化输出 1. 从“求圆的周长面积”看信息学入门的第一道分水岭刚接触信息学奥赛的同学十有八九都会撞上2067这道题。它出现在《信息学奥赛一本通》第二章“顺序结构”里名字朴素得不能再朴素——【例2.5】圆。很多同学扫一眼题目觉得“不就是输入半径、输出周长和面积吗数学课上算过八百遍了”然后噼里啪啦敲完代码一提交WA答案错误整个人懵在原地。我带队训练这些年几乎每届新生都会在这道题上栽一轮。它真正的考察重点根本不是圆的计算公式——公式谁都会——而是信息学竞赛里最基础也最要命的三件事浮点数的精度处理、格式化输出、以及代码的严谨性。这三件事恰恰是“学过编程”和“能打竞赛”之间的第一道分水岭。这篇东西我打算把2067从头到尾拆开揉碎了讲一遍包括正确写法、错误写法、以及为什么有些写法在OJ上会挂掉。无论你是刚把Dev-C装好的纯新手还是已经在刷一本通但卡在浮点输出上的同学这篇文章都值得你静下心看完。后面我还会聊到从这道题延伸出去的一些学习路线问题——包括不少人在群里问过的“弗洛伊德算法和2067有什么关系”这种困惑。2. 题干拆解题目到底让你干什么又留了哪些坑2.1 原始题意与输入输出要求先还原一下题目的本来面目。2067的完整描述大致是这样的输入圆的半径 r输出圆的直径 d、周长 c、面积 s。要求每个数据占一行保留 4 位小数。题目里给出的圆周率 π 取值为3.14159。看起来清爽利落。输入样例是1.5输出样例是三行数字分别是直径、周长和面积的结果都精确到小数点后四位。这里第一件值得注意的事题目明确给了 π 的取值。3.14159不是3.14也不是3.141592653589793。这个细节看似不起眼实际上是整道题第一个隐藏考点——你打算怎么在代码里使用这个 π2.2 竞赛题和数学题的思维差异在数学卷子上做这道题你写“因为r1.5所以直径3.0000周长9.4248面积7.0686”就完事了。但在OJ上计算机不会自动帮你理解“保留四位小数”是什么意思——它只会按你代码里的指令去截断或四舍五入。数学题考的是计算信息学题考的是表达。同一个数学结论用什么数据类型存、用什么格式输出、中间过程会不会损失精度决定了你是AC还是WA。这就是为什么我一直跟学生说刷一本通不要把它当数学练习册刷要把它当“用代码精确表达逻辑”的训练场。2.3 一个容易忽略的前提r 的数据类型题目的输入是一个实数——样例是1.5但测试数据里可能出现整数比如2也可能出现带很多位小数的数比如3.14159。所以读入变量必须用浮点类型不能用 int。这个看似废话但每年都有同学在变量声明上翻车。我在训练时经常让学生做一个实验把半径定义成 int输入2.5看看程序读进来的是什么。2。因为 int 直接把小数部分截断了。后面的所有计算都基于这个被截断的错误值结果自然全错。这不是什么高深的技术问题纯粹是数据类型意识不到位。提示在信息学竞赛中“读入数据的类型必须能完整容纳输入数据的取值范围和精度”是铁律。看到一个“实数”描述第一反应就该是 double至少是 float。3. π 的三种写法、两种隐患与一条铁律3.1 写法一直接往公式里填 3.14159很多新手的第一版代码长这样#include cstdio int main() { double r; scanf(%lf, r); printf(%.4lf\n, 2 * r); printf(%.4lf\n, 2 * 3.14159 * r); printf(%.4lf\n, 3.14159 * r * r); return 0; }这段代码能不能过在2067这道题上大概率能过。因为题目给定的 π 本来就是3.14159你直接字面量怼进去误差恰好为零。但我不建议你养成这个习惯。原因有两个第一代码可读性差。圆周率写成魔法数字magic number散落在公式里别人看你代码时不知道3.14159是什么过两周你自己看也未必反应得过来。第二换一道题就废了。很多题不会明确给你π的取值只告诉你“精确到小数点后N位”。这时候如果你写3.14159精度就不够用了。比如后面学到三角函数、几何计算、数学推导类题目时3.14159和真正的π之间的误差会被放大到肉眼可见。3.2 写法二定义常量 double PI 3.14159#include cstdio int main() { const double PI 3.14159; double r; scanf(%lf, r); printf(%.4lf\n, 2 * r); printf(%.4lf\n, 2 * PI * r); printf(%.4lf\n, PI * r * r); return 0; }这种写法比魔法数字好很多——语义清晰改起来也方便。如果题目给的π是3.14159265你只需要改一行const double PI后面所有公式不用动。在刷一本通阶段我推荐你用这种写法它起步简单、思维直观而且完全够用。3.3 写法三高精度π的获取方式与适用场景还有人用acos(-1.0)来获取π#include cstdio #include cmath int main() { const double PI acos(-1.0); double r; scanf(%lf, r); printf(%.4lf\n, 2 * r); printf(%.4lf\n, 2 * PI * r); printf(%.4lf\n, PI * r * r); return 0; }acos(-1.0)求的是反余弦函数在-1处的值数学上恰好等于π。这是竞赛选手很常用的一种π的获取方式因为double能表示的最精确的π用这个函数直接得到不需要你去背那一长串小数。在2067上这种写法也能过——因为精确的π保留四位小数后四舍五入下来恰好也是3.1416级别和3.14159在%0.4lf输出时经常一致。极端情况下可能差最后一位的1但在这道题的测试数据内没有暴露问题。不过我得提醒一句如果题目明确给了π的取值就按题目的来。不要自作聪明用acos(-1.0)也不要自作主张用3.14159265358979。竞赛的一条原则是“题目说用什么就用什么”——题目说的是3.14159那你就算用精确π能过也只是运气好没踩到那个微小的输出差异而已。3.4 三种写法对比写法优点缺点适用场景直接写3.14159简单、无额外开销可读性差、难维护仅限本题const double PI清晰、好改仍需人工核对小数位一本通绝大多数题目acos(-1.0)精度最高、无需记忆需include cmath、给人感觉“过度设计”三角函数、浮点精度敏感的后续题目从长远看我建议你从第一道题就养成“定义常量”的习惯。这不只是2067的问题而是整个竞赛生涯代码风格的地基。4. 浮点类型的选型float 还是 double以及一个经典翻车现场4.1 精度对比float 约 7 位有效数字double 约 15 位C/C里表示小数的基本类型有两种float单精度和double双精度。float占4字节double占8字节。float大约能保证7位有效数字的精确度double大约能保证15位有效数字的精确度。“有效数字”这个概念很多人没搞懂。不是说小数点后几位而是从第一个非零数字开始算的总位数。比如123456.7是7位有效数字0.0001234567也是7位有效数字。在2067这道题里输入半径、输出四位小数中间涉及乘法运算。如果r很大比如10000那么面积等于π×10^8大约9位数。用float的话第8位以后就开始不准确了用double则毫无压力。所以浮点计算一律优先double这是竞赛里的默认选择。4.2 一个真实的翻车现场float 计算面积差 0.0001我见过一个学生的代码全程float样例输入1.5时输出完全正确但OJ一提交就WA。他跑来问我说“是不是OJ有病”。我让他把输入改成100000再跑一遍他盯着屏幕上那个“31415900000.0000”愣了半天——正确结果应该是31415926535.8979。float的有效数字就这么点位数一大直接失真。这里其实暴露了一个好习惯的价值不能只在样例上验证程序。样例过了不代表程序对了OJ的测试数据里永远有你没想到的边界情况。训练自己做题时要主动构造几组极端输入来测试——这叫“自测”是竞赛选手的基本素养。注意几乎所有OJ题目都有隐藏的测试点。样例只是给你看的“最低保障”不是全部。过样例只能说明程序能跑不能说明程序正确。5. 格式化输出printf 的 %.4lf 与 cout 的 setprecision 之争5.1 printf 方案精确、可控、老派但可靠使用 printf 输出保留四位小数的格式如下printf(%.4lf\n, diameter);%.4lf的意思是以 doublelf格式输出保留小数点后4位.4。这几乎是竞赛里最标准、最老练的输出方式。它的好处是精确控制格式不拖泥带水——你写4位就是4位不会多不会少。很多从C语言教材入门、或者跟着学校老师学C的同学更习惯printf。因为它直白格式控制串里写什么输出就是什么样。5.2 cout 方案流式输出慎用 setprecision用cout也可以#include iostream #include iomanip using namespace std; int main() { double r; cin r; cout fixed setprecision(4) 2 * r endl; cout fixed setprecision(4) 2 * 3.14159 * r endl; cout fixed setprecision(4) 3.14159 * r * r endl; return 0; }这里面关键的三个元素fixed让输出使用定点表示法也就是“小数形式”而不是科学计数法。没有fixed的话很大的数会变成3.14159e04这种直接格式爆炸。setprecision(4)设置精度为4位。配合fixed就是小数点后4位。iomanipsetprecision 所在的标准库头文件忘了include会编译报错。这里有个经典的坑setprecision如果不配合fixed表示的是有效数字位数而不是小数位数。比如setprecision(5)输出123.456时结果可能是123.465位有效数字而不是123.45600。很多新手在这里摔得鼻青脸肿——当时样例输出看着对一换数据就乱了其实是没理解有效数字和定点小数的区别。5.3 为何竞赛圈更偏爱 printf信息学竞赛里有一个心照不宣的事实用printf的选手比用cout的多得多。原因有三第一是格式控制直观。%.4lf写出来你需要几位小数一目了然。而cout的fixedsetprecision组合需要记两条规则忘一条就出问题。第二是性能略优。cin/cout在默认设置下比scanf/printf慢不少——尤其是大量输入输出时。虽然2067这种题就三次输入三次输出差距完全可以忽略但从一开始养成用scanf/printf的习惯后面做大数据题时能少踩很多坑。第三是混用不易出错。有些同学既想用cin读入、又想用printf输出中间还要加上ios::sync_with_stdio(false)才能避免缓冲不同步导致的问题。这个知识点太容易忘了忘了就是各种莫名奇妙的错误。当然我也不是说要彻底不用cin/cout。如果你的老师、教材统一用iostream风格你也用熟了那就坚持用。关键不是用哪套而是搞清楚每套的规则。最怕的是两套都学了个半吊子到时候该加fixed不记得该加头文件不记得报错都不知道去哪查。6. 代码实现一版可直接照抄的完整参考以及每一行的理由6.1 参考代码#include cstdio using namespace std; int main() { const double PI 3.14159; double r; scanf(%lf, r); double d 2 * r; double c 2 * PI * r; double s PI * r * r; printf(%.4lf\n, d); printf(%.4lf\n, c); printf(%.4lf\n, s); return 0; }6.2 逐行拆解为什么要这么写第2行using namespace std;很多人不理解为什么有这行。因为scanf、printf这些函数定义在C的标准命名空间std里。不过由于cstdio里的函数其实是C语言的遗产在不写using namespace std;时直接用也通常不会报错——很多编译器都兼容这种写法。但为了严谨也为了避免以后用cin、cout、string这些确实需要std的东西时出问题从一开始就写上这行省得后面东加一行西加一行。第4行const double PI 3.14159;这是全程序最关键的“好习惯”体现。把π定义成常量比散落在公式里的3.14159好维护得多。第6行scanf(%lf, r);读入double类型必须用%lf——注意是l小写字母L和f的组合不是数字1和f也不是%f。%f在scanf里对应的是float指针用在double上会导致读取字节数不匹配程序直接行为诡异。提示一个经典低级错误就是把%lf写成%f。在scanf里%f读4个字节进float%lf读8个字节进double。写错的话数据大概率读成垃圾值输出自然全乱。第8~10行两个赋值语句先把计算中间结果存进变量再统一输出。有些同学喜欢直接写printf(%.4lf\n, 2 * r)不另存变量。这在功能上没问题但可读性差一点。我建议先存后输出一是方便调试中途打印d、c、s看看二是代码结构更清晰——计算结果是什么、输出的是什么一目了然。第12~14行三次printf每个输出占一行\n保留四位小数%.4lf。注意格式控制串里\n不要漏漏了就所有输出挤在一行格式必挂。第16行return 0;主函数返回0表示程序正常结束。虽然很多编译器不写也能过但这是规范的写法竞赛代码建议保持。6.3 关于读入方式的另一个选择上面用了scanf。如果你的编译器/教材主推cin也可以用#include iostream #include iomanip using namespace std; int main() { const double PI 3.14159; double r; cin r; cout fixed setprecision(4) 2 * r endl; cout fixed setprecision(4) 2 * PI * r endl; cout fixed setprecision(4) PI * r * r endl; return 0; }两种方案都能AC。选择标准只有一个你更熟悉哪个就用哪个。但无论选哪个都得把对应的格式化规则彻底搞懂——尤其是cout方案里的fixed不能漏。7. “保留四位小数”的底层真相精度误差、四舍五入与边界测试7.1 double 不是十进制而是二进制的近似很多同学以为double就是“精确的小数”这是一个天大的误解。计算机里的浮点数都是以二进制存储的。而十进制小数转二进制很多都无法精确表示——比如0.1在二进制里是一个无限循环小数double存储的只是它的近似值。这意味着什么意味着在代码里double x 0.1;x的实际值并不是严格等于0.1而是约等于0.1000000000000000055511151231257827。单独输出x时printf会帮你四舍五入成“0.100000”看着没问题。但如果你拿x做一大堆乘法和加法误差就会逐渐累积、放大最后在某一次输出时暴露出来。2067为什么会成为新手的第一道“浮点陷阱题”就是因为面积计算里有r * r如果r是个比较“脏”的浮点数误差经过平方运算后有可能被放大到第4位小数。7.2 printf 的四舍五入规律其实不总是“四舍五入”又一个冷知识printf 在做小数舍入时的规则在不同编译器、不同平台下可能出现差异。有些平台用的是“四舍五入”round-half-up有些用的是“四舍六入五成双”round-half-to-even也叫银行家舍入。这个差异在绝大多数题目里不会给你造成麻烦因为测试数据的构造者通常不会精确到让你卡在舍入规则的分界线上。但我曾经遇到过一个学生他提交的代码在自己电脑上输出是3.1416OJ上判定却是WA。我帮他一查原来他在本地用的是某个特殊版本编译器舍入行为跟OJ不一样。这个问题的本质不是谁的舍入更“对”而是OJ以它的标准输出为准。所以在本地测试通过并不代表AC——提交前最好能大概了解OJ的评测环境或者至少保证代码按常规编译器GCC/G的行为来写。7.3 如何自测代码边界数据大法针对2067我建议拿到代码后至少测以下几组数据输入1.5验证和样例输出一致。输入0半径是0直径、周长、面积应该都是0.0000。输入10000面积比较大验证double没有溢出、格式没有崩成科学计数法。输入3.14159半径恰好等于π的题设值验证运算有没有意外。自测时手算或者用计算器核对一下结果尤其是0和很大的数这两个边界。很多WA就是栽在这些“看起来不会考”的边界情况上。注意信息学竞赛的测试数据里永远包含边界。半径可能是极小值0可能是题目范围最大值可能是小数——你准备的测试越充分AC的概率越高。8. 顺手聊聊“弗洛伊德算法”与一本通的混入问题8.1 搜索时发现的热词异常最近刷题群里经常有人问“一本通2067和弗洛伊德算法有什么关系为什么我搜2067搜出来一堆弗洛伊德”这里解释一下没有任何关系。2067是基础入门章节的“圆周率”练习题弗洛伊德算法Floyd是最短路算法属于图论算法在一本通里大概要到“最短路径”那一章才会出现。搜索引擎和爬虫抓取时会把同一个教程站点的关键词混在一堆你要是跟着这个搜索路径去学很容易走偏。这就引出一个比较重要的学习原则刷一本通时按章节顺序走不要被搜索结果带跑。一本通的章节编排是有逻辑的——先顺序结构、再分支、再循环然后数组、函数、结构体最后才到算法。弗洛伊德算法出现在靠后的位置现在研究它除了增加焦虑没有任何用处。8.2 从2067看一本通入门章节的学习方法2067处于“第二章 顺序结构”里这一章的全部题目都是训练你最基本的“输入-计算-输出”这一闭环。怎么学这一章效率最高我的建议是这样第一每道题先自己思考并写码。哪怕写错也没关系写错的地方就是你的知识盲区。比如这题里%lf写错、float精度不够、setprecision忘了fixed——每一个错都是很好的教训。第二AC之后翻一翻讨论区或题解看看别人的写法。不是为了抄而是为了对比别人定义的变量名更清晰别人用了更简洁的写法这种对比能帮你积累代码风格上的经验。第三把每章的核心知识整理成自己的清单。比如“顺序结构”这章你的清单里可以写输入输出scanf / printf 的格式控制串浮点类型double优先%lf读入%.4lf输出常量定义const double PI 3.14159常见坑%f与%lf混用、fixed漏写、float精度不足这个清单以后复习时翻一翻比重新刷十道题都管用。8.3 一道2067能衍生出的变体与进阶方向学完这道题后你还可以顺手做几个“变体练习”帮助巩固输入直径求周长和面积考察你是否能逆向推导公式。输出时保留2位小数考察格式化输出的变通。输入多个半径逐个输出结果为后面学循环做铺垫。已知面积求半径需要引入开方函数sqrt为数学库函数的学习打基础。这些变体不用去OJ上找题自己在本地改一改、跑一跑就行。目的不是刷题量而是让你把“输入-计算-格式化输出”这套模型内化到条件反射的级别。9. 我个人带学生时的三个心得9.1 第一代码风格从第一天就要立规矩变量命名要见名知意不要搞a、b、c满天飞。2067虽然小但d、c、s这种缩写还能接受等你写到后面几百行的程序时糟糕的命名会让你自己都看不懂。我个人的规矩是能拼出完整的单词就拼出来比如diameter、circumference、area。单词太长可以适当缩写但必须在注释里写清楚含义。这个习惯越早养成后面调试越省力。9.2 第二WA了不要急着问人先按“数据-公式-类型-格式”排查每次学生跑过来跟我说“我这题WA了老师你帮我看看”我都会让他们按固定顺序自查数据读对了吗输入格式对不对类型对不对公式写对了吗数学表达有没有抄错类型够用吗会不会精度丢失格式对了吗保留几位小数换行了吗这四个问题排查下来九成以上的WA都能自己找到原因。自己去定位问题的过程比直接看答案珍贵得多。9.3 第三不要轻视任何“简单题”有句话在竞赛圈流传很久简单题不是拿来“会做”的而是拿来“不出错”的。2067这种题练的是你的细致程度和稳定度。真到了赛场上你前面这些小分题稳定全拿后面才有底气去冲难题。我见过太多选手难题能写出名堂反而在基础题上因为漏了个fixed、写错一个%lf丢掉不该丢的分。这种丢分最伤士气——明明会却输了。10. 最后的实操建议把2067当作你的“输出格式自检标准”如果你现在正卡在某道浮点输出相关的问题上不妨回头看看2067的写法。它几乎涵盖了你今后写任何“实数输出”题目都会遇到的元素读入double、乘除计算、固定小数位输出。把这几件事彻底吃透比多刷十道同类题都管用。还有一个我自己用的小技巧分享给大家准备一个自己常用的“浮点数输入输出模板”每次做新题时直接从这个模板起步。模板不需要很长类似这样#include cstdio using namespace std; int main() { double x; scanf(%lf, x); // 在这里写你的计算逻辑 printf(%.4lf\n, 结果); return 0; }这样每次做题你不用重新回忆scanf和printf的语法细节把精力集中在问题本身的逻辑上。题做多了这个模板会自然长成你自己的风格——有的人喜欢留更多的预处理宏有的人喜欢把const定义放在外面。这些都不重要重要的是你已经能用代码稳定地表达计算过程了。2067这道题我年年讲年年都有新收获。以前我带学生只看他能不能AC现在我更关注他在写这道题时有没有想过“float和double差在哪”“printf和cout到底听谁的”“边界数据要不要测”。想得越多的学生后面的路走得越远。希望你看完这篇文章不只是拿到一个AC而是把这道“简单题”背后的门道真正吃透。
返回列表