Пересечение двух лучей
Даны два луча: 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 шт):
Спасибо @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 хватило. В комментариях написали, что есть более элегантный способ без использования деления, поэтому я отдам все лавры ему, если таковой ответ здесь появится.
Даны три точки: 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.