由于许多除法运算,常数e作为无穷级数之和的标准表示对于计算是非常低效的。那么有没有其他方法可以有效地计算常数?
谢谢!
修改
在关注了一些链接后,我相信效率来自于一种称为二进制分裂的技术(虽然表示仍然是系列),这是我不熟悉的。如果有人熟悉它,请随时贡献。
答案 0 :(得分:23)
由于无法计算的每个数字,因此您将不得不选择一个停止点。
对于实际应用,“64位双精度浮点值尽可能接近'e'的真值 - 大约16位小数”是绰绰有余。
正如KennyTM所说,这个值已经在数学库中预先计算好了。 如果你想自己计算,正如Hans Passant指出的那样,阶乘已经快速增长。 该系列中的前22个项已经过度计算到该精度 - 如果它存储在64位双精度浮点变量中,则从该系列中添加更多项不会改变结果。 我认为你需要花费更长的时间来眨眼,而不是让你的电脑做22次分裂。所以我认为没有任何理由进一步优化这一点。
正如Matthieu M.指出的那样,这个值已经计算好了,您可以从Yee的网站上下载。
如果您想自己计算,那么许多数字将不适合标准的双精度浮点数。 你需要一个“bignum”图书馆。 与往常一样,您可以使用已有的许多免费bignum库中的一个,或者通过构建您自己的另一个具有自己特殊怪癖的bignum库来重新发明轮子。
结果 - 一个长数字文件 - 不是非常有用,但计算它的程序有时被用作测试“bignum”库软件的性能和准确性的基准,并作为压力测试来检查稳定性和新机器硬件的冷却能力。
一页非常简要地描述了the algorithms Yee uses to calculate mathematical constants。
维基百科"binary splitting" article更详细。 我认为您要寻找的部分是数字表示: 而不是将所有数字内部存储为小数点前后的一长串数字(或二进制点), Yee将每个术语和每个部分和存储为有理数 - 作为两个整数,每个整数是一长串数字。 例如,假设其中一个工作CPU被分配了部分和,
... 1/4! + 1/5! + 1/6! + ... .
而不是先为每个术语进行除法,然后添加,然后将一百万位定点结果返回给经理CPU:
// extended to a million digits
1/24 + 1/120 + 1/720 => 0.0416666 + 0.0083333 + 0.00138888
CPU可以首先使用有理算法将系列中的所有项加在一起,并将有理结果返回给管理器CPU:两个整数,每个可能有几百个数字:
// faster
1/24 + 1/120 + 1/720 => 1/24 + 840/86400 => 106560/2073600
以这种方式将数千个术语加在一起后,管理器CPU会在最后一个分区进行分割,得到小数点后的小数位数。
请记住要避免使用PrematureOptimization和 总是ProfileBeforeOptimizing。
答案 1 :(得分:10)
我不知道比该系列的泰勒展开更快的计算,即:
e = 1/0! + 1/1! + 1/2! + ...
或
1 / e = 1/0! - 1/1! + 1/2! - 1/3! + ...
考虑到这些是由计算the first 500 billion digits of e的A. Yee使用的,我认为没有太多的优化要做(或者更好,它可以优化,但没有人找到方法,AFAIK)
修改强>
非常粗略的实施
#include <iostream>
#include <iomanip>
using namespace std;
double gete(int nsteps)
{
// Let's skip the first two terms
double res = 2.0;
double fact = 1;
for (int i=2; i<nsteps; i++)
{
fact *= i;
res += 1/fact;
}
return res;
}
int main()
{
cout << setprecision(50) << gete(10) << endl;
cout << setprecision(50) << gete(50) << endl;
}
输出
2.71828152557319224769116772222332656383514404296875
2.71828182845904553488480814849026501178741455078125
答案 2 :(得分:10)
如果您使用的是double
或float
,M_E
已经存在math.h
常量。
#define M_E 2.71828182845904523536028747135266250 /* e */
http://en.wikipedia.org/wiki/Representations_of_e#As_an_infinite_series中还有e
的其他代表;所有这些都将涉及分裂。
答案 3 :(得分:8)
This page有一个很好的不同计算方法的概述。
这是来自Xavier Gourdon的一个小型C程序,用于计算计算机上e的9000位小数。对于π和由超几何系列的平均值定义的一些其他常数,存在相同类型的程序。
[来自https://codereview.stackexchange.com/a/33019的
#include <stdio.h> int main() { int N = 9009, a[9009], x; for (int n = N - 1; n > 0; --n) { a[n] = 1; } a[1] = 2; while (N > 9) { int n = N--; while (--n) { a[n] = x % n; x = 10 * a[n-1] + x/n; } printf("%d", x); } return 0; }
此程序 [代码高尔夫] 有117个字符。它可以更改为计算更多数字(将值9009更改为更多)并更快(将常量10更改为10的另一个幂和printf命令)。一个不太明显的问题是找到使用的算法。
答案 4 :(得分:6)
我在question regarding computing e by its definition via Taylor series的CodeReviews上给出了这个答案(因此,其他方法不是一个选项)。评论中提出了这里的交叉帖子。我删除了与其他主题相关的评论;那些对进一步解释migth感兴趣的人想查看原帖。
C中的解决方案(应该很容易适应C ++):
#include <stdio.h>
#include <math.h>
int main ()
{
long double n = 0, f = 1;
int i;
for (i = 28; i >= 1; i--) {
f *= i; // f = 28*27*...*i = 28! / (i-1)!
n += f; // n = 28 + 28*27 + ... + 28! / (i-1)!
} // n = 28! * (1/0! + 1/1! + ... + 1/28!), f = 28!
n /= f;
printf("%.64llf\n", n);
printf("%.64llf\n", expl(1));
printf("%llg\n", n - expl(1));
printf("%d\n", n == expl(1));
}
输出:
2.7182818284590452354281681079939403389289509505033493041992187500
2.7182818284590452354281681079939403389289509505033493041992187500
0
1
有两个要点:
此代码不计算1,1 * 2,1 * 2 * 3,...即O(n ^ 2),但一次计算1 * 2 * 3 * .... (即O(n))。
它从较小的数字开始。如果我们试图计算
1/1 + 1/2 + 1/6 + ... + 1/20!
并尝试添加它1/21!,我们将添加
1/21! = 1/51090942171709440000 = 2E-20,
到2.something,它对结果没有影响(double
包含大约16位有效数字)。此效果称为underflow。
然而,当我们从这些数字开始时,即,如果我们计算1/32!+ 1/31!+ ......它们都会产生一些影响。
这个解决方案似乎与我在我的64位机器上用expl
函数计算的内容一样,用gcc 4.7.2 20120921编译。
答案 5 :(得分:3)
您可能会获得一些效率。由于每个项都涉及 next 阶乘,因此可以通过记住阶乘的最后一个值来获得一些效率。
e = 1 + 1/1! + 1/2! + 1/3! ...
扩展等式:
e = 1 + 1/(1 * 1) + 1/(1 * 1 * 2) + 1/(1 * 2 * 3) ...
分母不是计算每个阶乘,而是乘以下一个增量。因此,将分母保持为变量并乘以它将产生一些优化。
答案 6 :(得分:2)
如果你可以使用最多七位数的近似值,请使用
3-sqrt(5/63)
2.7182819
如果您想要准确的值:
e = (-1)^(1/(j*pi))
其中j是虚数单位,pi是众所周知的数学常数(Euler's Identity)
答案 7 :(得分:1)
二进制拆分方法非常适合于模板元程序,该模板元程序产生一种类型,该类型表示对应于e的近似的有理数。 13次迭代似乎是最大值 - 任何更高的迭代都会产生“积分常数溢出”错误。
#include <iostream>
#include <iomanip>
template<int NUMER = 0, int DENOM = 1>
struct Rational
{
enum {NUMERATOR = NUMER};
enum {DENOMINATOR = DENOM};
static double value;
};
template<int NUMER, int DENOM>
double Rational<NUMER, DENOM>::value = static_cast<double> (NUMER) / DENOM;
template<int ITERS, class APPROX = Rational<2, 1>, int I = 2>
struct CalcE
{
typedef Rational<APPROX::NUMERATOR * I + 1, APPROX::DENOMINATOR * I> NewApprox;
typedef typename CalcE<ITERS, NewApprox, I + 1>::Result Result;
};
template<int ITERS, class APPROX>
struct CalcE<ITERS, APPROX, ITERS>
{
typedef APPROX Result;
};
int test (int argc, char* argv[])
{
std::cout << std::setprecision (9);
// ExpType is the type containing our approximation to e.
typedef CalcE<13>::Result ExpType;
// Call result() to produce the double value.
std::cout << "e ~ " << ExpType::value << std::endl;
return 0;
}
另一个(非元程序)模板化变体将在编译时计算双重逼近e。这个没有迭代次数的限制。
#include <iostream>
#include <iomanip>
template<int ITERS, long long NUMERATOR = 2, long long DENOMINATOR = 1, int I = 2>
struct CalcE
{
static double result ()
{
return CalcE<ITERS, NUMERATOR * I + 1, DENOMINATOR * I, I + 1>::result ();
}
};
template<int ITERS, long long NUMERATOR, long long DENOMINATOR>
struct CalcE<ITERS, NUMERATOR, DENOMINATOR, ITERS>
{
static double result ()
{
return (double)NUMERATOR / DENOMINATOR;
}
};
int main (int argc, char* argv[])
{
std::cout << std::setprecision (16);
std::cout << "e ~ " << CalcE<16>::result () << std::endl;
return 0;
}
在优化构建中,表达式CalcE<16>::result ()
将替换为实际的double值。
因为他们在编译时计算e,所以两者都可以说是非常有效的: - )
答案 8 :(得分:1)
从我的观点来看,计算e达到所需精度的最有效方法是使用以下表示:
e := lim (n -> inf): (1 + (1/n))^n
特别是如果你选择n = 2^x
,你可以用x乘法来计算效力,因为:
a^n = (a^2)^(n/2), if n % 2 = 0
答案 9 :(得分:1)
有几种“套管”算法以无限制的方式顺序计算数字。这很有用,因为您可以通过恒定数量的基本算术运算简单地计算“下一个”数字,而无需事先定义您希望生成多少位数。
这些应用了一系列连续的变换,使下一个数字到达1的位置,这样它们就不会受到浮点舍入误差的影响。效率很高,因为这些变换可以表示为矩阵乘法,它可以减少为整数加法和乘法。
简而言之,泰勒系列扩张
e = 1/0! + 1/1! + 1/2! + 1/3! ... + 1/n!
可以通过分解因子的小数部分来重写(请注意,为了使系列规则,我们已将1
移动到左侧):
(e - 1) = 1 + (1/2)*(1 + (1/3)*(1 + (1/4)...))
我们可以定义一系列函数f1(x)... fn(x),因此:
f1(x) = 1 + (1/2)x
f2(x) = 1 + (1/3)x
f3(x) = 1 + (1/4)x
...
e的值可以从所有这些函数的组成中找到:
(e-1) = f1(f2(f3(...fn(x))))
我们可以观察到每个函数中x的值由下一个函数确定,并且这些值中的每一个都在范围[1,2]上有界 - 也就是说,对于任何这些函数,它的值都是x将为1 <= x <= 2
由于是这种情况,我们可以通过分别使用x的值1和2来设置e的下限和上限:
lower(e-1) = f1(1) = 1 + (1/2)*1 = 3/2 = 1.5
upper(e-1) = f1(2) = 1 + (1/2)*2 = 2
我们可以通过组合上面定义的函数来提高精度,当数字在下限和上限匹配时,我们知道我们的e的计算值对于该数字是精确的:
lower(e-1) = f1(f2(f3(1))) = 1 + (1/2)*(1 + (1/3)*(1 + (1/4)*1)) = 41/24 = 1.708333
upper(e-1) = f1(f2(f3(2))) = 1 + (1/2)*(1 + (1/3)*(1 + (1/4)*2)) = 7/4 = 1.75
由于1和10位数字匹配,我们可以说精度为10ths的(e-1)近似值为1.7。当第一个数字在上限和下限之间匹配时,我们将其减去然后乘以10 - 这样,所讨论的数字总是在浮点精度高的1位置。
真正的优化来自线性代数中将线性函数描述为变换矩阵的技术。组合函数映射到矩阵乘法,因此所有这些嵌套函数都可以简化为简单的整数乘法和加法。减去数字并乘以10的过程也构成线性变换,因此也可以通过矩阵乘法来实现。
该方法的另一种解释: http://www.hulver.com/scoop/story/2004/7/22/153549/352
描述算法的论文: http://www.cs.ox.ac.uk/people/jeremy.gibbons/publications/spigot.pdf
通过矩阵运算执行线性变换的快速入门: https://people.math.gatech.edu/~cain/notes/cal6.pdf
注意,这个算法利用了Mobius变换,这是Gibbons论文中简要描述的一种线性变换。
答案 10 :(得分:0)
@nico回复:
...比该系列的泰勒展开式“更快”的计算,即:
e = 1/0! + 1/1! + 1/2! + ...
或
1 / e = 1/0! -1/1! + 1/2! -1/3! + ...
以下是通过代数方法改进牛顿方法收敛性的方法:
https://www.researchgate.net/publication/52005980_Improving_the_Convergence_of_Newton的s_Series_Approximation_for_e
关于它们是否可以与二进制拆分一起使用以提高计算速度似乎是一个悬而未决的问题。但是,这是Damian Conway使用Perl的示例,说明了此新方法在直接计算效率方面的改进。在标题为“?是用于估算”的部分中:
(此评论太长了,无法在10年6月12日10:28发布作为答复的答案)
答案 11 :(得分:-2)
从wikipedia替换x替换为