引言
说起圆周率,不得不提到两个人——刘徽和祖冲之。刘徽的割圆术基于圆的内接正多边形,他用正多边形的面积来逼近圆的面积。分割越多,内接正多边形和圆之间的面积越来越小,两者越来接近。无限分割之后,内接正多边形和圆将会合二为一。祖冲之在刘徽的基础上,设了一个直径为一丈的圆,在圆内切割计算。当他切割到圆的内接一百九十二边形时,得到了"徽率"的数值。他继续切割,作了三百八十四边形、七百六十八边形……直到他切割到二万四千五百七十六边形。祖冲之依次求出每个内接正多边形的边长。最后求得直径为一丈的圆,它的圆周长度在三丈一尺四寸一分五厘九毫二秒七忽到三丈一尺四寸一分五厘九毫二秒六忽之间。最终他推算出圆周率π的值在3.1415926和3.1415927之间,这是一个伟大成就!直到1000年后,15世纪阿拉伯数学家阿尔·卡西和16世纪法国数学家维叶特,才超过了他。
而现在,有了计算机,我们不必再用割圆术去求pi的值。接下来向 大家介绍几种求圆周率的方法,希望对大家的学习有所帮助。
一、math库
1. import math 2. pi = math.pi 3. print(pi) 4. # 运行结果为:3.141592653589793
math库是一个内置数学类的函数库,不支持复数类型(如读者有需要,可使用python自带的内置函数complex),仅支持整数和浮点数运算,math库一共提供了:
- 4个数字常数
- 44个函数,分为4类:
16个数值表示函数
8个幂对数函数
16个三角对数函数
4个高等特殊函数
而我们所需要的pi即为4个数字常数中的一个,直接利用math.pi调用即可。
输出结果:3.141592653589793。
二、蒙特卡洛(Monte Carlo)
蒙特卡洛方法,也称为计算机随机模拟方法,是一种基于“随机数”的计算方法。简单来说就是不断取样,逐渐逼近结果。
1. import numpy as np 2. import matplotlib.pyplot as plt 3. 4. # Draw a square and a circle 5. squareX = [1,-1,-1,1,1] # x coordinates of square 6. squareY = [1,1,-1,-1,1] # y coordinates of square 7. 8. circleX,circleY = [],[] 9. 10. for i in range(361): 11. circleX.append(np.cos(np.pi*i/180)) # x coordinates of circle 12. circleY.append(np.sin(np.pi*i/180)) # y coordinates of circle 13. 14. plt.rcParams['figure.figsize'] = [16, 8] # figure size 15. fig, ax = plt.subplots(1) 16. ax.plot(squareX, squareY, color='black') 17. ax.plot(circleX, circleY, color='b') 18. ax.set_aspect('equal') 19. plt.show()
我们运行上面的代码,可以得到下面这张图:
这是一个半径为1正方形,面积为2*2=4。内切一个单位圆,面积是1*1*π=π。这样圆的面积比正方形的面积就是π/4,向这个图形中随机添加多个点,统计落在圆里面的点数。再用统计出来的点数/总点数,就能近似代替π/4的值,这样就能吧π的值近似出来。取的总点数多,模拟效果越好,π值越精确,代码如下:
1. import numpy as np 2. import matplotlib.pyplot as plt 3. def monte_carlo_pi(N): 4. number_inside = 0 5. points_inside = [[], []] 6. points_outside = [[], []] 7. current_pi = [] 8. i = 0 9. count = 0 10. # n 为传入的总点数量 11. while i < N: 12. # 随机产生x,y坐标 13. x, y = np.random.uniform(-1, 1, 2) 14. # 如果x平方 + y平方 < 1,说明在圆内 15. if (pow(x, 2) + pow(y, 2)) < 1: 16. number_inside += 1 17. points_inside[0].append(x) 18. points_inside[1].append(y) 19. else: 20. points_outside[0].append(x) 21. points_outside[1].append(y) 22. current_pi.append(4 * number_inside /(i+1)) 23. i += 1 24. # π的值为:4 * (落在圆内的点/总的点) 25. return (points_inside, points_outside, current_pi)
调用函数,并做简单的可视化:
1. import numpy as np 2. import matplotlib.pyplot as plt 3. def monte_carlo_pi(N): 4. number_inside = 0 5. points_inside = [[], []] 6. points_outside = [[], []] 7. current_pi = [] 8. i = 0 9. count = 0 10. # n 为传入的总点数量 11. while i < N: 12. # 随机产生x,y坐标 13. x, y = np.random.uniform(-1, 1, 2) 14. # 如果x平方 + y平方 < 1,说明在圆内 15. if (pow(x, 2) + pow(y, 2)) < 1: 16. number_inside += 1 17. points_inside[0].append(x) 18. points_inside[1].append(y) 19. else: 20. points_outside[0].append(x) 21. points_outside[1].append(y) 22. current_pi.append(4 * number_inside /(i+1)) 23. i += 1 24. # π的值为:4 * (落在圆内的点/总的点) 25. return (points_inside, points_outside, current_pi) 26. # To see the entire image, go to Cell --> All Outputs --> Toggle Scrolling 27. 28. N = 5000 29. # Generate a bunch of x and y values between -1 and 1 30. points_inside, points_outside, current_pi = monte_carlo_pi(N) 31. 32. # Draw a square and a circle to frame out the simulation 33. squareX = [1,-1,-1,1,1] 34. squareY = [1,1,-1,-1,1] 35. 36. circleX,circleY = [],[] 37. 38. for i in range(361): 39. circleX.append(np.cos(np.pi*i/180)) 40. circleY.append(np.sin(np.pi*i/180)) 41. 42. 43. plt.rcParams['figure.figsize'] = [16, 8] 44. 45. fig, (ax1, ax2) = plt.subplots(1,2) 46. ax1.plot(squareX, squareY, color='black') 47. ax1.plot(circleX, circleY, color='b') 48. ax1.scatter(*points_inside, c='g', marker='.') 49. ax1.scatter(*points_outside, c='r', marker='.') 50. ax1.set_aspect('equal') 51. 52. ax2.plot(range(N), current_pi) 53. ax2.axhline(y=np.pi, c='r', ls='--') 54. ax2.axis([0, N, 0, 4]) 55. plt.show() 56. 57. print("Final pi: ", current_pi[-1])
左侧图是模拟的效果图,圆内点用绿色标注,圆外点用红色标注。右侧 图是计算出来的π值,可以看到当总点数越来越多时,π的值越来越稳定,不断向π的真实值逼近。总点数为5000时,计算出来的π值为:3.1624。如果你有兴趣的话,也可以不断增大总点数(N),看看什么时候才能超过祖冲之计算出来的精度!
三、黎曼和(定积分)
这就是高数的知识啦。利用黎曼和计算圆周率的近似值,以逼近单位圆第一象限的面积。将单位圆横坐标分成N分,每个矩形宽度为1/N,高度是sqrt(1-x**2)的函数值。
代码如下:
1. import math 2. def Riemann_pi(N): 3. pi = 0 4. fin_sum = 0 5. for i in range(0,N): 6. mid = (i+0.5)/N 7. fin_sum += 1/N * math.sqrt(1-mid**2) 8. pi = fin_sum*4 9. return pi
这个就是定积分的表现形式,将一个不规则形状的物体,分割成无数多个矩形,这些矩形的面积之和就是原来不规则物体的面积。
输出结果:3.1415929980246284,精度还是比较高的。
四、约翰.沃利斯(John Wallis)
直接给出公式:
代码:
1. def Wallis_pi(N): 2. sum_1 = 2 3. pi = 0 4. if N%2 != 0: 5. for i in range(3,N+1,2): 6. sum_1 *= ((i-1)/i)*((i+1)/i) 7. pi = sum_1*2 8. else: 9. for i in range(3,N+1,2): 10. sum_1 *= ((i-1)/i)*((i+1)/i) 11. sum_1 *= (N/(N+1)) 12. pi = sum_1*2 13. return (pi)
直接令sum_1 = 2,这样可以减少一次迭代次数,这样可以直接从3开始,分母用range加2;并且这里注意输入N=4和N=3是不一样的;N是一共有几个分数相乘,比如说N=4,那么就是公式里的前四项相乘;如果不用判断分开的话,N = 3和N=4就变成一样的结果了。所以我这里用if N%2==0判断了一下。后面按公式累乘就可以了。总感觉这里写的有些复杂,如果读者有什么好的想法也欢迎交流!
运行代码,传入N = 100 得到π= 3.126078900215409,N= 10000,得到π= 3.141435593589866
五、格雷戈里.莱布尼茨(Gregory-Leibniz)
公式如下:
代码:
1. def sign(n): 2. if n%4 == 1 : 3. return 1 4. else: 5. return -1 6. 7. def arctan_pi(N): 8. pi = 0 9. fin_sum = 0 10. for i in range(1,N+1): 11. fin_sum += (4/(sign(2*i-1)*(2*i-1))) 12. pi = fin_sum 13. return (pi)
首先,可以看到公式里出现了正负号,所以我先定义了一个判断正负号的函数sign,根据公式分母的特性,%4==1的时候为正号,所以返回1;%4==3的时候为负号,所以返回-1;之后按照公式写就好啦,理解了2*i-1就能理解上面这个代码块啦。可以先带几个数字走一下程序,相信你很快就能理解的。
经过100次迭代, π= 3.1315929035585537 ,经过10001次迭代,π= 3.1416926435905346
六、尼拉坎塔( Nilakantha)
公式:
代码:
1. import numpy as np 2. def Nilakantha_pi(N): 3. pi = 0 4. fin_sum = 0 5. if N > 1: 6. return (-1)**(N-1)*4/(2*N*(2*N+1)*(2*N+2)) + Nilakantha_pi(N-1) 7. else: 8. return 1/6+3
这里我用了递归的思想,因为这是个求和的公式;递归不仅可以很有效地减少代码量,而且会使程序更加有序;虽然说,递归能解决的问题,循环都能解决,但学好递归很有必要!对递归不熟悉的同学,可以先学一下斐波那契数列的递归求法。
代码中if判断,不断N-1,知道N=1的时候,执行else,值得注意的是,retur最后一项还要在此基础上加3,这样可以得到π的近似值。
经过100次迭代, π= 3.1415924109719824
七、韦达公式(Francois Viete)
公式:
代码:
1. from math import sqrt 2. import numpy as np 3. def Viete_pi(N): 4. a = sqrt(2) 5. all_a = sqrt(2) 6. for i in range(1,N): 7. a = sqrt(2+a) 8. all_a *= a 9. num_1 = all_a 10. num_2 = 2**N 11. pi = 2/(num_1/num_2) 12. return pi
这是个不断嵌套的累乘问题,我把a和all_a分开,让a不断嵌套,让all_a和a进行累乘;并且注意,我的分母分子是分开计算的,返回的all_a 作为num_1即为分子,分母是2**N,得到这两个数之后,代入公式中,做一个简单变换就可以计算出来pi的值。
经过100次迭代, π= 3.141592653589794。
小插曲:
在这题的求解过程中,偶然间看到另一个博主写的代码,链接如下:韦达公式求π
1. # coding:utf-8 2. from math import sqrt 3. def Vieta(): 4. #请在此添加代码 5. #********** Begin *********# 6. a = sqrt(2)/2.0 7. yield a 8. while True: 9. a = sqrt((1+a)/2.0) 10. yield a 11. #********** End *********# 12. N = int(input()) 13. v = Vieta(); p = 1.0 14. for i in range(N+1): 15. #请在此添加代码 16. #********** Begin *********# 17. p *= next(v) 18. #********** End *********# 19. print ("%.6f"%(2.0/p))
这段代码看得我有点迷糊,最主要的是yield这个关键词没有理解。
这个yield到底是个啥啊?古老的记忆开始苏醒,之前自己好像接触过,先添两个经验贴:
按照我的理解:
- 可以粗浅的看成return,程序进行到那里就不会进行了。
- 不管yield出现在函数什么地方,函数都将变成一个生成器。
- 直到调用next方法,函数才开始执行,并将yiled之后的值返回给前面的参数。
- 使用yield关键词可以有效地节省时间和空间。
八、连分数(Continued fractions)
公式:
代码:
1. import numpy as np 2. def CF_pi_1(k, N): 3. pi = 0 4. if k < N: 5. k += 1 6. return 2*k-3 + (k-1)**2/CF_pi_1(k,N) 7. else: 8. return 2*k-1 9. def CF_pi(k, N): 10. return 4/CF_pi_1(k,N)
同样是用递归处理,但这次和上面有所不同,他的最终结果要用4去除,但是无论放在if-else的哪一个return语句里面,稍微推算一下都能很容易发现达不到预期的效果。所以又创建了一个函数,用来接受上一个函数的值并处理。这样可以把这两个部分分开。如果题目要求仅使用一个函数来完成的话,我现在也没有什么思路。如果读者有什么好的想法,也欢迎私信交流。
这段代码如果理解不了的话,可以先看看2013年蓝桥杯的真题:2013年蓝桥杯 黄金连分数
经过5次迭代, π= 3.142342342342342。
九、各方法比较
求π的各方法比较
方法 | 取点 / 区间 / 迭代次数 | 所求的π的近似值 |
math库 | 直接调用就可以 | 3.141592653589793 |
蒙特卡洛(Monte Carlo) | 取5000个点 | 3.1624 |
黎曼和(定积分) | 划分10000个区间 | 3.1415929980246284 |
约翰.沃利斯(John Wallis) | 迭代10000次 | 3.141435593589866 |
格雷戈里.莱布尼茨(Gregory-Leibniz) | 迭代10001次 | 3.1416926435905346 |
尼拉坎塔( Nilakantha) | 迭代100次 | 3.1415924109719824 |
韦达公式(Francois Viete) | 迭代100次 | 3.141592653589794 |
连分数(Continued fractions) | 迭代5次 | 3.142342342342342 |
十、总结
上述就是今天所要介绍的pi的几种求法,在日常生活中如果要用到π,绝大多数用math.pi就能解决,十分方便。蒙特卡洛方法在数学建模中经常会用到,如果你感兴趣,也可以去看看蒲丰投针的概率问题。黎曼和也就是定积分,不断划分区域,近似替代总和。从约翰.沃利斯到连分数其实都可以归结为是级数求π的问题,区别在于收敛的速度快慢,有的需要迭代上百上万次才能有一个好一点的预测值,而有的只需要5次10次。
之后如果我遇到了π的相关题目,我也不可能去写公式去求,我也很直接调用math.pi,但这不代表这个探索过程是没有意义的。在这个过程中,我加深了对递归的进一步理解,对yield关键词的初步认识,加强了代码逻辑能力,最重要的是在不断解决问题中的自豪感。
由于水平有限,编写过程中难免会有纰漏,欢迎广大读者不吝赐教,欢迎指正!