Точность и эпсилон
Почему нельзя писать == для вещественных чисел, как выбрать эпсилон и когда без него можно обойтись вовсе.
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).
Важно пользоваться только этими функциями и нигде не писать голые ==, <, >. Один забытый оператор в середине решения даёт ошибку, которая проявляется на одном тесте из сотни.
Как выбрать эпсилон
Общего ответа нет, но есть ориентиры.
| ситуация | эпсилон |
|---|---|
| координаты до , простые формулы | |
| координаты до | |
| длинные цепочки вычислений | |
| задача просит точность | на сравнения |
Логика: long double даёт около 18 значащих цифр. Если координаты порядка , то на дробную часть остаётся примерно 9 цифр, и эпсилон меньше уже бессмыслен.
Слишком маленький эпсилон — числа, которые должны быть равны, считаются разными. Слишком большой — разные считаются равными. Второе обычно опаснее: ошибка проявляется на вырожденных тестах, где точки почти совпадают.
Лучший эпсилон — отсутствие эпсилона
Главный совет раздела: если координаты целые, считайте в целых.
Скалярное и векторное произведения целых координат целые. Значит, проверки «коллинеарны», «перпендикулярны», «слева или справа», «точка на отрезке», «отрезки пересекаются» и площадь можно делать точно, без единого эпсилона.
Вещественные числа нужны только там, где появляется деление или корень: расстояние, точка пересечения, угол.
Практический приём: разделить решение на две части. Всю логику — на целых, и только финальный ответ — в вещественных. Это заметно надёжнее, чем писать всё в double и подбирать эпсилон.
Вывод
По умолчанию cout печатает шесть значащих цифр и может уйти в экспоненциальную запись:
cout << 1e20; // 1e+20 — чекер это не примет
cout << fixed << setprecision(15) << x; // так правильно
fixed фиксирует запись без экспоненты, setprecision задаёт число знаков после запятой. Обе манипуляции ставятся один раз в начале.
Правило: если задача просит точность , выводите 10–15 знаков. Лишние цифры не вредят, а обрезанные теряют точность, которую вы честно посчитали.
Исключение — когда условие требует ровно знаков. Тогда именно .
Типичные источники погрешности
Вычитание близких чисел. Если и почти равны, в разности теряются старшие цифры, и относительная погрешность резко растёт. Именно поэтому «сначала вычесть, потом делить» иногда работает хуже, чем наоборот.
Деление на почти ноль. Пересечение почти параллельных прямых даёт огромные координаты с огромной погрешностью. Проверяйте определитель на ноль до деления.
Корень из почти нуля. Производная корня в нуле бесконечна, поэтому маленькая погрешность под корнем даёт большую после. Ещё хуже — корень из отрицательного числа, полученного из-за погрешности вместо нуля: sqrt вернёт NaN, и дальше всё сломается молча.
acos за границей диапазона. Косинус, посчитанный с погрешностью, может оказаться равен , и acos вернёт NaN. Если считаете угол через acos, обрежьте аргумент в явно.
Как это отлаживать
Погрешность коварна тем, что ошибка не воспроизводится на маленьких тестах. Что помогает:
- прогнать стресс с целыми координатами в маленьком диапазоне — там правильный ответ считается точно;
- отдельно проверить вырожденные случаи: совпадающие точки, коллинеарные тройки, вертикальные прямые, нулевые векторы;
- вывести промежуточные величины в
cerrи посмотреть, где вместо ожидаемого нуля появилось .
Последний пункт — самый полезный. Обычно видно, что величина, которая должна быть ровно нулём, нулём не стала, и остаётся понять, какое сравнение это пропустило.