EduBrick

Пересечение отрезков и расстояние до отрезка

Знаки векторных произведений плюс проверка прямоугольников — и коллинеарный случай перестаёт быть проблемой.

5 мин

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

Пересекаются ли отрезки

Даны отрезки ABAB и CDCD. Пересекаются ли они хотя бы в одной точке?

Первая мысль — пересечь прямые и проверить, что точка попала в оба отрезка. Так можно, но появляется деление, а с ним вещественные числа и погрешность. Есть способ без деления.

Идея. Отрезки пересекаются, если CC и DD лежат по разные стороны от прямой ABAB, и AA и BB лежат по разные стороны от прямой CDCD.

Сторона определяется знаком векторного произведения:

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;
}

Двенадцать строк, ни одного деления, работает во всех случаях — включая касание концами, вложенные коллинеарные отрезки и вырожденные отрезки-точки.

Проверено против точного решения в целых числах (система на параметры tt и ss, решённая рациональными числами без округления): триста тысяч случайных пар отрезков с координатами от 4-4 до 44, расхождений нет. Диапазон выбран маленьким намеренно — именно там плотно встречаются касания и совпадения, на которых наивные решения и ломаются.

Сама точка пересечения

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

Эти концы считаются как пересечение проекций — те самые x1,x2,y1,y2x_1, x_2, y_1, y_2 из кода выше.

Расстояние от точки до отрезка

Вторая задача. Расстояние от PP до отрезка ABAB — это либо длина перпендикуляра, либо расстояние до ближайшего конца.

Какой случай, определяется знаками скалярных произведений:

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 было бы тоже верно — но тогда нужно строить второй вектор.

Проверено: на двадцати тысячах случайных конфигураций результат совпал с перебором двадцати тысяч точек вдоль отрезка, с точностью 10310^{-3} — предел точности самого перебора, а не формулы.

Расстояние до луча

То же самое, но проверка одна: только за началом луча.

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)});
}

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

Проверка на пересечение здесь обязательна: без неё пересекающиеся отрезки дадут ненулевой ответ.