【问题标题】:'Grokkable' algorithm to understand exponentiation where the exponent is floating point'Grokkable' 算法来理解指数是浮点的指数
【发布时间】:2015-04-25 04:22:29
【问题描述】:

先澄清一下:

  • 2^3 = 8。这相当于 2*2*2。简单。
  • 2^4 = 16。这相当于 2*2*2*2。也很简单。
  • 2^3.5 = 11.313708...呃,这不太容易理解。

我想要的是一个简单的算法,它最清楚地显示了 2^3.5 = 11.313708。除了基本的加法、减法、乘法或除法运算符之外,它最好不要使用任何函数。

代码当然不一定要快,也不一定要短(尽管这会有所帮助)。别担心,它可以近似于给定的用户指定准确度(这也应该是算法的一部分)。我希望会有一个二进制印章/搜索类型的事情发生,因为这很容易理解。

到目前为止,我已经找到了this,但最佳答案远非从概念层面上简单理解。

答案越多越好,所以我可以尝试了解解决问题的不同方法。

我对答案的语言偏好是 C#/C/C++/Java,或者我所关心的伪代码。

【问题讨论】:

  • 看起来您正在尝试理解数学本身。也许 SO 不是最好的地方。更一般地说,也许查看伪代码或算法不是要走的路。查找有关该主题的易于理解的数学文章。
  • 我认为他正在寻找完全相反的东西。大多数求幂实现依赖于不容易提出的数学关系。他正在寻找一种可能不是最好的算法,但可以帮助他了解程序员如何用尽可能少的数学技巧来实现它。

标签: c# c++ c algorithm exponentiation


【解决方案1】:

好的,让我们只使用二进制搜索、加法和乘法来实现 pow(x, y)。

在 1 以下驾驶y

首先,把这个排除在外:

pow(x, y) == pow(x*x, y/2)
pow(x, y) == 1/pow(x, -y)

这对于处理负指数并将y 驱动到1 以下很重要,事情开始变得有趣。这将问题减少到在0<y<1 中查找pow(x, y)

实现sqrt

在这个答案中,我假设您知道如何执行sqrt。我知道sqrt(x) = x^(1/2),但很容易实现它,只需使用二分查找即可使用y*y=x 搜索功能找到y = sqrt(x),例如:

#define EPS 1e-8

double sqrt2(double x) {
    double a = 0, b = x>1 ? x : 1; 
    while(abs(a-b) > EPS) {
        double y = (a+b)/2;
        if (y*y > x) b = y; else a = y;
    }
    return a;
}

寻找答案

基本原理是每个小于 1 的数字都可以近似为分数之和1/2^x

0.875 = 1/2 + 1/4 + 1/8
0.333333... = 1/4 + 1/16 + 1/64 + 1/256 + ...

如果你找到这些分数,你实际上会发现:

x^0.875 = x^(1/2+1/4+1/8) = x^(1/2) * x^(1/4) * x^(1/8)

这最终导致

sqrt(x) * sqrt(sqrt(x)) * sqrt(sqrt(sqrt(x)))

所以,实现(在 C++ 中)

#define EPS 1e-8

double pow2(double x, double y){
    if (x < 0 and abs(round(y)-y) < EPS) {
        return pow2(-x, y) * ((int)round(y)%2==1 ? -1 : 1);
    } else if (y < 0) {
        return 1/pow2(x, -y);
    } else if(y > 1) {
        return pow2(x * x, y / 2);
    } else {
        double fraction = 1;
        double result = 1;

        while(y > EPS) {
            if (y >= fraction) {
                y -= fraction;
                result *= x;
            }

            fraction /= 2;
            x = sqrt2(x);
        }
        return result;
    }
}

【讨论】:

  • @DanW 抱歉,sqrt2 的值小于 1 时出现错误。现在已修复。
  • 也许还可以修复负根的奇整数幂(它们目前返回 0)。
  • 这会有点问题,因为它涉及在复杂域中实现sqrt。即使我们不使用算法返回任何复数。
  • 你不能简单地求根,然后如果指数是奇数,那么最后的结果是否为负?在这种情况下,如果指数也是负数,那么也做 1/result(除非它是负数,甚至,你不需要做任何这些改变)。
  • 问题是真正的指数pow(-27, 1/3.0) == -3,例如。但要从算法中得出结论,我们需要处理很多复数。
