【问题标题】:Why is my arithmetic with a long long int behaving this way?为什么我的 long long int 算术会以这种方式表现?
【发布时间】:2019-09-22 01:47:55
【问题描述】:

我正在尝试使用 long long 数据类型计算大整数,但是当它变得足够大 (2^55) 时,算术行为是不可预测的。我在 Microsoft Visual Studio 2017 工作。

在第一种情况下,我在初始化中从long long 变量m 中减去2。这适用于所有n,直到我尝试54,然后m 将不会被2 减去。

#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <map>
#include <set>

using namespace std;

#define LL long long

int main()
{
    LL n;
    cin >> n;
    LL m = pow(2, n + 1) - 2;
    cout << m;
    return 0;
}

但是,使用此代码 m 确实会减去 2 并且可以正常工作。

#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <map>
#include <set>

using namespace std;

#define LL long long

int main()
{
    LL n;
    cin >> n;
    LL m = pow(2, n + 1);
    m -= 2;
    cout << m;
    return 0;
}

我希望两个代码是等效的,为什么不是这样?

【问题讨论】:

  • 我很好奇你在哪里找到#define LL long long。我经常在这个网站上看到它,但我不知道是谁或什么在传播它。编辑:这是另一种代码高尔夫习惯吗?
  • @Lucas 但using LL = long long 也是编译时间,输入大小相同,而且更好。没有理由在那里使用宏
  • @pipe:我们完全理解为什么。我们不想仅仅因为您认为代码示例“非常好”而推广不良做法。这个网站上有很多被using namespace std;破坏的代码示例
  • @ThomasMatthews 当你告诉人们更喜欢 有符号 整数类型的位移时,请提醒他们,有符号整数类型溢出是未定义的行为 i>,这不仅仅是理论上的未定义行为,而且会在常见的编译器(如 gcc)上产生实际的错误,并且如果你没有有意识地考虑溢出,即使它被编译为行为,数值结果也会令人惊讶和意外完全符合固定宽度整数类型的预期。

标签: c++ floating-point precision long-long


【解决方案1】:

问题

LL m = pow(2, n + 1) - 2;

pow(2, n + 1) 不是long long。它的类型为double(参考cppreference),因为值太大,从它减去2 不会改变它的值。这意味着m 将没有正确的值。正如您已经发现的那样,您需要先分配结果,然后再进行减法。另一种选择是编写你自己的pow,当给定一个整数类型时它会返回一个整数类型,这样你就可以同时进行幂和减法。

【讨论】:

  • 对于 OP:您应该使用位移而不是 pow() 函数。类似于(1 &lt;&lt; (n + 1))
  • @StackDanny 问题出在pow(2, 54),而不是pow(2, 53)
  • 很好的答案,但您可能应该提到它在第二个示例中起作用的原因是pow(2,n)“意外”完全适合最常用的浮点表示,即使对于非常大的结果也是如此,所以绝不会涉及舍入误差。
  • @pipe,对于两个和二进制计算机的权力,这绝不是一个“意外”。
  • @ThomasMatthews 你需要1ULL &lt;&lt; (n + 1) 才能移动超过 31 位
【解决方案2】:

我希望两个代码是等效的,为什么不是这样?

你的期望是错误的。您的第二个代码将等同于:

auto m = static_cast<LL>( pow(2, n + 1) ) - 2;

由于arithmetic operators 的转换规则以及std::pow() 在这种情况下返回double 的事实:

对于二元运算符(移位除外),如果提升的操作数具有不同的类型,则应用附加的隐式转换集,称为通常的算术转换,其目标是产生通用类型(也可通过 std::common_type 访问类型特征)。如果在任何整数提升之前,一个操作数是枚举类型,而另一个操作数是浮点类型或不同的枚举类型,则不推荐使用此行为。 (C++20 起)

如果任一操作数具有范围枚举类型,则不执行转换:另一个操作数和返回类型必须具有相同的类型

否则,如果任一操作数为 long double,则另一个操作数将转换为 long double

否则,如果任一操作数为双精度,则另一操作数转换为双精度

否则,如果任一操作数为浮点数,则将另一个操作数转换为浮点数

...

(重点是我的)您的原始表达式将导致double - double 而不是long long int - long long int,就像您在第二种情况下所做的那样,因此存在差异。

【讨论】:

    【解决方案3】:

    pow 函数返回一个 double 类型的值,它只有 53 位精度。虽然返回的值将适合 double,即使 n 大于 53,减去 2 会导致类型为 double 的值需要超过 53 位的精度,因此减法的结果会四舍五入为最接近的可表示值。

    之所以进行减法运算,是因为从pow 返回的double 值被分配给long long,然后你从long long 中减去int

    由于您不处理浮点数并且您只是将 2 提高到幂,因此您可以用简单的左移替换对 pow 的调用:

    LL m = (1LL << (n + 1)) - 2;
    

    这会将所有中间值保留为long long 类型。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多