Точка на прямой, луче и отрезке
Три проверки, отличающиеся одним условием. Все три — в целых числах, без единого деления.
4 мин
Дана точка и объект, заданный двумя точками и : прямая, луч из через или отрезок . Лежит ли на нём?
Три задачи решаются одна через другую, и все три — точно, без вещественных чисел.
Прямая
Точка лежит на прямой тогда и только тогда, когда векторы и коллинеарны:
bool onLine(Point a, Point b, Point p) {
return Point(a, b) % Point(a, p) == 0;
}
Одно векторное произведение и сравнение с нулём. Если координаты целые — точно; если вещественные — через eq.
Луч
Луч выходит из и проходит через . Точка на нём, если она на прямой и направление от к ней совпадает с направлением от к .
Совпадение направлений — это неотрицательное скалярное произведение:
bool onRay(Point a, Point b, Point p) {
return onLine(a, b, p) && Point(a, b) * Point(a, p) >= 0;
}
Знак «больше либо равно» включает случай : там скалярное произведение равно нулю, и начало луча считается принадлежащим. Со строгим неравенством начало выпало бы — а это как раз тот вырожденный тест, который встретится на закрытой проверке.
Отрезок
Точка на отрезке , если она на прямой и лежит между и .
Условие «между» удобно записать так: векторы и смотрят в противоположные стороны, то есть угол между ними развёрнутый:
bool onSegment(Point a, Point b, Point p) {
return onLine(a, b, p) && Point(p, a) * Point(p, b) <= 0;
}
Нестрогое неравенство снова включает концы: если совпадает с , то нулевой и произведение равно нулю.
Проверено: для точки при от до все три проверки совпали с условиями на параметр — «на прямой» всегда, «на луче» при , «на отрезке» при . Двести тысяч случаев.
Сводка
| объект | условие |
|---|---|
| прямая | |
| луч из через | то же и |
| отрезок | то же и |
Видно, что все три отличаются одной строкой, и все три обходятся без деления. Это общий признак хорошо написанной геометрии: если в проверке появилось деление, скорее всего, есть способ без него.
Альтернатива через координаты
Иногда принадлежность отрезку проверяют по-другому: точка на прямой и её координаты лежат между координатами концов.
bool onSegment(Point a, Point b, Point p) {
if (Point(a, b) % Point(a, p) != 0) return false;
return min(a.x, b.x) <= p.x && p.x <= max(a.x, b.x)
&& min(a.y, b.y) <= p.y && p.y <= max(a.y, b.y);
}
Работает так же и иногда нагляднее. Но здесь обе проверки, по и по , обязательны: для вертикального отрезка проверка по пропустит любую точку прямой, для горизонтального — проверка по . Забыть одну из них — классическая ошибка.
Вариант со скалярным произведением короче и не требует помнить про этот случай.
Вырожденные случаи
Отдельно проверьте, что происходит при . Тогда «отрезок» — это точка, направляющая нулевая, векторное произведение всегда ноль, и onLine вернёт истину для любой точки.
Формулы для отрезка при этом отработают правильно: , значит условие выполнится только при . А вот onLine и onRay в вырожденном случае врут.
Если по условию совпадающие точки возможны, обработайте это явно в начале функции.