基于蒙特卡洛方法计算圆周率PI
1 圆周率
pi是一个无理数,是一个常数,可以解释自然,解释直线和曲线之间的关系。生活中最常见的是圆,所以叫圆周率。最初人们求圆的面积和周长时,都要用到它。用物理的测量方式,可以直接率算它,多次计算求均值,就可以得到比较精确的结果。使用实验和几何的方式,手工记录再计算,显然确实耗时耗力。现在有了计算机,可以将“多次”这个过程,交给计算机来计算。
最早,人们使用了割圆术,内接正多边形来逼近圆周长。祖老师是非常牛B的,直接将精度计算到了小数点后第6位,而且是在宋代【约1000年以前】,甩了欧洲几百年啊。
在学习了C语言的控制结构以后,就可以采用蒙特卡洛的方法【听起来很神,但原理比较简单,简称MC方法,利用的是确定性和随机性,使用概率的方式来解决问题】,来模拟,蒙特卡洛过程,比如丢针或是丢豆子过程,看看豆子在方中落入圆中的概率,用古典概率的方式,解决求pi的问题。
当然,数学家们要考虑的是
- 收敛性
- 误差
- 精度
这些方面的问题。
但作为计算机程序员,那就简单了,一个字:干。
方法:随机投点法
2 使用蒙特卡洛计算PI的原理
蒙特卡洛法(Monte Carlo method)本质上是一种以概率统计理论为指导的一类非常实用和重要的数值计算方法,又叫统计模拟方法。基本思想就是通过一系列“实验”,以某件事情出现的频率来估计一个随机事件的概率,并将其作为问题的解。
2.1丢豆子原理图
图中,红点表示落入圆形中,蓝点和红点均会落入方形中。每个点的生成是随机的,坐标始终在方中,这样的点即为“实验点”,显然,可以统计落入圆形中的计数redN和落入方形中的计数redN+blueN,随着实验点的不断增加,圆形的面积AreaCircle和方形的面积AreaSquare的比值,就应该接近于两种落入的次数【即频率】比。即
AreaCircle = pi*r*r;
AreaSquare = 2*r * 2*r;
因为:面积比 = 计数比;
所以:(pi*r*r)/(4*r*r) = pi/4 = redN/(redN+blueN);
那么:pi = 4*redN/(redN+blueN);
而用计算机生成随机坐标来模拟时
A分子即为正好落在圆中的计数
B分母即为总计数
也就是说:
可以从数学上证明:
所以,数学才是高大上啊。程序员只是应用而已。
3 编程实现
核心问题:
- 随机点如何生成
- 如何判断随机点落在了圆中
- 循环计数
- 扩大实验次数
- 更进一步:使用计算机进行多次模拟,然后取均值,是不是会更准确
解决方法:
- 使用srand()植入随机种子,使用rand()生成随机数
- 生成一对坐标后,映射到[0,1]区间上,即可进行到原点[0,0]的距离判断
- 控制循环终点,每次做一次模拟投豆子,找落点
- 多次测试,即可以搞定,也可以自动化测试
- 自动化测试过程中,再累计结果后再做均值处理
具体程序
/*
原理:基于简单的概率定律
在单位方形内随机生成点,达到一定数量后,这些点肯定是均匀地分布在方形中;
自然就会有一些点落在了1/4圆中;
在方形内随机点,落在方形里面的1/4圆的概率一样,但面积上的关系是:恰好是一个古典概率,即两个的面积比
实验假设,如果
圆的半径为单位r=1,则
1 则1/4圆的面积:AreaCircle = pi*r*r/4 = pi/4
2 则方形的面积:AreaSquare = r*r = 1
3 上下作比值,得pi的算法 pi = (4*AreaCircle)/AreaSquare
4 随机生成N个(x,y)坐标,然后测面积比,即概率比,即测这个点是否在圆内
具体实验:
落在方形中的次数,即实验次数
落在1/4圆中的次数,即计算得到的计数
两个比值,应该就是面积比,反推即可以计算PI
显然,次数越多,概率越准,PI就计算得越精确
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
int main(void)
{
int p, circle, square, toss, rm, i;
float PI, x, y, r;
char ch;
srand((unsigned int)time(NULL));
rm = RAND_MAX; // #define RAND_MAX 0x7fff (即十进制32767 二进制01111111 11111111)
do{
circle = 0;
// 输入实验次数
do{
printf("Enter the number of tosses(2<=N<=5000) : ");
scanf("%d", &toss);
} while (toss < 2 || toss > 50000); // end of in do-while
// 总实验次数
square = toss;
for ( i = 0; i < toss; i++)
{
p = rand();
x = (float)p / rm; // 生成坐标x,一定是一个[0,1]之间的数
p = rand();
y = (float)p / rm; // 生成坐标y,一定是一个[0,1]之间的数
r = sqrt((x * x) + (y * y)); // 计算离原点的距离
if (r<=1) // 判断点落在圆中的次数
{
circle++;
}
}
PI = 4 * (float)circle / square;
printf("the value of PI is : %f\n\n", PI);
printf("Do you want to continue?(Y/N): ");
getchar(); // 先读到回车符
ch = getchar();
} while ((ch == 'y') || (ch == 'Y')); //end of out do-while
printf("Thank you\n");
return 0;
}
详见程序中的注释,主要是几个库函数的应用
- srand()
- rand()
- time()
- 这里,程序用的是1/4圆,也可以扩展到整个圆,当然,计数区间会有变化
如果扩展到整个圆,可以参考如下的流程图进行程序编写
4 小结
这是随机投点法,中学生都能明白。如果会微积分,还可以使用样本平均值法。
- 可以分层抽样
- 可以关联抽样
本实验的局限性
- 计算机产生的随机点具有一定的局限性,也就是说,不能做到绝对的均匀随机
- 试验次数不可能趋近于无穷,次数仍然不够多,受限于次数【数据类型】
本示例可以训练的内容和达到的目标
- 理解和深入理解蒙特卡洛方法
- 训练和使用C语言的控制结构,主要是判断和循环
- 库函数的使用,站在巨人肩上解决实际问题
- 对不懂概率和C语言的人吹吹牛B,算法一出,瞬间高大上