Пересечение двух лучей

Даны два луча: AB и CD (A и C - вершины лучей, B и D лежат на лучах). Проверить, пересекаются ли они.

Пример:

Вход:

0 1
1 2
1 -1
1 0

Выход:

YES

Хочу решать следующим образом:

  • Построить уравнения прямых AB' и CD' по данным точкам
  • Определить точку пересечения прямых, решив систему уравнений методом Краммера
  • Если прямые не пересекаются, то вывести NO
  • Если прямые совпадают, то проверить начала лучей (если начинаются в одной точке - YES). Если ответа не получили, то нужно еще проверить направления лучей (либо они противоположны - NO, либо в одну сторону - YES)
  • Если прямые пересекаются, проверить, лежит ли точка пересечения на обоих лучах (если точка лежит на обеих прямых, остается проверить, что вектор, образованный началом луча и самой точкой, сонаправлен с лучом)

Вот вроде на бумаге все учел, но один тест падает с WA... Подскажите, что еще нужно учесть? Вот кусок кода:

class Point {
public:
    explicit Point(long double _x = 0, long double _y = 0) : x(_x), y(_y) {};

    // Всякие наивные операции по типу сложения
    
    void read_point() { std::cin >> x >> y; }
public:
    long double x, y;
};

class Vector {
public:
    // Переносим вектор в начало координат (a = 0)
    explicit Vector(Point _a = Point(0, 0), Point _b = Point(0, 0)) : b((_b - _a)) {};
    
    // Всякие наивные операции по типу сложения
public:
    Point b;
};

class Line {
public:
    explicit Line(long double _a = 0, long double _b = 0, long double _c = 0) : A(_a), B(_b), C(_c) {}; // По коэфф-ам
    Line(Point a, Point b) : A(b.y - a.y), B(a.x - b.x), C(b.x * a.y - a.x * b.y) {}; // Две точки

    Vector get_leading_vector() const {
        return Vector(Point(), Point(B, -A));
    }
    
    /*
     * Если прямые не пересекаются - 0
     * Если прямые пересекаются ровно в одной точке - 1
     * Если прямые совпадают - 2
     * */
    int get_intersect_point(Line &other, Point &p) const {
        long double det = A * other.B - B * other.A, det1 = B * other.C - other.B * C, det2 = other.A * C - A * other.C;
        if (det != 0) {
            p = Point(det1 / det, det2 / det);
            return 1;
        } else { // Коллинеарны
            if (det1 == 0 && det2 == 0) return 2;
            else return 0;
        }
    }

public:
    long double A, B, C;
};

long double scalar_prod(Vector v1, Vector v2) {
    return v1.b.x * v2.b.x + v1.b.y * v2.b.y;
}

int main() {

    Point a, b, c, d, p;
    a.read_point();
    b.read_point();
    c.read_point();
    d.read_point();
    Line AB(a, b), CD(c, d);

    int line_intersect_status = AB.get_intersect_point(CD, p);
    bool are_rays_intersect;

    if (line_intersect_status == 2) {
        if (a == c) are_rays_intersect = true; // Лучи начинаются с одной точки
        else { // Направление лучей только может быть либо в одну сторону, либо в противоположные
            are_rays_intersect = !(AB.get_leading_vector() + CD.get_leading_vector() == Vector(Point(), Point()));
        }
    } else if (line_intersect_status == 1) { // Лежит ли точка пересечения на луче
        are_rays_intersect = scalar_prod(Vector(a, b), Vector(a, p)) >= 0 &&
                             scalar_prod(Vector(c, d), Vector(c, p)) >= 0;
    } else are_rays_intersect = false;

    print_yes_no(are_rays_intersect);
    return 0;
}

Ответы (2 шт):

Автор решения: AmEl

Спасибо @MBo за контрпример. Действительно, заплатка в виде a == c, призванная поддерживать случаи по типу:

0 0
0 1
0 0
0 -1

меня напрягала. На самом деле, достаточно проверить, лежит ли начало у каждого луча на другом луче при совпадении прямых, т.е.:

// Предполагаем, что точка точно лежит на прямой
bool is_point_on_the_ray(Point a, Point b, Point p) {
    return scalar_prod(Vector(a, b), Vector(a, p)) >= 0;
}

int main() {
    // ...
    if (line_intersect_status == 2) {
        are_rays_intersect = is_point_on_the_ray(c, d, a) || is_point_on_the_ray(a, b, c);
    } else if (line_intersect_status == 1) {
        are_rays_intersect = is_point_on_the_ray(a, b, p) && is_point_on_the_ray(c, d, p);
    } else are_rays_intersect = false;
    
    // ...
}

Но опять же, отмечать это как ответ я не буду, возможно, мне в какой-то степени повезло, что точности long double хватило. В комментариях написали, что есть более элегантный способ без использования деления, поэтому я отдам все лавры ему, если таковой ответ здесь появится.

→ Ссылка
Автор решения: Stanislav Volodarskiy

Даны три точки: p1 = (x1, y1), p2 = (x2, y2) и p = (x, y). Тогда определитель ниже равен удвоенной площади треугольника Δp1p2p (общая формула площади треугольника в декартовых координатах на плоскости). Площадь со знаком, она положительна если точки расположены против часовой стрелки, отрицательна если точки - по часовой стрелке, равна нулю, если точки на одной прямой.

x1 y1 1
x2 y2 1
x y 1

