我正在为 bcmath
扩展编写一个包装器,关于bcpow()
的错误 #10116 特别烦人——它将$right_operand
($exp
(转换为(本机 PHP,而不是任意长度(整数,因此当您尝试计算数字的平方根(或任何其他高于 1
的根(时,您总是得到1
而不是正确的结果。
开始寻找可以让我计算一个数字的第n个根的算法,我发现这个答案看起来很可靠,我实际上使用 WolframAlpha 扩展了公式,我能够将其速度提高约 5%,同时保持结果的准确性。
这是一个纯粹的PHP实现,模仿我的BCMath实现及其局限性:
function _pow($n, $exp)
{
$result = pow($n, intval($exp)); // bcmath casts $exp to (int)
if (fmod($exp, 1) > 0) // does $exp have a fracional part higher than 0?
{
$exp = 1 / fmod($exp, 1); // convert the modulo into a root (2.5 -> 1 / 0.5 = 2)
$x = 1;
$y = (($n * _pow($x, 1 - $exp)) / $exp) - ($x / $exp) + $x;
do
{
$x = $y;
$y = (($n * _pow($x, 1 - $exp)) / $exp) - ($x / $exp) + $x;
} while ($x > $y);
return $result * $x; // 4^2.5 = 4^2 * 4^0.5 = 16 * 2 = 32
}
return $result;
}
上述方法似乎效果很好,除非1 / fmod($exp, 1)
不产生整数。例如,如果$exp
是0.123456
,它的逆数将是8.10005
的,pow()
和_pow()
的结果会有点不同(demo(:
-
pow(2, 0.123456)
=1.0893412745953
-
_pow(2, 0.123456)
=1.0905077326653
-
_pow(2, 1 / 8)
=_pow(2, 0.125)
=1.0905077326653
如何使用"手动"指数计算达到相同的精度水平?
用于查找(正(数a
的第 n个根的算法是用于查找零的牛顿算法
f(x) = x^n - a.
这只涉及以自然数作为指数的幂,因此很容易实现。
使用指数0 < y < 1
计算幂,其中y
不是整数1/n
n
的形式更为复杂。做模拟,解决
x^(1/y) - a == 0
将再次涉及计算具有非整数指数的幂,这正是我们试图解决的问题。
如果y = n/d
是有理的,分母d
小,则通过计算很容易解决问题
x^(n/d) = (x^n)^(1/d),
但是对于大多数有理0 < y < 1
,分子和分母相当大,中间x^n
会很大,因此计算会使用大量内存并且需要(相对(很长时间。(对于0.123456 = 1929/15625
的例子指数,它不是太糟糕,但0.1234567
会相当费力。
计算一般有理0 < y < 1
的幂的一种方法是写
y = 1/a ± 1/b ± 1/c ± ... ± 1/q
用整数a < b < c < ... < q
和乘/除各个x^(1/k)
.(每个有理0 < y < 1
都有这样的表示,最短的这种表示通常不涉及很多项,例如
1929/15625 = 1/8 - 1/648 - 1/1265625;
在分解中仅使用加法会导致具有较大分母的更长表示,例如
1929/15625 = 1/9 + 1/82 + 1/6678 + 1/46501020 + 1/2210396922562500,
所以这将涉及更多的工作。
通过混合这些方法可以进行一些改进,首先通过y
的连续分数展开找到一个接近有理近的y
小分母 - 例如指数1929/15625 = [0;8,9,1,192]
并使用前四个偏商产生近似10/81 = 0.123456790123...
[注意,10/81 = 1/8 - 1/648
,最短分解为纯分数的部分和是收敛的] - 然后将余数分解为纯分数。
然而,一般来说,这种方法会导致计算大n
的第n个根,如果最终结果的预期精度很高,这也是缓慢和内存密集型的。
总而言之,实现exp
和log
和使用可能更简单、更快捷。
x^y = exp(y*log(x))