PHP:如何将数字提高到(微小的)小数指数?
PHP: How to raise number to (tiny) fractional exponent?
我正在使用 bcmath
在 PHP 中进行计算,需要将 e
提高一个小数指数。不幸的是,bcpow()
只接受整数指数。指数通常比浮点数允许的精度更高,因此普通算术函数不会削减它。
例如:
$e = exp(1);
$pow = "0.000000000000000000108420217248550443400745280086994171142578125";
$result = bcpow($e, $pow);
结果为 "1"
,错误为 "bc math warning: non-zero scale in exponent"。
我可以使用其他函数代替 bcpow()
吗?
你最好的选择可能是使用泰勒级数展开。正如您所指出的,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 的值。
老问题,但人们可能仍然感兴趣。
所以 Kevin 用泰勒多项式得到了正确的想法,但是当你直接从它推导出你的算法时,你可能会遇到麻烦,主要是你的代码在使用大截止值时对于长输入字符串会变慢$i.
原因如下:
在每一步,我的意思是每一个新的 $i,代码都会调用 bcfac($i)。每次调用 bcfac 时,它都会执行 $i-1 次计算。而 $i 一直上升到 299……这几乎是 45000 次操作!不是你快速'n'简单的浮点运算,而是缓慢的 BC 字符串运算 - 如果你设置 bcscale(100) 你的 bcmul 必须处理多达 10000 对字符!
bcpow 也随着 $i 的增加而变慢。不如 bcfac,因为它可能使用类似于平方和乘法的方法,但它仍然增加了一些东西。
总的来说,所需时间随着计算的多项式项的数量呈二次方增长。
那么……怎么办?
这里有一个提示:
无论何时处理多项式,尤其是泰勒多项式,请使用 Horner 方法。
它转换为: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 次单独的阶乘。
Horner 的方法再次扭转局面!
一些其他想法:
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 并计算所需迭代次数的算法会很好。已经有了一些涉及 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 作为开发点)
使用 Horner 的方法来显着提高性能。
将结果变成一个 bc 操作循环。
在处理 x > 1 时也使用 ln(x) = -ln(1/x) 来保证收敛。
有用的功能(不要忘记在使用前设置 bcscale())
function bc_fact($f){return $f==1?1:bcmul($f,bc_fact(bcsub($f, '1')));}
function bc_exp($x,$L=50){$r=bcadd('1.0',$x);for($i=0;$i<$L;$i++){$r=bcadd($r,bcdiv(bcpow($x,$i+2),bc_fact($i+2)));}return $r;}#e^x
function bc_ln($x,$L=50){$r=0;for($i=0;$i<$L;$i++){$p=1+$i*2;$r = bcadd(bcmul(bcdiv("1.0",$p),bcpow(bcdiv(bcsub($x,"1.0"),bcadd($x,"1.0")),$p)),$r);}return bcmul("2.0", $r);}#2*Sum((1/(2i+1))*(((x-1)/x+1)^(2i+1)))
function bc_pow($x,$p){return bc_exp(bcmul((bc_ln(($x))), $p));}
我正在使用 bcmath
在 PHP 中进行计算,需要将 e
提高一个小数指数。不幸的是,bcpow()
只接受整数指数。指数通常比浮点数允许的精度更高,因此普通算术函数不会削减它。
例如:
$e = exp(1);
$pow = "0.000000000000000000108420217248550443400745280086994171142578125";
$result = bcpow($e, $pow);
结果为 "1"
,错误为 "bc math warning: non-zero scale in exponent"。
我可以使用其他函数代替 bcpow()
吗?
你最好的选择可能是使用泰勒级数展开。正如您所指出的,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 的值。
老问题,但人们可能仍然感兴趣。
所以 Kevin 用泰勒多项式得到了正确的想法,但是当你直接从它推导出你的算法时,你可能会遇到麻烦,主要是你的代码在使用大截止值时对于长输入字符串会变慢$i.
原因如下: 在每一步,我的意思是每一个新的 $i,代码都会调用 bcfac($i)。每次调用 bcfac 时,它都会执行 $i-1 次计算。而 $i 一直上升到 299……这几乎是 45000 次操作!不是你快速'n'简单的浮点运算,而是缓慢的 BC 字符串运算 - 如果你设置 bcscale(100) 你的 bcmul 必须处理多达 10000 对字符!
bcpow 也随着 $i 的增加而变慢。不如 bcfac,因为它可能使用类似于平方和乘法的方法,但它仍然增加了一些东西。
总的来说,所需时间随着计算的多项式项的数量呈二次方增长。
那么……怎么办?
这里有一个提示:
无论何时处理多项式,尤其是泰勒多项式,请使用 Horner 方法。
它转换为: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 次单独的阶乘。
Horner 的方法再次扭转局面!
一些其他想法:
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 并计算所需迭代次数的算法会很好。已经有了一些涉及 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 作为开发点) 使用 Horner 的方法来显着提高性能。 将结果变成一个 bc 操作循环。 在处理 x > 1 时也使用 ln(x) = -ln(1/x) 来保证收敛。
有用的功能(不要忘记在使用前设置 bcscale())
function bc_fact($f){return $f==1?1:bcmul($f,bc_fact(bcsub($f, '1')));}
function bc_exp($x,$L=50){$r=bcadd('1.0',$x);for($i=0;$i<$L;$i++){$r=bcadd($r,bcdiv(bcpow($x,$i+2),bc_fact($i+2)));}return $r;}#e^x
function bc_ln($x,$L=50){$r=0;for($i=0;$i<$L;$i++){$p=1+$i*2;$r = bcadd(bcmul(bcdiv("1.0",$p),bcpow(bcdiv(bcsub($x,"1.0"),bcadd($x,"1.0")),$p)),$r);}return bcmul("2.0", $r);}#2*Sum((1/(2i+1))*(((x-1)/x+1)^(2i+1)))
function bc_pow($x,$p){return bc_exp(bcmul((bc_ln(($x))), $p));}