Почему моя арифметика с длинным длинным int ведет себя так?

Я пытаюсь вычислить большие целые числа с long long типом данных, но когда он становится достаточно большим (2^55), арифметическое поведение непредсказуемо. Я работаю в Microsoft Visual Studio 2017.

В этом первом случае я вычитаю 2 из long long переменной m в инициализации. Это нормально работает для всех 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;
}

Я ожидаю, что оба кода будут эквивалентны, почему это не так?

Ответы

Ответ 1

Проблема с

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

в том, что pow(2, n + 1) не является long long. Он имеет тип double (см. cppreference) и, поскольку значение очень велико, вычитание 2 из него не изменит его значения. Это означает, что m не будет иметь правильное значение. Как вы уже нашли, вам нужно сначала присвоить результат, а затем выполнить вычитание. Другая альтернатива - написать свой собственный pow, который будет возвращать целочисленный тип, когда ему дан целочисленный тип, чтобы вы могли одновременно делать возведение в степень и вычитание.

Ответ 2

Я ожидаю, что оба кода будут эквивалентны, почему это не так?

Ваше ожидание неверно. Ваш второй код будет эквивалентен этому:

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

как из-за правила преобразования для арифметических операторов и факта, что std::pow() возвращает double в этом случае:

Для бинарных операторов (кроме сдвигов), если повышенные операнды имеют разные типы, применяется дополнительный набор неявных преобразований, известных как обычные арифметические преобразования, с целью создания общего типа (также доступного через черту типа std :: common_type), Если до какого-либо интегрального преобразования один операнд имеет тип перечисления, а другой - тип с плавающей запятой или другой тип перечисления, такое поведение не рекомендуется. (начиная с С++ 20)

Если один из операндов имеет тип перечисления с областью действия, преобразование не выполняется: другой операнд и возвращаемый тип должны иметь одинаковый тип.

В противном случае, если один из операндов long long, другой операнд преобразуется в long double

В противном случае, если один из операндов является двойным, другой операнд преобразуется в двойной

В противном случае, если один из операндов является float, другой операнд преобразуется в float

...

(выделение мое), ваше исходное выражение привело бы к double double вместо long long int - long long int как вы делаете во втором случае, отсюда и разница.

Ответ 3

Функция pow возвращает значение типа double, которое имеет только 53 бита точности. Хотя возвращаемое значение будет соответствовать double даже если n больше 53, вычитание 2 приводит к значению типа double, для которого требуется точность более 53 битов, поэтому результат вычитания округляется до ближайшего представимого значения.

Причина, по которой происходит вычитание, заключается в том, что double значение, возвращаемое параметром pow, присваивается long long, а затем вы вычитаете int из long long.

Так как вы не имеете дело с числами с плавающей запятой и вы увеличиваете только 2 до степени, вы можете заменить вызов pow простым смещением влево:

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

Это сохраняет все промежуточные значения в типе long long.