EduBrick

Точность и эпсилон

Почему нельзя писать == для вещественных чисел, как выбрать эпсилон и когда без него можно обойтись вовсе.

4 мин

Вещественные числа в компьютере хранятся приближённо. Одно и то же математически число, посчитанное двумя разными способами, даст два слегка разных представления.

Поэтому проверка if (a == b) для double почти всегда неверна: она вернёт ложь там, где числа равны по смыслу.

Геометрия страдает от этого больше всех, потому что состоит из делений, корней и цепочек умножений.

Сравнения с эпсилоном

const long double EPS = 1e-9;

bool eq(long double a, long double b) { return fabsl(a - b) < EPS; }
bool lt(long double a, long double b) { return a + EPS < b; }
bool le(long double a, long double b) { return a < b + EPS; }

Всё остальное выражается через них: «больше» — это lt(b, a), «не меньше» — le(b, a).

Важно пользоваться только этими функциями и нигде не писать голые ==, <, >. Один забытый оператор в середине решения даёт ошибку, которая проявляется на одном тесте из сотни.

Как выбрать эпсилон

Общего ответа нет, но есть ориентиры.

ситуация эпсилон
координаты до 10310^3, простые формулы 10910^{-9}
координаты до 10910^9 10610^{-6}
длинные цепочки вычислений 10610^{-6}
задача просит точность 10610^{-6} 10910^{-9} на сравнения

Логика: long double даёт около 18 значащих цифр. Если координаты порядка 10910^9, то на дробную часть остаётся примерно 9 цифр, и эпсилон меньше 10910^{-9} уже бессмыслен.

Слишком маленький эпсилон — числа, которые должны быть равны, считаются разными. Слишком большой — разные считаются равными. Второе обычно опаснее: ошибка проявляется на вырожденных тестах, где точки почти совпадают.

Лучший эпсилон — отсутствие эпсилона

Главный совет раздела: если координаты целые, считайте в целых.

Скалярное и векторное произведения целых координат целые. Значит, проверки «коллинеарны», «перпендикулярны», «слева или справа», «точка на отрезке», «отрезки пересекаются» и площадь можно делать точно, без единого эпсилона.

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

Практический приём: разделить решение на две части. Всю логику — на целых, и только финальный ответ — в вещественных. Это заметно надёжнее, чем писать всё в double и подбирать эпсилон.

Вывод

По умолчанию cout печатает шесть значащих цифр и может уйти в экспоненциальную запись:

cout << 1e20;                            // 1e+20 — чекер это не примет
cout << fixed << setprecision(15) << x;  // так правильно

fixed фиксирует запись без экспоненты, setprecision задаёт число знаков после запятой. Обе манипуляции ставятся один раз в начале.

Правило: если задача просит точность 10610^{-6}, выводите 10–15 знаков. Лишние цифры не вредят, а обрезанные теряют точность, которую вы честно посчитали.

Исключение — когда условие требует ровно kk знаков. Тогда именно kk.

Типичные источники погрешности

Вычитание близких чисел. Если aa и bb почти равны, в разности теряются старшие цифры, и относительная погрешность резко растёт. Именно поэтому «сначала вычесть, потом делить» иногда работает хуже, чем наоборот.

Деление на почти ноль. Пересечение почти параллельных прямых даёт огромные координаты с огромной погрешностью. Проверяйте определитель на ноль до деления.

Корень из почти нуля. Производная корня в нуле бесконечна, поэтому маленькая погрешность под корнем даёт большую после. Ещё хуже — корень из отрицательного числа, полученного из-за погрешности вместо нуля: sqrt вернёт NaN, и дальше всё сломается молча.

acos за границей диапазона. Косинус, посчитанный с погрешностью, может оказаться равен 1.00000000011.0000000001, и acos вернёт NaN. Если считаете угол через acos, обрежьте аргумент в [1,1][-1, 1] явно.

Как это отлаживать

Погрешность коварна тем, что ошибка не воспроизводится на маленьких тестах. Что помогает:

  • прогнать стресс с целыми координатами в маленьком диапазоне — там правильный ответ считается точно;
  • отдельно проверить вырожденные случаи: совпадающие точки, коллинеарные тройки, вертикальные прямые, нулевые векторы;
  • вывести промежуточные величины в cerr и посмотреть, где вместо ожидаемого нуля появилось 101310^{-13}.

Последний пункт — самый полезный. Обычно видно, что величина, которая должна быть ровно нулём, нулём не стала, и остаётся понять, какое сравнение это пропустило.