EduBrick

Точка на прямой, луче и отрезке

Три проверки, отличающиеся одним условием. Все три — в целых числах, без единого деления.

4 мин

Дана точка PP и объект, заданный двумя точками AA и BB: прямая, луч из AA через BB или отрезок ABAB. Лежит ли PP на нём?

Три задачи решаются одна через другую, и все три — точно, без вещественных чисел.

Прямая

Точка лежит на прямой ABAB тогда и только тогда, когда векторы AB\vec{AB} и AP\vec{AP} коллинеарны:

[AB,AP]=0[\vec{AB}, \vec{AP}] = 0
bool onLine(Point a, Point b, Point p) {
    return Point(a, b) % Point(a, p) == 0;
}

Одно векторное произведение и сравнение с нулём. Если координаты целые — точно; если вещественные — через eq.

Луч

Луч выходит из AA и проходит через BB. Точка на нём, если она на прямой и направление от AA к ней совпадает с направлением от AA к BB.

Совпадение направлений — это неотрицательное скалярное произведение:

bool onRay(Point a, Point b, Point p) {
    return onLine(a, b, p) && Point(a, b) * Point(a, p) >= 0;
}

Знак «больше либо равно» включает случай P=AP = A: там скалярное произведение равно нулю, и начало луча считается принадлежащим. Со строгим неравенством начало выпало бы — а это как раз тот вырожденный тест, который встретится на закрытой проверке.

Отрезок

Точка на отрезке ABAB, если она на прямой и лежит между AA и BB.

Условие «между» удобно записать так: векторы PA\vec{PA} и PB\vec{PB} смотрят в противоположные стороны, то есть угол между ними развёрнутый:

(PA,PB)0(\vec{PA}, \vec{PB}) \le 0
bool onSegment(Point a, Point b, Point p) {
    return onLine(a, b, p) && Point(p, a) * Point(p, b) <= 0;
}

Нестрогое неравенство снова включает концы: если PP совпадает с AA, то PA\vec{PA} нулевой и произведение равно нулю.

Проверено: для точки A+tABA + t \cdot \vec{AB} при tt от 1-1 до 33 все три проверки совпали с условиями на параметр — «на прямой» всегда, «на луче» при t0t \ge 0, «на отрезке» при 0t10 \le t \le 1. Двести тысяч случаев.

Сводка

объект условие
прямая ABAB [AB,AP]=0[\vec{AB}, \vec{AP}] = 0
луч из AA через BB то же и (AB,AP)0(\vec{AB}, \vec{AP}) \ge 0
отрезок ABAB то же и (PA,PB)0(\vec{PA}, \vec{PB}) \le 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);
}

Работает так же и иногда нагляднее. Но здесь обе проверки, по xx и по yy, обязательны: для вертикального отрезка проверка по xx пропустит любую точку прямой, для горизонтального — проверка по yy. Забыть одну из них — классическая ошибка.

Вариант со скалярным произведением короче и не требует помнить про этот случай.

Вырожденные случаи

Отдельно проверьте, что происходит при A=BA = B. Тогда «отрезок» — это точка, направляющая нулевая, векторное произведение всегда ноль, и onLine вернёт истину для любой точки.

Формулы для отрезка при этом отработают правильно: (PA,PB)=PA20(\vec{PA}, \vec{PB}) = |\vec{PA}|^2 \ge 0, значит условие 0\le 0 выполнится только при P=AP = A. А вот onLine и onRay в вырожденном случае врут.

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