【解决方案2】:

您可以非常轻松地验证 2^3.5 = 11.313708:检查 11.313708^2 = (2^3.5)^2 = 2^7 = 128

我认为了解您实际为此进行的计算的最简单方法是刷新您对对数的理解 - 一个起点是http://en.wikipedia.org/wiki/Logarithm#Exponentiation

如果您真的想用最少的技术计算非整数幂,一种方法是将它们表示为分母为 2 的幂的分数,然后取大量平方根。例如。 x^3.75 = x^3 * x^(1/2) * x^(1/4) 然后 x^(1/2) = sqrt(x), x^(1/4) = sqrt(sqrt(x )) 等等。

这是另一种方法,基于验证猜测的想法。给定 y,您想找到 x 使得 x^(a/b) = y,其中 a 和 b 是整数。这个等式意味着 x^a = y^b。您可以计算 y^b,因为您知道这两个数字。你知道 a,所以你可以 - 正如你最初所怀疑的那样 - 使用二进制截断或一些数值上更有效的算法来解决 x^a = y^b 通过简单地猜测 x,计算 x^a 这个猜测,将它与y^b,然后迭代改进猜测。

示例:假设我们希望通过这种方法找到 2^0.878。然后设置 a = 439, b = 500,所以我们希望找到 2^(439/500)。如果我们设置 x=2^(439/500) 我们有 x^500 = 2^439,因此计算 2^439 并(通过二进制或其他方式)找到 x 使得 x^500 = 2^439。

【讨论】:

  • 这适用于 3.5,但是指数在哪里像 3.8784569 这样可怕呢?我不热衷于使用 sqrt,因为这会使事情进一步复杂化。
  • 将 3.8784569 表示为带有二进制点的二进制数:3.8784569 = 11.11....(因为 3.878 > 3.75)并取大量的平方根 - 或者学习对数和指数,即无论如何都值得去做。
  • 如果可以的话,我想避免使用 sqrt 和 log,不是因为这对我来说是额外的工作,而是因为我想尝试将算法推广到之后以最直观的方式处理四分法。
  • 我添加了另一种基于验证猜测的方法,它不需要平方根。对en.wikipedia.org/wiki/Tetration#Extension_to_real_heights 的快速浏览表明,人们已经考虑将 tetration 推广到真实的高度,并且可能无法同时获得您可能希望的所有属性 - 当然这可能意味着以前没有考虑过一些推广可能会吸引你。
  • 我对你的第二种方法有点困惑。哪些变量是基数和指数?还有什么是a?您是否在考虑类似于我自己的实现 here 的东西?
【解决方案3】:

从其他优秀帖子中获得想法,我想出了自己的实现。答案基于base^(exponent*accuracy) = answer^accuracy 的想法。鉴于我们事先知道baseexponentaccuracy 变量,我们可以执行搜索(二进制截断或其他),以便通过找到answer 来平衡等式。我们希望等式两边的指数都是整数(否则我们将返回平方数),因此我们可以将accuracy 设为任何我们喜欢的大小,然后将其四舍五入到最接近的整数。

我给出了两种方法。第一个非常慢,并且通常会产生非常大的数字,这不适用于大多数语言。另一方面,它不使用日志,概念上更简单。

public double powSimple(double a, double b)
{
    int accuracy = 10;

    bool negExponent = b < 0;
    b = Math.Abs(b);
    bool ansMoreThanA = (a>1 && b>1) || (a<1 && b<1);   // Example 0.5^2=0.25 so answer is lower than A.
    double accuracy2 = 1.0 + 1.0 / accuracy;
    double total = a;
    for (int i = 1; i < accuracy* b; i++) total = total*a;

    double t = a;           
    while (true) {
        double t2 = t;
        for(int i = 1; i < accuracy; i++) t2 = t2 * t; // Not even a binary search. We just hunt forwards by a certain increment
        if((ansMoreThanA && t2 > total) || (!ansMoreThanA && t2 < total)) break;
        if (ansMoreThanA) t *= accuracy2; else t /= accuracy2;
    }
    if (negExponent) t = 1 / t;
    return t;
}

