资讯详情

C语言格里高利公式求PI:精度边界与循环实现

📅 2026/11/3 23:51:55 | 华诺云谱 👁 阅读
C语言格里高利公式求PI:精度边界与循环实现
1. 先把题目掰开揉碎这个 PI 到底在求什么C 语言练习里用格里高利公式求给定精度的 PI 值这道题几乎是每个初学者绕不过去的一道坎。它出现在在线评测平台的习题集里题面通常只有短短几行给定一个精度 eps用格里高利公式计算 π 的近似值要求精确到最后一项的绝对值小于 eps 为止。看起来要素齐全实际上每一个短语背后都埋着一个容易踩的坑——什么是最后一项、精度判断放在哪、这一项到底加不加进去、输出保留几位小数任何一处理解偏差提交上去就是答案错误。先把这道题的价值说清楚。它不是一道单纯的循环练习题而是把三个东西捆在一起考级数展开的数学直觉、循环边界条件的精确控制、浮点数的行为认知。会写 for 循环的人很多但能把当某一项的绝对值小于 eps 时停止这句话翻译成没有歧义的代码的人比例并不高。更适合谁看如果你是刚学完 while 循环、正在被在线评测平台反复打回的同学这篇会帮你把每个边界都对齐如果你已经能 AC但说不清为什么 eps 取 1e-6 时结果只有 6 位左右可信那第三节的误差估算值得看一遍。格里高利公式本身长这样π/4 1 - 1/3 1/5 - 1/7 1/9 - ...也就是π 4 × (1 - 1/3 1/5 - 1/7 ...)一个分母为奇数、符号交替、绝对值单调递减趋于零的交错级数。数学上它收敛到 π/4工程上我们用前 N 项的部分和乘 4 去逼近 π。这道题让你做的就是把取多少项这个决定权交给一个输入参数 eps让程序自己判断什么时候该停。关键在于这个级数收敛得非常慢。误差量级大约是 1/N想让 π 精确到小数点后 6 位需要几十万项想要 8 位就要几千万项。这个慢不是缺陷而是这道题最有教学价值的地方——它逼着你去想我的循环到底跑了多少次会不会超时输出的小数位真的可信吗2. 核心公式与精度语义把每一句话翻译成代码2.1 通项、符号与累加变量怎么组织把公式写成代码第一件事是把通项抽象出来。第 n 项n 从 1 开始可以写成a_n (-1)^(n-1) / (2n - 1)分母是 2n-1也就是 1、3、5、7、9 这一串奇数符号是正、负、正、负交替。累加所有 a_n 得到 S最后 π ≈ 4S。代码里我不建议真的去算pow(-1, n-1)那个函数又慢又容易因为浮点误差出问题。常规做法是维护一个符号变量每轮翻转int sign 1; double sum 0.0; for (int n 1; ; n) { double term 1.0 / (2 * n - 1); if (term eps) break; sum sign * term; sign -sign; } double pi 4.0 * sum;这段代码里有三个必须想清楚的点1.0写成1会变成整型除法直接得 02 * n - 1在 n 很大时会不会溢出sign用 int 还是直接用sum term后term -term。关于溢出实际算一下就知道eps 取到 1e-9 时n 大约到 5 亿2 * n是 10 亿刚好在 int约 21 亿上限范围内但如果题目的 eps 更小或者你把分母写成2*n1的变体就有溢出风险。稳妥的做法是用long long存分母或者干脆不维护 n直接维护分母本身double term 1.0; double denominator 1.0; while (term eps) { sum term; denominator 2.0; term 1.0 / denominator; }这种写法连符号都省了——在循环里对 term 取负即可。我个人更偏爱维护分母的写法原因很简单分母自增 2 是一次浮点加法比每轮做一次整数乘法和一次除法便宜而且天然不会溢出。2.2 最后一项绝对值小于 eps 到底加不加这是这道题最容易出错的地方也是最多人反复提交不过的原因。题面说精确到最后一项的绝对值小于 eps 为止隐含的语义是这一项不计入总和。换句话说循环条件是当前项的绝对值 ≥ eps 时继续加否则停。但教材和不同版本的题面存在细微差别。有些版本的表述是当某一项的绝对值小于 eps 时停止累加有些是累加直到最后一项的绝对值小于 eps后者在语感上像是这一项已经加进去了。这两种理解在结果上会差一个 eps 量级的项对于保留 6 位小数的输出eps 1e-6 时正好卡在最后一位上肉眼几乎看不出差别但评测机的比对是精确的差一位就是错。我的处理原则是以不计入为准然后自己造两个测试点验证。验证方法很直接取 eps 0.5手算一下。第一项是 11 不小于 0.5所以加进去sum 1π ≈ 4第二项是 1/3 ≈ 0.333小于 0.5停止。如果按计入的语义sum 1.333π ≈ 5.333。两者差得很远一眼就能看出评测机认哪一种。这个技巧我强烈建议每个做这道题的人都试一次——用一个夸张的 eps 把边界差异放大比盯着题面猜语义可靠得多。2.3 循环结构while、do-while 与 for 的取舍三种循环都能写但表达的语义不一样选错了会在边界上出问题。while是首选因为它的语义就是条件成立才进入循环体和项不够小就继续加完全对应double term 1.0, sum 0.0, denominator 1.0; while (term eps) { sum term; denominator 2.0; term 1.0 / denominator; // 注意这里 term 还没取负下面单独处理符号 }do-while的坑在于它至少执行一次循环体。如果 eps 输入 2大于第一项 1按不计入的语义正确结果应该是 sum 0、π 0而 do-while 会强行加进第一项得到 π 4。所以除非题目明确要求至少累加一项否则别用 do-while。for配合 break 也可以但要注意 break 的位置必须在累加之前否则就把不该加的那一项加进去了。这类看起来都对、结果差一点的代码是调试时间的主要消耗源。2.4 符号处理三种写法与各自的风险符号交替的实现有三条路各有适用场景。第一条独立符号变量sign -sign;。可读性最好新手最不容易错代价是每轮一次整数取反几乎零开销。我平时写题解默认用这种。第二条直接对项取负term -term;。省一个变量但前提是你的 term 变量本身带符号。如果 term 带符号那么精度判断就必须写fabs(term) eps忘了 fabs 就会因为负数永远小于 eps 而只跑一轮直接得到 π ≈ 4。这个 bug 极其常见我在新手代码里见过不下几十次。第三条递推式更新term -term * (2.0 * n - 1) / (2.0 * n 1);。这是从 a_{n1} -a_n × (2n-1)/(2n1) 推出来的好处是每轮只做乘除各一次不需要重新算分母某些情况下更快。但递推会累积舍入误差跑几百万轮之后term 的相对误差会被放大。对于 eps 只到 1e-6 的题目影响可以忽略但如果 eps 小到 1e-10 以上我更倾向每轮重新算1.0 / denominator让每一项都是干净的。3. 完整实现与逐行拆解3.1 最简版本只输出 π 值假设题面要求输出保留 6 位小数的 π 值输入是一行精度 eps。完整代码如下#include stdio.h int main(void) { double eps; if (scanf(%lf, eps) ! 1) { return 0; } double sum 0.0; double term 1.0; /* 第 1 项的绝对值 */ double denominator 1.0; int sign 1; while (term eps) { sum sign * term; sign -sign; denominator 2.0; term 1.0 / denominator; } printf(%.6f\n, 4.0 * sum); return 0; }逐段看。scanf(%lf, eps)里的%lf对应 double写成%f在 scanf 里是未定义行为很多平台上会直接读到垃圾值这是新手最常犯的输入错误之一。判断返回值是好习惯虽然这道题不需要但养成检查输入的习惯后面做多组输入的题目会省很多事。变量初始化的顺序值得说一句term 初值是 1.0第一项denominator 初值也是 1.0两者在第 n1 时是自洽的。循环体里先累加、再翻符号、最后分母加 2 并重算 term。这个顺序保证了每轮进入循环时term 就是待累加的那一项的绝对值sum 里装的是它之前所有项的和。这个性质叫循环不变量写循环的时候脑子里有这个不变量边界条件基本不会错。输出用%.6f因为 4.0 * sum 是 double格式化输出会自动四舍五入。如果题面要求输出成pi 3.141593这种带前缀的格式一定要注意空格和等号的位置在线评测平台对输出格式是逐字符比对的多一个空格也是错。3.2 加一个计数器输出 π 与迭代次数不少版本的题目会额外要求输出迭代次数或者要求输出最后一项的值。这时候加一个计数器就行#include stdio.h int main(void) { double eps; scanf(%lf, eps); double sum 0.0; double term 1.0; double denominator 1.0; int sign 1; int count 0; while (term eps) { sum sign * term; sign -sign; denominator 2.0; term 1.0 / denominator; count; } printf(pi %.6f\n, 4.0 * sum); printf(count %d\n, count); return 0; }这里有个细节count 的类型用 int 还是 long longeps 1e-9 时要跑 5 亿轮int 装得下但如果你手贱把 eps 改成 1e-12就会溢出。我的习惯是这种计数一律用long long多 4 个字节换一个不溢出的保证很划算。另外如果你要让程序既能输出 π 又能输出最后一项注意最后一项是已经越过阈值的那一项就是让 while 退出的那个 term而不是最后被累加进去的那一项。这两者差一个分母的位置题面如果说最后一项大概率指的是让循环停止的那一项的绝对值。这个语义在不同题面里也有分歧还是老办法用一个夸张的 eps 试出来。3.3 误差与迭代次数估算eps 到底要吃多少项这一节是我觉得比代码本身更有价值的部分。很多人 AC 之后就走了从来没想过自己的循环跑了多少轮、结果的第 7 位为什么不可信。交错级数有个漂亮的性质余项截断误差的绝对值不会超过第一个被舍弃的项的绝对值而且部分和从两侧逼近真值。也就是说当你因为 |a_N| eps 而停止时π 的误差大约在 4 × eps 的量级。这个结论很好用。eps 1e-6那么 π 的误差大约 4e-6——注意是 4e-6不是 1e-6。也就是说如果你输出 6 位小数最后一位大概率是错的。实测一下eps 1e-6 时程序输出的 π 是 3.141595 左右具体值取决于停止边界而真值是 3.1415926535...第 6 位小数确实对不上。这不是程序写错了而是精度参数本身不够。想稳定拿到 6 位正确小数eps 至少要取到 1e-7 甚至 1e-8。再看迭代次数。停止条件是 1/(2N-1) eps反解出 N ≈ 1/(2·eps)。代入几个常见的 epseps 取值约需迭代次数 Nπ 的误差量级在普通机器上的耗时1e-45,0004e-4瞬间1e-550,0004e-5瞬间1e-6500,0004e-6几毫秒1e-75,000,0004e-7几十毫秒1e-850,000,0004e-8约 0.1 到 0.5 秒1e-9500,000,0004e-9可能超时这张表解释了两件事。第一为什么 eps 取 1e-8 时程序感觉卡了一下——它真的在跑五千万次循环。第二在线评测平台如果给 1 秒时限把 eps 设到 1e-9 是很危险的循环体质朴的写法很可能超时。如果你在题解区看到有人说这题会 TLE多半就是卡在这里。省时间的办法有几个。把1.0 / denominator换成递推乘法能省掉除法把sign * term的两个分支合并能省掉对符号的判断编译器开-O2之后这类简单浮点循环的优化效果相当可观。不过在线评测平台一般不会让你改编译选项所以最实际的优化就是减少循环体内的运算次数。3.4 要不要上 Kahan 求和一个理性判断既然聊到浮点精度就得说说累加误差。朴素累加在项数巨大时会出现大数吃小数的问题当 sum 已经接近 0.785而 term 小到 1e-18 时sum term的结果可能还是 sum因为 term 落在了 double 的有效位之外。这种情况下继续循环毫无意义只是空转。Kahan 补偿求和能缓解这个问题原理是额外维护一个误差项把每次加法丢掉的那部分捡回来double sum 0.0, c 0.0; while (term eps) { double y sign * term - c; double t sum y; c (t - sum) - y; sum t; sign -sign; denominator 2.0; term 1.0 / denominator; }但说实话这道题里 Kahan 属于过度设计。原因是交错级数的部分和量级始终在 0.785 附近不会无限增长每项的相对误差是 1e-16 量级五十万项累加起来也就 1e-11 左右的偏差相对于 4e-6 的截断误差小了整整五个数量级完全被淹没。真正需要 Kahan 的场景是累加几十亿个同号的小数比如统计分析里的求和。在这道题上用 Kahan收益是零代价是代码可读性大幅下降还得解释给看代码的人听。我的态度是知道有这个东西知道它解决什么问题知道在这道题里不需要这就够了。盲目上高级技巧是很多人在刷题时越刷越迷茫的原因。4. 提交后各种答案错误的排查实录4.1 边界判断错位一张对照表搞定这类题目的错误里八成出在边界上。我把见过的几种典型错法和症状整理成表遇到 WA 可以先对照一遍。现象可能原因排查方法结果比正确值大一点点把不满足精度的那一项也加进去了用 eps 0.5 手算验证边界语义结果恒为 4.000000 或 0.000000只跑了一轮或一轮没跑检查 term 是否带符号、是否漏了 fabs结果差一位小数eps 取值不够截断误差超过输出精度把 eps 缩小 10 倍观察结果是否收敛结果恒为 0整型除法1/(2n-1)直接得 0检查有没有写1.0编译不过%lf用在 printf 里或者漏了头文件printf 中 double 用%f即可前几个测试点过、后面超时eps 极小循环次数爆炸数一下迭代次数考虑递推优化结果恒为 4这个症状特别典型第一项 1 不小于 eps 的判断写反了或者符号变成负数之后没加 fabs导致第二轮 term 是 -1/3而 -0.333 eps 恒成立循环立刻退出sum 只剩 1π 4。4.2 数据类型与整数除法的连环坑这是我见过的第二大类错误而且它有一个很讨厌的特性代码看起来完全正确跑出来是 0 或者 1。double term 1 / (2 * n - 1); /* 错整型除法结果是 0 */ double term 1.0 / (2 * n - 1); /* 对 */C 语言的规则是两个整数相除结果截断取整。1 除以 1 得 11 除以 3 得 0然后 0 被隐式转换成 double 赋给 term从第二项开始全是 0程序立刻结束。还有一处容易忽略的double eps; scanf(%f, eps);。前面说过scanf 里 double 必须用%lf。用错的话在某些平台上会把输入的 8 个字节只写进去 4 个eps 变成一个极小的垃圾值循环跑几亿次然后超时或者直接变成 0 导致死循环。这类问题用调试器看一眼变量值就能定位但光看代码很难发现。第三个坑是int溢出。前面算过eps 1e-9 时 n 会到 5 亿2 * n是 10 亿还在 int 范围内但如果题目把判断条件改成用2n1或者你多写了一层乘法就可能翻车。我现在的习惯是任何可能超过一千万的计数一律 long long。4.3 输出格式的隐形扣分在线评测平台的比对方式有三种精确匹配、忽略行末空格、浮点容差。这道题绝大多数情况下是精确匹配所以输出格式必须和题面一字不差。常见的要求有这几种输出3.141593直接printf(%.6f\n, pi)输出pi 3.141593注意等号两边各一个空格输出PI 3.14159265保留 8 位输出3.14保留 2 位我吃过一次亏题面写的是输出 π 的近似值保留 6 位小数我输出的时候结尾多打了一个空格因为习惯性在格式串里写%.6f \n结果连挂三个测试点。后来养成的习惯是格式串里只写\n绝不在数字后面加空格提交前把自己的输出复制出来和题面给的样例输出逐字符对比一遍尤其是小数点前后的空格数量。还有一个隐蔽的坑printf(%.6f, 4.0 * sum)里的4.0 * sum是 double没问题但如果写成printf(%d, ...)或者把结果先赋给一个 int就会输出完全无关的数字。编译器一般会警告但很多人不看警告直接提交。4.4 超时与死循环的分辨程序提交后显示运行超时有两种完全不同的原因得先分清。第一种是真死循环。比如把判断写成while (term 0)而由于浮点下溢term 永远不为 0 但也不再减小循环永远不结束。或者 eps 输入成了 0 或负数term eps恒成立。这类问题的特征是程序在本地跑也停不下来CtrlC 才断。排查办法很简单在循环里加一个计数跑到一亿次就打印个提示退出看看 term 到底变成了什么值。第二种是循环次数太多虽然能结束但超过时限。前面算过eps 1e-9 要跑五亿轮每轮一次除法加一次加法在较慢的评测机上跑两三秒很正常。这种不算死循环但很慢的情况可以通过打印迭代次数确认。优化手段按性价比排序一是把每轮两次浮点运算一次除法、一次加法压到最少用递推代替重算二是减少循环体内的分支比如把sign * term拆成两个累加变量分别累加正项和负项最后相减三是确认自己在用 double 而不是 long doublelong double 在部分平台上慢好几倍。我实测过同样 eps 1e-8递推写法比重新算除法的写法快大约 30%在时限紧张的题目上这 30% 可能就是过与不过的区别。5. 从这道题往外延伸的几条路5.1 换一个收敛更快的公式试试格里高利公式的收敛速度是线性倒数的太慢。如果你对用程序算 π这件事产生了兴趣可以试试马青公式π/4 4·arctan(1/5) - arctan(1/239)其中 arctan(1/x) 用泰勒级数展开每算一项大约能得到 1.4 位十进制精度。这个公式的收敛速度快得离谱——算几十项就能得到几十位正确的 π。用它替代格里高利公式重写一遍程序你会直观感受到算法选择对性能的影响有多大同一个问题换个公式迭代次数从五千万降到几十。这条路的延伸价值在于它让你明白程序优化不只是把代码写得更紧凑更重要的是换一个更好的数学模型。这是很多刷题的人容易忽略的视角。5.2 拿这道题练测试驱动的思维这道题其实是个非常好的构造测试用例的练习场。我平时做这类题会先想清楚四类用例第一类是常规值eps 1e-5 这种。第二类是边界值eps 1、eps 0.9、eps 1.000001看程序在阈值附近的表现是否连续。第三类是极端值eps 1e-12看程序是不是还能在合理时间内给出结果。第四类是非法输入eps 0 或者负数虽然题目不会这么测但想想程序会怎样是有意义的。养成这个习惯之后你会发现很多想当然的边界其实没想清楚。比如 eps 1 的时候程序输出应该是 0 还是 4按不计入的语义是 0按至少要算一项的语义是 4。这个问题在你写代码之前就必须回答而不是提交失败之后再回来猜。5.3 几个我自己踩过、也想提醒你的点第一个别迷信看起来对。浮点输出的四舍五入是银行家舍入还是四舍五入在不同平台上行为略有差异卡在 5 的边界上可能出问题。如果结果要求精确到某一位尽量让 eps 比需要的精度小一到两个数量级把最后一位的抖动压下去。第二个fabs和abs别混用。abs是整数取绝对值对 double 用会先隐式转成 int小数部分直接丢掉。这个坑在 term 接近 1 的时候看不出来到后面项很小时就会悄悄出错。用fabs需要#include math.h。第三个别过早优化。我见过有人在第一版代码里就塞进 Kahan 求和加递推加循环展开结果跑出来答案是错的然后花两小时定位到底是哪个优化引入的 bug。正确顺序永远是先写最直白的版本确认结果正确再考虑性能每改一处就重测一次。第四个也是我觉得最值得记住的这道题的重点从来不是算出 π而是精确地表达一个边界条件。等你去写真实的业务代码会遇到大量类似重试到剩余次数为 0 为止累加到累计金额超过阈值为止的逻辑那时候你会感谢这道题逼你把 while 的边界想清楚了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