Пересечение отрезков и расстояние до отрезка
Знаки векторных произведений плюс проверка прямоугольников — и коллинеарный случай перестаёт быть проблемой.
5 мин
Две задачи, на которых чаще всего теряют баллы в геометрии: обе выглядят простыми и обе полны вырожденных случаев.
Пересекаются ли отрезки
Даны отрезки и . Пересекаются ли они хотя бы в одной точке?
Первая мысль — пересечь прямые и проверить, что точка попала в оба отрезка. Так можно, но появляется деление, а с ним вещественные числа и погрешность. Есть способ без деления.
Идея. Отрезки пересекаются, если и лежат по разные стороны от прямой , и и лежат по разные стороны от прямой .
Сторона определяется знаком векторного произведения:
int sign(ld v) { return v > EPS ? 1 : (v < -EPS ? -1 : 0); }
int d1 = sign(Point(a, b) % Point(a, c));
int d2 = sign(Point(a, b) % Point(a, d));
int d3 = sign(Point(c, d) % Point(c, a));
int d4 = sign(Point(c, d) % Point(c, b));
if (d1 * d2 > 0 || d3 * d4 > 0) return false; // точки по одну сторону
Произведение знаков положительно, когда обе точки по одну сторону, — тогда пересечения точно нет.
Чего не хватает
Проверки выше недостаточно, и это главная ловушка задачи.
Если все четыре знака нулевые, отрезки лежат на одной прямой. Тогда условия выполняются всегда — но пересекаться отрезки при этом не обязаны: они могут лежать на прямой далеко друг от друга.
Лечится проверкой пересечения ограничивающих прямоугольников:
ld x1 = max(min(a.x, b.x), min(c.x, d.x));
ld x2 = min(max(a.x, b.x), max(c.x, d.x));
ld y1 = max(min(a.y, b.y), min(c.y, d.y));
ld y2 = min(max(a.y, b.y), max(c.y, d.y));
return x1 <= x2 && y1 <= y2;
Пересечение проекций на каждую ось: максимум левых границ не превосходит минимума правых. Тот же приём, что в задачах на отрезки.
Удобно, что эта проверка нужна только в коллинеарном случае, но выполнять её можно всегда — она никогда не даёт ложного отказа.
Целиком
bool segmentsIntersect(Point a, Point b, Point c, Point d) {
int d1 = sign(Point(a, b) % Point(a, c));
int d2 = sign(Point(a, b) % Point(a, d));
int d3 = sign(Point(c, d) % Point(c, a));
int d4 = sign(Point(c, d) % Point(c, b));
if (d1 * d2 > 0 || d3 * d4 > 0) return false;
ld x1 = max(min(a.x, b.x), min(c.x, d.x));
ld x2 = min(max(a.x, b.x), max(c.x, d.x));
ld y1 = max(min(a.y, b.y), min(c.y, d.y));
ld y2 = min(max(a.y, b.y), max(c.y, d.y));
return x1 <= x2 && y1 <= y2;
}
Двенадцать строк, ни одного деления, работает во всех случаях — включая касание концами, вложенные коллинеарные отрезки и вырожденные отрезки-точки.
Проверено против точного решения в целых числах (система на параметры и , решённая рациональными числами без округления): триста тысяч случайных пар отрезков с координатами от до , расхождений нет. Диапазон выбран маленьким намеренно — именно там плотно встречаются касания и совпадения, на которых наивные решения и ломаются.
Сама точка пересечения
Если нужна не только проверка, но и точка, — пересеките прямые. Но помните про случай, когда отрезки коллинеарны и пересекаются по целому отрезку: тогда ответ не точка, и условие обычно просит вывести концы общей части.
Эти концы считаются как пересечение проекций — те самые из кода выше.
Расстояние от точки до отрезка
Вторая задача. Расстояние от до отрезка — это либо длина перпендикуляра, либо расстояние до ближайшего конца.
Какой случай, определяется знаками скалярных произведений:
flowchart LR
L["P левее A:<br/>ответ dist(P, A)"] --- M["P напротив отрезка:<br/>ответ — перпендикуляр"] --- R["P правее B:<br/>ответ dist(P, B)"]
ld distToSegment(Point a, Point b, Point p) {
if (Point(a, b) * Point(a, p) < 0) return dist(a, p); // за краем A
if (Point(a, b) * Point(b, p) > 0) return dist(b, p); // за краем B
return distToLine(a, b, p);
}
Обратите внимание на второе условие: там взято Point(a, b), а не Point(b, a). Мы уже посчитали направляющую и переиспользуем её, поэтому знак сравнения меняется на противоположный. Написать Point(b, a) * Point(b, p) < 0 было бы тоже верно — но тогда нужно строить второй вектор.
Проверено: на двадцати тысячах случайных конфигураций результат совпал с перебором двадцати тысяч точек вдоль отрезка, с точностью — предел точности самого перебора, а не формулы.
Расстояние до луча
То же самое, но проверка одна: только за началом луча.
ld distToRay(Point a, Point b, Point p) {
if (Point(a, b) * Point(a, p) < 0) return dist(a, p);
return distToLine(a, b, p);
}
Расстояние между отрезками
Естественное продолжение. Если отрезки пересекаются, расстояние ноль. Иначе минимум достигается на конце одного из них:
ld distBetween(Point a, Point b, Point c, Point d) {
if (segmentsIntersect(a, b, c, d)) return 0;
return min({distToSegment(a, b, c), distToSegment(a, b, d),
distToSegment(c, d, a), distToSegment(c, d, b)});
}
Почему достаточно четырёх проверок: если отрезки не пересекаются, ближайшая пара точек обязательно включает хотя бы один конец. Внутренняя точка обоих отрезков минимумом быть не может — иначе можно было бы сдвинуться вдоль и уменьшить расстояние.
Проверка на пересечение здесь обязательна: без неё пересекающиеся отрезки дадут ненулевой ответ.