6

我正在使用 PHP 在 PHP 中进行计算bcmath,并且需要e通过小数指数进行计算。不幸的是,bcpow()只接受整数指数。指数通常比浮点数允许的精度更高,因此普通的算术函数不会削减它。

例如:

$e = exp(1);
$pow = "0.000000000000000000108420217248550443400745280086994171142578125";
$result = bcpow($e, $pow);

结果"1"出现错误“bc 数学警告:指数中的非零刻度”。

我可以使用其他功能来代替bcpow()吗?

4

2 回答 2

6

您最好的选择可能是使用泰勒级数展开。正如您所指出的,PHP 的 bcpow 仅限于提高到整数幂。

所以你可以做的是滚动你自己的 bc 阶乘函数并使用 wiki 页面来实现指数函数的泰勒级数展开。

function bcfac($num) { 
    if ($num==0) return 1;
    $result = '1';
    for ( ; $num > 0; $num--)
        $result = bcmul($result,$num);
    return $result;
}
$mysum = '0';
for ($i=0; $i<300; $i++) {
    $mysum = bcadd($mysum, bcdiv(bcpow($pow,$i), bcfac($i)) );
}
print $mysum;

显然,这$i<300是无穷大的近似值……您可以更改它以满足您的性能需求。

$i=20,我得到了

1.00000000000000000010842021724855044340662275184110560868263421994092888869270293594926619547803962155136242752708629105688492780863293090291376157887898519458498571566021915144483905034693109606778068801680332504212458366799913406541920812216634834265692913062346724688397654924947370526356787052264726969653983148004800229537555582281617497990286595977830803702329470381960270717424849203303593850108090101578510305396615293917807977774686848422213799049363135722460179809890014584148659937665374616

这是令人欣慰的,因为这么小的指数应该会产生非常接近 1.0 的值。

于 2015-11-02T21:11:47.960 回答
2

老问题,但人们可能仍然感兴趣。

所以凯文用泰勒多项式得到了正确的想法,但是当你直接从它推导出算法时,你可能会遇到麻烦,主要是当对 $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) 来保证收敛。

于 2016-08-17T22:19:48.440 回答