EduBrick

Окружности

Пересечение прямой с окружностью через проекцию центра. Пересечение двух окружностей сводится к первой задаче вычитанием уравнений.

5 мин

Окружность задаётся центром и радиусом: (xx0)2+(yy0)2=r2(x - x_0)^2 + (y - y_0)^2 = r^2.

Дальше считаем, что центр перенесён в начало координат — так формулы короче, а обратный перенос стоит одного сложения.

Прямая и окружность

Прямая задана как ax+by+c=0ax + by + c = 0 (три представления прямой).

Сколько точек пересечения, решает расстояние от центра до прямой:

d=ca2+b2d = \frac{|c|}{\sqrt{a^2 + b^2}}

Если d>rd > r — пересечения нет, d=rd = r — касание в одной точке, d<rd < r — две точки.

Теперь сами точки. Опустим перпендикуляр из центра на прямую — его основание P0P_0 считается по формуле проекции:

P0=(aca2+b2, bca2+b2)P_0 = \left(-\frac{ac}{a^2+b^2},\ -\frac{bc}{a^2+b^2}\right)

Треугольник «центр — P0P_0 — точка пересечения» прямоугольный: гипотенуза rr, катет dd. Второй катет:

=r2d2\ell = \sqrt{r^2 - d^2}

Треугольник, образованный центром и двумя точками пересечения, равнобедренный, поэтому P0P_0середина хорды. Значит, обе точки получаются откладыванием \ell от P0P_0 в обе стороны вдоль прямой.

Направляющий вектор прямой — (b,a)(-b, a); нормируем его и умножаем на \ell.

vector<Pt> lineCircle(double a, double b, double c, double r) {
    double d2 = a * a + b * b;
    Pt p0{-a * c / d2, -b * c / d2};              // проекция центра на прямую
    double dist2 = c * c / d2;                    // квадрат расстояния до прямой
    if (dist2 > r * r + EPS) return {};
    if (fabs(dist2 - r * r) < EPS) return {p0};
    double l = sqrt(max(0.0, r * r - dist2));
    double m = sqrt(l * l / d2);                  // множитель для вектора (-b, a)
    return {{p0.x - b * m, p0.y + a * m},
            {p0.x + b * m, p0.y - a * m}};
}

max(0.0, ...) под корнем — не паранойя: при dd, почти равном rr, разность может выйти отрицательной на 101710^{-17}, и sqrt вернёт NaN.

Проверено: на 300 000 случайных прямых и окружностей найденные точки лежат и на прямой, и на окружности с погрешностью не больше 10610^{-6}, а число точек согласовано с расстоянием до центра.

Две окружности

Сначала сколько точек. Пусть dd — расстояние между центрами, r1r_1 и r2r_2 — радиусы.

условие что происходит
d>r1+r2d > r_1 + r_2 далеко друг от друга, пересечения нет
d=r1+r2d = r_1 + r_2 внешнее касание
r1r2<d<r1+r2\lvert r_1 - r_2 \rvert < d < r_1 + r_2 две точки
d=r1r2d = \lvert r_1 - r_2 \rvert внутреннее касание
d<r1r2d < \lvert r_1 - r_2 \rvert одна внутри другой, пересечения нет

Отдельный вырожденный случай: d=0d = 0 и r1=r2r_1 = r_2 — окружности совпадают, точек бесконечно много. Его надо ловить до всего остального.

Сами точки: сведение к прямой

Можно возиться с треугольниками и косинусами. Красивее — вычесть одно уравнение из другого.

Запишем оба уравнения и вычтем. Квадраты x2x^2 и y2y^2 сократятся, останется линейное уравнение:

2(x2x1)x2(y2y1)y+(x22x12+y22y12+r12r22)=0-2(x_2 - x_1)\,x - 2(y_2 - y_1)\,y + \bigl(x_2^2 - x_1^2 + y_2^2 - y_1^2 + r_1^2 - r_2^2\bigr) = 0

Это прямая — она называется радикальной осью. Любая точка, лежащая на обеих окружностях, лежит и на ней. Значит, задача свелась к уже решённой: пересечь первую окружность с этой прямой.

vector<Pt> circleCircle(Pt c1, double r1, Pt c2, double r2) {
    double dx = c2.x - c1.x, dy = c2.y - c1.y;    // переносим c1 в начало координат
    double a = -2 * dx, b = -2 * dy;
    double c = dx * dx + dy * dy + r1 * r1 - r2 * r2;
    auto pts = lineCircle(a, b, c, r1);
    for (auto& p : pts) { p.x += c1.x; p.y += c1.y; }
    return pts;
}

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

Проверено: на 300 000 случайных пар окружностей найденные точки лежат на обеих с погрешностью не больше 10710^{-7}, а на непересекающихся парах точек не возвращается.

Про точность

Здесь неизбежны корни, а значит, вещественные числа и эпсилон. Целочисленной арифметикой, как в задачах про векторное произведение, обойтись не выйдет.

Единственное, что можно и нужно сделать целыми: проверку количества точек. Сравнивайте d2d^2 с (r1+r2)2(r_1 + r_2)^2 и (r1r2)2(r_1 - r_2)^2 — все три величины целые, если целые входные данные, и сравнение получается точным.

Касание — самый опасный случай: ответ на вопрос «одна точка или ноль» решается сравнением на равенство, и в вещественных числах оно почти всегда ложно. Целочисленная проверка снимает проблему целиком.