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