Почему моя арифметика с длинным длинным 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
.