Зафиксируем p1, p2. Будем менять только p. Определить положителен если p "слева" от прямой p1p2, отрицателен если "справа", равен нулю если точка на прямой. Значение определителя - линейная функция от координат x и y:

d(x, y) = (y1 - y2)x + (x2 - x1)y + (x1y2 - x2y1).

Луч задаётся парой точек: p - начало луча, q - точка на луче. Любую точку r на луче можно представить в виде r(t) = (1 - t)p + tq, где t - неотрицательное число. В частности r(0) = p, r(1) = q. В координатах будет:

xt = (1 - t)px + tqx, yt = (1 - t)py + tqy, где t ≥ 0.

Обозначим f(t) = d(xt, yt). d, xt, yt - линейные функции, следовательно f тоже линейная.

Луч r(t) пересекается с прямой p1p2 тогда и только тогда, когда существует t ≥ 0 такое что f(t) = 0.

f - линейная. Чтобы определить что она имеет корень, достаточно проанализировать значения f(0) и f(1). Например, если 0 < f(0) ≤ f(1), корня точно нет. Тоже верно для случая f(1) ≤ f(0) < 0. Если f(0) = f(1) = 0, луч целиком лежит на прямой. Иначе луч пересекается с прямой в одной точке.

Вся эта теория используется в функции int intersect(const LineEq &eq, const Ray &ray), которая определяет как размещены на плоскости прямая и луч. Они могут не пересекаться совсем, пересекаться в одной точке, луч может лежать на прямой. Функция f явно не строится, нам нужны только значения f(0) = d(px, py) и f(1) = d(qx, qy).

Функция bool intersect(const Ray &r1, const Ray &r2) определяет пересечение двух лучей:

  • если первый луч не пересекает вторую прямую (продолжение второго луча), то и лучи не пересекаются;
  • если второй луч не пересекает первую прямую, лучи не пересекаются;
  • если оба пересечения луч-прямая происходят в одной точке, то и сами лучи пересекаются в этой точке;
  • если оба пересечения луч-прямая происходят во многих точках, лучи лежат на общей прямой, этот случай требует дополнительной работы.

Пусть луч задан началом p = (px, py) и точкой на луче q = (qx, qy). На прямой pq лежит точка r = (x, y). Тогда r лежит на луче, тогда и только тогда когда (r - p)(q - p) ≥ 0. В координатах:

(x - px)(qx - px) + (y - py)(qy - py) = (qx - px)x + (qy - py)y + (px(px - qx) + py(py - qy)).

Последнее выражение линейная функция g(x, y). Подставим в неё координаты точек из второго луча и проверим принимает ли она неотрицательные значения. Логика очень похожа на пересечение луча и прямой, только проще.

Это вся теория:

#include <iostream>

struct Point { int x, y; };
struct Ray { Point p, q; };

// |x1 y1 1|
// |x2 y2 1| = (y1 - y2)x + (x2 - x1)y + (x1y2 - x2y1)
// |x  y  1|
class LineEq {
public:
    LineEq(const Point &p1, const Point &p2)
    // 2M             2M              2MM
    : a(p1.y - p2.y), b(p2.x - p1.x), c(p1.x * p2.y - p2.x * p1.y) {}
    //                                            6MM
    int operator()(const Point &p) const { return a * p.x + b * p.y + c; }
private:
    int a, b, c;
};

// 0 - no intersection, 1 - one point intersection, 2 - ray lays on line
int intersect(const LineEq &eq, const Ray &ray) {
    const int f0 = eq(ray.p);
    const int f1 = eq(ray.q);
    if (f1 <= f0 && f0 < 0) { return 0; }
    if (0 < f0 && f0 <= f1) { return 0; }
    return (f0 != f1) ? 1 : 2;
}

// (r - p)(q - p) =
// = (x - px)(qx - px) + (y - py)(qy - py) =
// = (qx - px)x + (qy - py)y + (px(px - qx) + py(py - qy))
class RayEq {
public:
    RayEq(const Point &p, const Point &q)
    // 2M             2M              4MM
    : a(q.x - p.x), b(q.y - p.y), c(p.x * -a + p.y * -b) {}
    //                                            8MM
    int operator()(const Point &r) const { return a * r.x + b * r.y + c; }
private:
    int a, b, c;
};

bool intersect(const Ray &r1, const Ray &r2) {
    const int i12 = intersect(LineEq(r1.p, r1.q), r2);
    if (i12 == 0) {
        return false;
    }
    const int i21 = intersect(LineEq(r2.p, r2.q), r1);
    if (i21 == 0) {
        return false;
    }
    if (i12 == 1) {
        return true;
    }

    const RayEq eq(r1.p, r1.q);
    const int f0 = eq(r2.p);
    if (0 <= f0) {
        return true;
    }
    const int f1 = eq(r2.q);
    return f0 < f1;
}

int main() {
    Ray r1, r2;
    if (std::cin
        >> r1.p.x >> r1.p.y
        >> r1.q.x >> r1.q.y
        >> r2.p.x >> r2.p.y
        >> r2.q.x >> r2.q.y
    ) {
        std::cout << ((intersect(r1, r2)) ? "YES" : "NO") << '\n';
    }
}

Решение использует только целые числа и операции +, -, * и сравнения. Если модуль координат точек не превосходит M, то максимальное значение в коде не превосходит 8M2. Соответствующие комментарии есть в коде. Для типа int требуется 8M2 < 231. То есть, переполнения не будет пока модуль координат меньше 214.

→ Ссылка