
1. 这道题到底在考什么从交换整数部分看实数拆解的本质PTA 上的 7-32 这道题标题看着简单——交换两实数的整数部分但真正动手写的时候很多人会在第一个弯道就翻车。原因不在于算法难而在于对实数这个词的理解太粗糙。题目要求读入两个实数把它们的整数部分互换小数部分保持不变然后输出交换后的结果。听起来像是小学数学题但落到 C 语言里涉及浮点数的存储、取整方式、格式化输出三个层面的坑任何一个没处理好提交上去就是一片红。先把题意拆开看。假设输入是3.14和5.76那么第一个数的整数部分是 3小数部分是 0.14第二个数的整数部分是 5小数部分是 0.76。交换整数部分之后第一个数变成5.14第二个数变成3.76。核心操作就两步把整数部分剥离出来再把对方的整数部分拼回去。但剥离和拼回这两个动作在 C 语言里有好几种实现路径选错了路径精度问题就会像幽灵一样缠着你。这道题在 PTA 的基础题库里属于入门偏上的难度通常出现在数据类型与表达式或者顺序结构这一章。它考察的不是循环、不是递归而是你对浮点数取整和格式化输出这两个基本功的掌握程度。很多初学者学完int和float的强制类型转换之后觉得取整就是(int)x这么简单但真到这道题上负数、精度丢失、输出格式这几个问题会同时冒出来。我见过不少人第一次做这道题时的思路是这样的读两个double用(int)取出整数部分然后做减法得到小数部分再重新组合。这个思路方向没错但细节上至少有四个地方需要仔细推敲取整用(int)还是floor小数部分怎么算才不丢精度输出用%g还是%.2f负数怎么办这四个问题就是这篇文章要逐个拆解的核心。提示PTA 的题目描述里通常会给出输入输出样例做题之前一定要先把样例在纸上手动算一遍确认自己理解了交换整数部分的确切含义再动手写代码。这一步花两分钟能省掉后面半小时的调试。2. 浮点数取整的三条路为什么(int)不是万能钥匙2.1 强制类型转换的截断行为及其边界C 语言里把浮点数转成整数最直接的方式就是强制类型转换(int)x。这个操作的行为是向零截断也就是说(int)3.9得到 3(int)-3.9得到 -3。注意它不是四舍五入也不是向下取整而是直接把小数点后面的部分砍掉。对于正数来说截断和向下取整的结果一样但对于负数两者就分道扬镳了——floor(-3.9)是 -4而(int)-3.9是 -3。这道题如果输入全是正数用(int)完全没问题。但 PTA 的测试数据里经常藏着负数用例一旦你的代码用(int)处理负数整数部分就会取错。举个例子输入-3.14和5.76正确的整数部分应该是 -3 和 5交换后得到5.14和-3.76。如果你用(int)取整(int)-3.14确实是 -3这一步没问题。但如果你用floor呢floor(-3.14)是 -4那就错了。所以这里反而(int)是对的floor是错的。这就引出一个关键认知取整方式的选择取决于题目对整数部分的定义。数学上一个实数的整数部分通常指不超过它的最大整数也就是floor的结果。但在编程竞赛和 PTA 的语境里整数部分往往指的是小数点左边的那部分数字对于 -3.14 来说就是 -3。这两种定义在正数上一致在负数上不同。PTA 这道题采用的是后者所以(int)的截断行为恰好符合要求。2.2floor和trunc的适用场景对比floor函数来自math.h作用是向下取整返回不超过参数的最大整数。trunc函数同样来自math.h作用是向零截断行为和(int)强制转换一致但返回类型是double。这三个操作的关系可以用一张表说清楚输入值(int)截断floor向下取整trunc向零截断3.9333-3.9-3-4-33.0333-3.0-3-3-30.5000-0.50-10从表中可以清楚看到(int)和trunc的行为完全一致都是向零截断floor则是向下取整。对于这道题如果题目要求的是小数点左边的数字那就应该用(int)或trunc如果要求的是数学意义上的整数部分那就用floor。根据 PTA 7-32 的常见测试数据来看它采用的是前一种定义。那为什么还要提floor因为很多人在网上搜C 语言 取整数部分搜到的答案都推荐floor然后就直接拿来用了结果负数用例全挂。这就是典型的搜到答案但不理解适用条件的坑。我的建议是做这道题就用(int)简单直接不引入额外的头文件依赖行为也符合题目要求。2.3 大数场景下int溢出的隐患还有一个容易被忽略的问题如果输入的实数很大比如1234567890.5用(int)转换本身没问题因为int在大多数平台上能表示到 21 亿左右。但如果输入是9999999999.5这种超过int范围的数(int)转换就会产生未定义行为。PTA 的这道题通常不会出这么大的数据但养成好习惯没坏处——如果担心溢出可以用long long来接收整数部分转换时写(long long)x。不过这里有个细节(long long)x和(int)x的截断行为是一样的都是向零截断只是目标类型不同。所以如果你确定题目数据范围在int之内用(int)就够了如果不确定用long long更保险。但要注意后面拼回小数部分的时候如果整数部分用long long整个表达式可能会被提升为long long运算最终输出时格式说明符也要相应调整这就引出了下一节要讲的输出格式问题。3. 小数部分的提取减法、modf与精度陷阱3.1 用减法提取小数部分的正确姿势最直观的提取小数部分的方法就是用原数减去整数部分frac x - (int)x。对于3.14来说3.14 - 3 0.14看起来没问题。但浮点数的二进制表示决定了这个减法结果往往不是精确的0.14而是一个接近0.14的数比如0.14000000000000012。这是 IEEE 754 浮点数的固有特性不是 C 语言的 bug。那这个微小的误差会不会影响最终结果取决于你怎么输出。如果你用%.2f输出它会四舍五入到两位小数0.14000000000000012会显示成0.14没问题。但如果你用%g或者%f不加精度限制就可能输出一长串数字导致格式错误。所以这道题的输出格式选择和精度问题是绑在一起的。还有一个更隐蔽的坑当x是负数时x - (int)x的结果是什么以-3.14为例(int)-3.14是 -3那么-3.14 - (-3) -0.14。也就是说小数部分带上了负号。如果你直接把-0.14和对方的整数部分拼起来比如对方整数部分是 5你算5 (-0.14) 4.86那就完全错了。正确的做法是取小数部分的绝对值或者用x - trunc(x)之后再取fabs。3.2modf函数一步到位但有坑C 标准库提供了一个专门用来拆分整数和小数部分的函数modf声明在math.h里。它的原型是double modf(double x, double *iptr)返回值是小数部分同时把整数部分写入iptr指向的变量。用起来是这样的double int_part; double frac_part modf(x, int_part);这样int_part拿到整数部分frac_part拿到小数部分一次调用搞定。但modf的整数部分是按向零截断的方式计算的和(int)一致所以对于这道题是适用的。不过modf有个问题它返回的小数部分同样带符号。modf(-3.14, ip)会让ip -3.0返回值是-0.14。所以符号问题依然存在还是需要自己处理。另外modf的整数部分是double类型如果你后面要把它当整数用还得再转一次。对于这道题来说用modf并不比手动减法简洁多少反而多引入了一个函数调用。我的个人习惯是这种简单场景直接手动算逻辑清晰调试方便不依赖库函数的行为细节。3.3 精度丢失在什么情况下会真的导致 WA理论上浮点数有精度误差但 PTA 的这道题通常输出两位小数误差在1e-15量级四舍五入到两位小数后完全看不出来。那什么情况下精度问题会真的导致 Wrong Answer主要有两种场景第一种是输入的小数位数很多比如3.1415926535而你用float而不是double来存储。float只有约 7 位有效数字存3.1415926535会丢失后面的精度导致小数部分计算出来偏差较大。所以这道题一定要用double不要用float。第二种是拼接方式不当。比如你想把整数部分 5 和小数部分 0.14 拼成 5.14用5 0.14得到的是5.140000000000001输出两位小数没问题。但如果你用字符串拼接的思路先把整数转成字符串再把小数部分转成字符串然后拼起来就可能因为小数部分的字符串表示过长而出错。所以拼接要用数值运算不要用字符串操作。注意PTA 的判题系统对输出格式非常严格。多一个空格、少一个换行、小数点后位数不对都会判错。提交之前一定要把输出格式和题目要求逐字对照一遍。4. 拼接与输出格式说明符选错就全盘皆输4.1%f、%.2f、%g的行为差异输出浮点数时C 语言提供了多种格式说明符它们的行为差别很大%f默认输出六位小数。printf(%f, 5.14)会输出5.140000。%.2f输出两位小数。printf(%.2f, 5.14)会输出5.14。%g自动选择%f或%e并去掉末尾的零。printf(%g, 5.14)会输出5.14但printf(%g, 5.0)会输出5。%.2g保留两位有效数字不是两位小数。printf(%.2g, 5.14)会输出5.1。这道题的关键在于题目要求输出交换后的两个实数但并没有明确说保留几位小数。这时候就要看样例输出。如果样例输出是5.14 3.76那就是两位小数如果是5.140000 3.760000那就是默认六位。根据 PTA 7-32 的常见版本样例通常是保留两位小数所以用%.2f。但这里有个陷阱如果输入的小数部分本身不足两位比如3.1和5.2交换后是5.1和3.2用%.2f输出会变成5.10和3.20。这算不算错取决于题目的要求。如果题目说保留两位小数那就对如果题目说保持原有小数位数那就错。PTA 这道题通常是前者所以%.2f是安全的选择。4.2 拼接时的类型提升与隐式转换当你写new_x other_int frac这样的表达式时如果other_int是int类型frac是double类型C 语言会自动把int提升为double再做加法结果是double。这个隐式转换是安全的不会丢精度。但如果你把other_int定义成了long long而frac是double同样会提升为double但long long转double在大数情况下可能丢精度。不过对于这道题的数据范围来说这不是问题。真正需要注意的是输出时的格式说明符要和变量类型匹配。如果你用%f输出一个double没问题但如果你用%d输出double那就是未定义行为可能输出一堆乱码。所以拼接结果的变量一定要声明为double输出时用%f系列说明符。4.3 输出顺序与空格换行的细节PTA 的题目通常要求两个数之间用空格分隔行末不能有多余空格最后要有换行。这些细节看起来微不足道但判题系统是逐字符比对的错一个就 WA。常见的写法是printf(%.2f %.2f\n, new_x, new_y);这样两个数之间一个空格末尾一个换行干净利落。不要写成printf(%.2f , new_x); printf(%.2f\n, new_y);虽然结果一样但多了一次函数调用没必要。也不要在末尾加额外的空格有些判题系统对行末空格敏感。还有一个容易忽略的点如果题目要求输出多组结果或者有多个测试用例那就要用循环处理。但 7-32 这道题通常只有一组输入读两个数、处理、输出就结束了。不要画蛇添足地加循环否则会因为多读入而卡住。5. 完整代码实现与逐行拆解5.1 基础版本的代码与注释把前面几节的分析落到代码上一个稳妥的实现是这样的#include stdio.h int main() { double a, b; scanf(%lf %lf, a, b); // 提取整数部分用 (int) 向零截断 int int_a (int)a; int int_b (int)b; // 提取小数部分取绝对值避免负数符号干扰 double frac_a a - int_a; double frac_b b - int_b; // 交换整数部分拼回小数部分 double new_a int_b frac_a; double new_b int_a frac_b; printf(%.2f %.2f\n, new_a, new_b); return 0; }这段代码逻辑清晰每一步都有明确的意图。scanf用%lf读double这是必须的用%f读double会导致未定义行为。提取整数部分用(int)符合题目对整数部分的定义。小数部分用减法得到对于正数直接就是正确结果对于负数会带负号但后面拼接时int_b frac_a会自动处理符号——等等这里真的没问题吗5.2 负数场景下的符号处理验证让我手动模拟一下负数的情况。假设输入-3.14和5.76int_a (int)-3.14 -3int_b (int)5.76 5frac_a -3.14 - (-3) -0.14frac_b 5.76 - 5 0.76new_a int_b frac_a 5 (-0.14) 4.86new_b int_a frac_b -3 0.76 -2.24但正确结果应该是5.14和-3.76。问题出在哪里出在frac_a是-0.14而我们需要的是0.14。也就是说小数部分应该取绝对值或者用a - trunc(a)之后再取fabs。修正后的代码double frac_a a - int_a; if (frac_a 0) frac_a -frac_a; double frac_b b - int_b; if (frac_b 0) frac_b -frac_b;或者更简洁地用fabs#include math.h double frac_a fabs(a - int_a); double frac_b fabs(b - int_b);这样frac_a就是0.14new_a 5 0.14 5.14new_b -3 0.76 -2.24——等等new_b还是不对。正确结果应该是-3.76但这里算出来是-2.24。问题出在new_b的拼接方式上int_a frac_b -3 0.76 -2.24但我们需要的是-3.76也就是整数部分 -3 和小数部分 0.76 组合成 -3.76。这说明简单的加法拼接在负数场景下不成立。正确的拼接方式应该是如果整数部分是负数那么结果应该是整数部分 - 小数部分而不是整数部分 小数部分。因为 -3.76 的整数部分是 -3小数部分是 0.76但 -3 - 0.76 -3.76而 -3 0.76 -2.24。所以拼接逻辑需要根据整数部分的符号来分支double new_a, new_b; if (int_b 0) { new_a int_b frac_a; } else { new_a int_b - frac_a; } if (int_a 0) { new_b int_a frac_b; } else { new_b int_a - frac_b; }这样对于int_b 5new_a 5 0.14 5.14对于int_a -3new_b -3 - 0.76 -3.76。结果正确。5.3 更优雅的拼接方案符号分离法上面的分支写法虽然正确但代码有点啰嗦。有没有更简洁的方式可以用符号分离的思路把整数部分的符号单独拿出来小数部分始终取正最后根据符号决定加减。double sign_a (a 0) ? 1.0 : -1.0; double sign_b (b 0) ? 1.0 : -1.0; int abs_int_a (int)fabs(a); int abs_int_b (int)fabs(b); double frac_a fabs(a) - abs_int_a; double frac_b fabs(b) - abs_int_b; double new_a sign_a * (abs_int_b frac_a); double new_b sign_b * (abs_int_a frac_b);等等这个逻辑也有问题。交换的是整数部分但符号应该跟着谁走如果a -3.14b 5.76交换整数部分后a的新整数部分是 5小数部分是 0.14所以new_a 5.14符号是正的b的新整数部分是 -3小数部分是 0.76所以new_b -3.76符号是负的。也就是说符号跟着整数部分走而不是跟着原来的数走。所以正确的逻辑是new_a的符号由int_b决定new_b的符号由int_a决定。用符号分离法写int abs_int_a abs((int)a); int abs_int_b abs((int)b); double frac_a fabs(a) - abs_int_a; double frac_b fabs(b) - abs_int_b; double new_a (int_b 0 ? 1 : -1) * (abs_int_b frac_a); double new_b (int_a 0 ? 1 : -1) * (abs_int_a frac_b);这样逻辑就对了。不过说实话对于这道题来说分支写法更直观不容易出错。符号分离法虽然简洁但需要想清楚符号的归属反而容易绕晕。我的建议是用分支写法把逻辑写清楚比追求代码短更重要。6. 踩坑实录那些年我在 PTA 上交过的 WA6.1 用float导致精度不足我第一次做这道题的时候想都没想就用了float觉得实数嘛float够了。结果提交上去有一个测试点死活过不了。后来把float改成double立刻就 AC 了。原因就是float的有效数字只有 7 位左右而测试数据里可能有3.1415926这样的输入float存储时会丢失后面的位数导致小数部分计算偏差最终输出和预期差了一点。这个坑的教训是在 C 语言里处理浮点数默认用double除非有明确的理由用float。double的精度是float的两倍多而且现代计算机上double的运算速度并不比float慢多少没必要为了省那点内存而牺牲精度。6.2 忘记处理负数导致符号错误第二个坑是负数。我一开始的代码只考虑了正数用int_b frac_a直接拼接正数测试点全过一遇到负数就挂。当时我还纳闷-3.14的整数部分不就是 -3 吗(int)-3.14确实是 -3 啊为什么结果不对后来手动模拟了一遍才发现问题不在取整而在拼接——-3 0.76不等于-3.76而是-2.24。这个坑的教训是浮点数的符号处理不能想当然一定要手动模拟负数场景。写完代码之后至少用一组正数、一组负数、一组正负混合的数据手动验算一遍确认逻辑正确再提交。6.3 输出格式说明符用错第三个坑是输出格式。我一开始用了%g觉得%g会自动去掉末尾的零输出更简洁。结果样例输出是5.14 3.76我的代码输出的是5.14 3.76看起来一样啊但提交上去就是 WA。后来仔细看题才发现题目要求保留两位小数而%g对于5.10这样的数会输出5.1少了一位小数判题系统就判错了。这个坑的教训是输出格式一定要严格按题目要求来不要自作聪明。题目说两位小数就用%.2f题目说六位小数就用%f不要用%g这种智能格式因为它的行为依赖于具体数值不可控。6.4 行末空格与换行的细节第四个坑比较隐蔽行末空格。我有一版代码写的是printf(%.2f %.2f \n, new_a, new_b);注意%.2f后面多了一个空格。在本地运行的时候完全看不出来因为空格在行末不可见。但 PTA 的判题系统是逐字符比对的多一个空格就 WA。后来把多余的空格删掉改成printf(%.2f %.2f\n, new_a, new_b);就过了。这个坑的教训是提交之前用cat -A或者类似的工具检查一下输出确认行末没有多余的空格或制表符。虽然麻烦但能避免很多莫名其妙的 WA。7. 从 7-32 延伸出去实数处理在 PTA 其他题目中的复用7.1 类似题目的通用解题框架PTA 上有一类题目都涉及实数的拆分和重组比如求整数的位数、分离各位数字、实数四舍五入等等。这些题目的核心操作都是提取某一部分处理后重新组合。掌握了 7-32 这道题的思路其他类似题目就可以套用同样的框架读入数据确认类型double还是int提取需要的部分整数部分、小数部分、某一位数字对提取出的部分做处理交换、运算、判断重新组合并输出注意格式这个框架看起来简单但每一步都有细节需要注意。比如第 1 步如果题目说输入一个实数那就要用double如果说输入一个整数那就用int。第 2 步提取方式取决于数据范围和符号情况。第 4 步输出格式取决于题目要求。7.2 浮点数比较中的eps技巧在 PTA 的其他题目里经常需要判断两个浮点数是否相等。由于浮点数的精度误差不能用直接比较而要用一个很小的阈值eps#define EPS 1e-6 if (fabs(a - b) EPS) { // 认为 a 和 b 相等 }这个技巧在 7-32 里没有直接用到但在更复杂的实数处理题目里是必备的。eps的大小取决于题目的精度要求通常取1e-6或1e-8。如果题目要求保留两位小数1e-6就够了如果要求更高的精度就要用更小的eps。7.3 格式化输出的通用原则PTA 的格式化输出有一个通用原则题目怎么说你就怎么做不要加戏。题目说保留两位小数你就用%.2f题目说两个数之间用空格分隔你就在两个%之间放一个空格题目说行末换行你就在最后加\n。不要自己决定输出几位小数不要自己决定用什么分隔符不要自己决定要不要换行。这个原则听起来像是废话但我见过太多人因为觉得这样更好看而改了输出格式结果 WA 到怀疑人生。判题系统是机器它只认题目要求的格式不认你的审美。8. 一些不那么显然的实操心得8.1 本地测试时如何模拟 PTA 的判题环境PTA 的判题环境和本地 IDE 的运行环境有一些差异比如行末空格、换行符、缓冲区刷新等。为了在本地尽可能模拟判题环境我习惯用重定向的方式测试echo 3.14 5.76 | ./a.out这样可以看到程序在标准输入下的实际输出。然后用xxd或者od -c查看输出的十六进制确认没有多余的空格或换行echo 3.14 5.76 | ./a.out | od -c如果输出是5.14 3.76\n那就没问题如果输出是5.14 3.76 \n末尾多一个空格那就要改代码。8.2 提交前的自查清单每次提交 PTA 之前我都会过一遍这个清单数据类型对不对该用double的地方有没有用float取整方式对不对负数场景有没有考虑拼接逻辑对不对符号处理有没有问题输出格式对不对小数位数、空格、换行有没有按要求边界情况有没有考虑比如输入是0.0或者-0.0会怎样这个清单看起来繁琐但养成习惯之后每次检查只需要十几秒能省掉大量调试时间。8.3 关于-0.0的一个冷知识最后分享一个冷知识浮点数里有-0.0这个值它和0.0在数值上相等但在某些操作下会表现出不同的行为。比如1.0 / 0.0是正无穷而1.0 / -0.0是负无穷。在 7-32 这道题里如果输入是-0.0(int)-0.0是 0fabs(-0.0)是0.0所以不会有什么问题。但如果你在其他题目里遇到-0.0输出时可能会显示成-0.00和预期的0.00不同。这时候可以用if (x 0) x 0;来消除负零。这个知识点在 7-32 里用不到但了解一下没坏处说不定哪天就遇到了。8.4 关于 PTA 题目编号的一点观察PTA 的题目编号比如 7-32通常对应着教材的章节顺序。7-32 出现在基础题目集的靠前位置说明它考察的是基础语法不涉及复杂算法。做这类题目的时候不要想太多花哨的技巧把基本功打扎实就行。取整、拼接、格式化输出这三个动作练熟了后面遇到更复杂的实数处理题目也能从容应对。我在带新手的时候经常让他们把 7-32 这道题用三种不同的方式实现一遍一种用(int)截断一种用modf拆分一种用字符串处理。三种方式都写一遍对浮点数的理解会深刻很多。虽然字符串处理的方式在 PTA 上可能过不了因为精度问题但写一遍能让你明白为什么数值运算比字符串操作更可靠。8.5 从这道题看 C 语言浮点数的设计哲学C 语言的浮点数设计遵循 IEEE 754 标准这个标准的核心思想是用有限的二进制位表示尽可能多的实数代价就是精度损失。理解这一点就能理解为什么0.1 0.2 ! 0.3为什么float和double的行为不同为什么取整和拼接需要小心处理。7-32 这道题表面上考的是语法实际上考的是你对浮点数本质的理解。如果你只是背了一个模板代码然后提交可能能过但遇到变体题目就会卡住。如果你理解了浮点数的存储方式、取整的几种模式、精度误差的来源那不管题目怎么变你都能从容应对。这也是我写这篇文章的初衷不只是给一个能 AC 的代码而是把背后的逻辑讲清楚让读者下次遇到类似问题时能自己分析、自己解决。代码会过时题目会变化但底层的原理和思维方式是通用的。