各位读者想必对圆周率并不陌生——小学五年级,诸位就能接触到圆周率。
圆周率的简史
圆周率的几何定义十分简单,用圆的周长除以直径,就得到了圆周率。
但纯靠徒手测量,难免有些误差。古代的很多文明都有发现所谓“密率”,比如阿基米德率先提出 π 的大小应该在3.14到3.15之间,在算数并不发达的古希腊文明,将圆周率精确到两位小数已经是壮举。
汉代,中国人刘徽在《九章算术》中发明了“割圆法”,即对圆作外切和内接正N边形,不断逼近。他最终逼近到了正36边型,得到“徽率” π = 3.14,精确到两位小数。
祖冲之则继承了刘徽的算法,暴力计算到正24576边形,算出 π = 3.1415926,领先全世界几百年。
欧洲人在文艺复兴之后从中国人手中接走了红旗,达芬奇,欧拉,欧几里得等一众学者的出现让欧洲人发现了许多以更高效率计算 π 的方法,在电子计算机被发明之后,人们大可以想算几位就算几位。
计算 π 任意位数的精确值
公式
现在,我们通常用计算机循环迭代计算无穷级数,以此不断逼近 π 的值。为了提高效率,又要求这个无穷级数的收敛速度尽可能的快,这样我们就能以最少的计算量,算出最多的位数。一个常用的公式是马青公式,如下图。
马青公式由英国天文学教授约翰·马青于1706年发现。每计算一项就能将π的精确度提升1.4位小数。
现在公式摆在了面前,我们要设计一个Python脚本来实现它。
算法
在Python中,典型双精度浮点的精度上限不过几十位小数,如果我们要计算超过100位的圆周率,就不能简单地用变量和循环,这里需要引入 Decimal 库来实现十进制高精度计算。我们可以指定计算精度。我们还引入tqdm库,这样方便显示计算进度。
from decimal import *
from tqdm import tqdm
getcontext().prec = 100000 #十万位精度,需要9万余次迭代。
观察公式,不难发现公式大致分为两个部分,即那两个需要我们用循环迭代运算的无穷级数。这里把前面那项简称为前项,后面那项简称为后项,方便理解。
我们要先设定两项的初始值,分别是1/5和1/239。在式子中我们还能观察到式子中两项之间的符号在交替改变,所以设变量Flip来判断循环时该加上还是该减去。最后初始化Pi的变量,以及创建一个计次变量用于循环计次。
Pi = Decimal(0)
First = Decimal(1) / Decimal(5)
Last = Decimal(1) / Decimal(239)
Flip = False
Count = Decimal(1)
Acc = 95000 #迭代次数,每一次迭代可以推进1.4十进制位的精度。
接下来进入主要的循环运算部分
for i in tqdm(range(Acc),position=0,file=sys.stdout,desc="Progress"):
if Count != Decimal(1):
if not Flip:
First = First - (Decimal(1) / ((Decimal(2)*Count - Decimal(1))*(Decimal(5)**(Decimal(2)*Count - Decimal(1)))))
Last = Last - (Decimal(1) / ((Decimal(2)*Count - Decimal(1))*(Decimal(239)**(Decimal(2)*Count - Decimal(1)))))
Flip = not Flip
elif Flip:
First = First + (Decimal(1) / ((Decimal(2)*Count - Decimal(1))*(Decimal(5)**(Decimal(2)*Count - Decimal(1)))))
Last = Last + (Decimal(1) / ((Decimal(2)*Count - Decimal(1))*(Decimal(239)**(Decimal(2)*Count - Decimal(1)))))
Flip = not Flip
Pi = (Decimal(16) * First) - (Decimal(4) * Last)
Count = Count + Decimal(1)
if Count % 100 == 0 or Count == Acc - 1:
#os.system('cls') # 清空Windows控制台
#tqdm.write(str(Pi))
pass
Python为了确保计算精度,所有的运算操作全部都要用Decimal对象进行,如果在此期间不进行任何控制台输出,将显著提高运行速度,尤其在精度特别高的时候。
最终的运算结果就是变量Pi的值,直接输出即可。
计算效率
Python是执行速度相对较慢的脚本编程语言,但Python代码相较于C++更易书写,高精度计算也更易在Python中实现。由于Python存在全局解释器锁,所以该脚本的计算速度很大程度上取决于CPU的单核性能,迭代的次数,以及要求的精确度。
对于现代消费级CPU(主频1.8Ghz)来说,使用该脚本计算十万位精确度的 π 值,大约需要2~3个小时。服务器专用的CPU则速度更快,因为其单核性能优于消费级的产品。
在库的选用方面,Decimal库的计算效率偏低,因为它的计算结果和过程依靠字符串来实现。
但Decimal可以精确计算数十万乃至数百万位的小数,且不会出现因二进制运算产生的循环小数误差,这一点是Numpy等一众基于C++编写的Python算术库所不具备的,在计算π的精确值这个问题上,我们并无其他更优的选择。
一万位以内的π值,脚本可以在3~5分钟之内完成计算,一千位则几秒便可完成计算。这是因为Python运行时对常用的数字进行了高速缓存,待高速缓存随着计算精度的升高而用尽,计算速度显著减慢。
完整代码
import math
from tqdm import tqdm
from decimal import *
import sys
import time
import os
Pi = Decimal(0)
Acc = int(input("How many times to calc:"))
getcontext().prec = round(1.4*Acc)
First = Decimal(1) / Decimal(5)
Last = Decimal(1) / Decimal(239)
Flip = False
Count = Decimal(1)
for i in tqdm(range(Acc),position=0,file=sys.stdout,desc="Progress"):
if Count != Decimal(1):
if not Flip:
First = First - (Decimal(1) / ((Decimal(2)*Count - Decimal(1))*(Decimal(5)**(Decimal(2)*Count - Decimal(1)))))
Last = Last - (Decimal(1) / ((Decimal(2)*Count - Decimal(1))*(Decimal(239)**(Decimal(2)*Count - Decimal(1)))))
Flip = not Flip
elif Flip:
First = First + (Decimal(1) / ((Decimal(2)*Count - Decimal(1))*(Decimal(5)**(Decimal(2)*Count - Decimal(1)))))
Last = Last + (Decimal(1) / ((Decimal(2)*Count - Decimal(1))*(Decimal(239)**(Decimal(2)*Count - Decimal(1)))))
Flip = not Flip
Pi = (Decimal(16) * First) - (Decimal(4) * Last)
Count = Count + Decimal(1)
if Count % 100 == 0 or Count == Acc - 1:
#os.system('cls') # 清空Windows控制台
#tqdm.write(str(Pi))
pass
print(str(Pi))
Python写在最后
现在,用你自己的电脑计算圆周率并不是什么明智的选择。
因为早就有计算机科学家用超级计算机将圆周率计算到了几万亿位,你在网上也能轻松查询到一百万位圆周率的精确数值,所以大可不必自己用CPU计算。
那么问题来了,用自己的电脑计算 π 有什么意义?
我说,没准儿,它能检测一下你电脑CPU的单核性能如何。