因为它使用 log(),所以下面的这个有点复杂。但是速度要快得多,并且不会遇到上述超高数量的问题。

public double powSimple2(double a, double b)
{
    int accuracy = 1000000;

    bool negExponent= b<0;
    b = Math.Abs(b);
    double accuracy2 = 1.0 + 1.0 / accuracy;
    bool ansMoreThanA = (a>1 && b>1) || (a<1 && b<1);   // Example 0.5^2=0.25 so answer is lower than A.

    double total = Math.Log(a) * accuracy * b;

    double t = a;
    while (true) {
        double t2 = Math.Log(t) * accuracy;
        if ((ansMoreThanA && t2 > total) || (!ansMoreThanA && t2 < total)) break;
        if (ansMoreThanA) t *= accuracy2; else t /= accuracy2;
    }
    if (negExponent) t = 1 / t;
    return t;
}

【讨论】:

  • 不知道为什么或谁反对这一点。它按预期执行,并且可能是所有答案中最短和最简单的。也许他们愿意发表评论。
【解决方案4】:

其中大部分归结为能够反转电源操作。

换句话说,基本思想是(例如)N2 应该基本上是 N1/2 的“相反”,这样如果你做某事喜欢:

M = N2

L = M1/2

那么你在 L 中得到的结果应该与 N 中的原始值相同(忽略任何舍入等)。

在数学上,这意味着 N1/2sqrt(N) 相同,N1/3 是 N 的立方根,依此类推。

之后的下一步将类似于 N3/2。这几乎是相同的想法:分母是根,分子是幂,所以 N3/2 是 N 的立方(或平方根的立方)的平方根N 个——结果相同)。

对于小数,我们只是用一种稍微不同的形式来表示一个分数,所以像 N3.14 这样的东西可以被视为 N314/100——百分之一N 的根的 314 次幂。

至于您如何计算这些:有很多不同的方法,很大程度上取决于您喜欢在复杂性(芯片面积,如果您在硬件中实现它)和速度之间的折衷。显而易见的方法是使用对数:AB = Log-1(Log(A)*B)

对于一组更受限制的输入,例如只求 N 的平方根,您通常可以比那种极其通用的方法做得更好。例如,binary reducing method 非常快——在软件中实现,它仍然与英特尔的 FSQRT 指令的速度差不多。

【讨论】:

  • 看起来很有趣 - 我很快就会深入了解。如果可能的话,我确实想避免使用 log/pow。
  • 我想我可能已经根据您所说的关于指数为 314/100 的内容实现了您的总体想法。或许可以通过look here 确认。
【解决方案5】:

正如 cmets 中所述,不清楚您是想要分数幂如何工作的数学描述,还是计算分数幂的算法。

我会假设后者。

对于几乎所有函数(如 y = 2^x),都有一种方法可以使用泰勒级数 http://en.wikipedia.org/wiki/Taylor_series 来近似函数。这将任何合理表现的函数近似为多项式,并且多项式可以仅使用乘法、除法、加法和减法来计算(所有这些 CPU 都可以直接执行)。如果您计算 y = 2^x 的泰勒级数并插入 x = 3.5,您将得到 11.313...

这几乎肯定不是在您的计算机上实际执行求幂的方式。对于不同的输入,有许多算法运行得更快。例如,如果您使用泰勒级数计算 2^3.5,那么您必须查看许多项才能以任何准确度计算它。但是,泰勒级数在 x = 0.5 时的收敛速度比 x = 3.5 时快得多。所以一个明显的改进是将 2^3.5 计算为 2^3 * 2^0.5,因为 2^3 很容易直接计算。现代求幂算法将使用很多很多技巧来加速处理 - 但原理仍然大致相同,将求幂函数近似为某个无限和,并根据需要计算尽可能多的项以获得所需的精度。

【讨论】:

    猜你喜欢
    • 2019-07-25
    • 2011-05-24
    • 1970-01-01
    • 2011-05-24
    • 2016-03-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多