老问题,但人们可能仍然感兴趣。
所以凯文用泰勒多项式得到了正确的想法,但是当你直接从它推导出算法时,你可能会遇到麻烦,主要是当对 $i 使用大的截止值时,你的代码对于长输入字符串会变慢。
原因如下:在每一步,我的意思是对于每个新的 $i,代码都会调用 bcfac($i)。每次调用 bcfac 时,它都会执行 $i-1 计算。并且 $i 一直上升到 299 ......这几乎是 45000 次操作!不是您快速简单的浮点运算,而是缓慢的 BC 字符串运算 - 如果您设置 bcscale(100) 您的 bcmul 必须处理多达 10000 对字符!
bcpow 也会随着 $i 的增加而减慢。不如 bcfac 那么多,因为它可能使用类似于平方和乘法的方法,但它仍然添加了一些东西。
总体而言,所需时间随计算的多项式项的数量呈二次方增长。
那么该怎么办?
这里有一个提示:
每当您处理多项式,尤其是泰勒多项式时,请使用霍纳方法。
它转换为:exp(x) = x^0/0!+ x^1/1!+ x^2/2!+ x^3/3!+ ...
...变成: exp(x) = ((( ... )*x/3+1 )*x/2+1 )*x/1+1
突然之间,您根本不需要任何幂或阶乘!
function bc_exp($number) {
$result = 1;
for ($i=299; $i>0; $i--)
$result = bcadd(bcmul(bcdiv($result, $i), $number), 1);
return $result;
}
无论 $i 是什么,这对于每个步骤都只需要 3 次 bc 操作。初始值为 $i=299(以与 kevin 的代码相同的精度计算 exp),我们现在只需要 897 次 bc 操作,而超过 45000 次。即使使用 30 作为截止值而不是 300,我们现在只需要 87 次 bc 操作,而其他代码仅对阶乘仍需要 822 次。
霍纳的方法再次拯救了一天!
其他一些想法:
1) Kevin 的代码可能会在 input="0" 时崩溃,这取决于 bcmath 如何处理错误,因为代码在第一步尝试 bcpow(0,0) ($i=0)。
2) 较大的指数需要较长的多项式,因此需要更多的迭代,例如 bc_exp(300) 会给出错误的答案,即使 $i=299,为什么像 bc_exp(3) 这样的东西也能正常工作。每项加 x^n/n! 到结果,所以在多项式开始收敛之前,这个项必须变小。现在比较两个连续的术语:
( x^(n+1)/(n+1)! ) / ( x^n/n! ) = x/n
每个被加数都比前一个要大 x/n 倍(我们通过霍纳方法使用),所以为了 x^(n+1)/(n+1)!要变小 x/n 也必须变小,只有当 n>x 时才会出现这种情况。
结论:只要迭代次数小于输入值,结果就会发散。只有当您添加步骤直到您的迭代次数大于输入时,算法才会开始缓慢收敛。
为了达到让愿意使用 bcmath 的人满意的结果,您的 $i 需要比您的 $number 大得多。当您尝试计算诸如 e^346674567801 之类的东西时,这是一个巨大的问题
一种解决方案是将输入分为整数部分和小数部分。比在整数部分使用 bcpow 并在小数部分使用 bc_exp ,由于小数部分小于 1,它现在从一开始就收敛了。最后将结果相乘。
e^x = e^(intpart+fracpart) = e^intpart * e^fracpart = bcpow(e,intpart) * bc_exp(fracpart)
你甚至可以直接在上面的代码中实现它:
function bc_exp2($number) {
$parts = explode (".", $number);
$fracpart = "0.".$parts[1];
$result = 1;
for ($i=299; $i>0; $i--)
$result = bcadd(bcmul(bcdiv($result, $i), $fracpart), 1);
$result = bcmul(bcpow(exp(1), $parts[0]), $result);
return $result;
}
请注意,exp(1) 为您提供了一个浮点数,它可能无法满足您作为 bcmath 用户的需求。根据您的 bcscale 设置,您可能希望使用更准确的 e 值。
3)谈论迭代次数:在大多数情况下,300 次将是多余的,而在其他一些情况下甚至可能还不够。采用 bcscale 和 $number 并计算所需迭代次数的算法会很好。Alraedy 得到了一些涉及 log(n!) 的想法,但还没有具体的想法。
4) 要将此方法与任意基数一起使用,您可以使用 a^x = e^(x*ln(a))。在使用 bc_exp(而不是在 bc_exp2 中这样做)之前,您可能希望将 x 分为 intpart 和 fracpart,以避免不必要的函数调用。
function bc_pow2($base,$exponent) {
$parts = explode (".", $exponent);
if ($parts[1] == 0){
$result = bcpow($base,$parts[0]);
else $result = bcmul(bc_exp(bcmul(bc_ln($base), "0.".$parts[1]), bcpow($base,$parts[0]);
return result;
}
现在我们只需要编写bc_ln。我们可以使用与上面相同的策略:
取自然对数函数的泰勒多项式。(由于 ln(0) 没有定义,所以取 1 作为发展点)使用霍纳的方法来大幅度提高性能。将结果变成一个 bc 操作循环。在处理 x > 1 时还要使用 ln(x) = -ln(1/x) 来保证收敛。