跳转至

反演變換

引入

反演變換適用於題目中存在多個圓/直線之間的相切關係的情況。利用反演變換的性質,在反演空間求解問題,可以大幅簡化計算。

定義

給定反演中心點 \(O\) 和反演半徑 \(R\)。若平面上點 \(P\)\(P'\) 滿足:

  • \(P'\) 在射線 \(\overrightarrow{OP}\)
  • \(|OP| \cdot |OP'| = R^2\)

則稱點 \(P\) 和點 \(P'\) 互為反演點。

解釋

下圖所示即為平面上一點 \(P\) 的反演:

Inv1

性質

  1. \(O\) 外的點的反演點在圓 \(O\) 內,反之亦然;圓 \(O\) 上的點的反演點為其自身。

  2. 不過點 \(O\) 的圓 \(A\),其反演圖形也是不過點 \(O\) 的圓。

    Inv2

    • 記圓 \(A\) 半徑為 \(r_1\),其反演圖形圓 \(B\) 半徑為 \(r_2\),則有:

      \[ r_2 = \frac{1}{2}\left(\frac{1}{|OA| - r_1} - \frac{1}{|OA| + r_1}\right) R^2 \]

      證明:

      Inv3

      根據反演變換定義:

      \[ \begin{aligned} |OC|\cdot|OC'| &= (|OA|+r_1)\cdot(|OB|-r_2) = R^2 \\ |OD|\cdot|OD'| &= (|OA|-r_1)\cdot(|OB|+r_2) = R^2 \end{aligned} \]

      消掉 \(|OB|\),解方程即可。

    • 記點 \(O\) 座標為 \((x_0, y_0)\),點 \(A\) 座標為 \(x_1, y_1\),點 \(B\) 座標為 \(x_2, y_2\),則有:

      \[ \begin{aligned} x_2 &= x_0 + \frac{|OB|}{|OA|} (x_1 - x_0) \\ y_2 &= y_0 + \frac{|OB|}{|OA|} (y_1 - y_0) \end{aligned} \]

      其中 \(|OB|\) 可在上述求 \(r_2\) 的過程中計算得到。

  3. 過點 \(O\) 的圓 \(A\),其反演圖形是不過點 \(O\) 的直線。

    Note

    為什麼是一條直線呢?因為圓 \(A\) 上無限接近點 \(O\) 的一點,其反演點離點 \(O\) 無限遠。

    Inv4

  4. 兩個圖形相切且存在不為點 \(O\) 的切點,則他們的反演圖形也相切。

例題

「ICPC 2013 杭州賽區」Problem of Apollonius

題目大意

求過兩圓外一點,且與兩圓相切的所有的圓。

解法

首先考慮解析幾何解法,似乎很難求解。

考慮以需要經過的點為反演中心進行反演(反演半徑任意),所求的圓的反演圖形是一條直線(應用性質 \(3\)),且與題目給出兩圓的反演圖形(性質 \(2\))相切(性質 \(4\))。

於是題目經過反演變換後轉變為:求兩圓的所有公切線。

求出公切線後,反演回原平面即可。

示例代碼
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;

const double EPS = 1e-8;       // 精度係數
const double PI = acos(-1.0);  // π
const int N = 4;

struct Point {
  double x, y;

  Point(double x = 0, double y = 0) : x(x), y(y) {}

  const bool operator<(Point A) const { return x == A.x ? y < A.y : x < A.x; }
};  // 點的定義

typedef Point Vector;  // 向量的定義

Vector operator+(Vector A, Vector B) {
  return Vector(A.x + B.x, A.y + B.y);
}  // 向量加法

Vector operator-(Vector A, Vector B) {
  return Vector(A.x - B.x, A.y - B.y);
}  // 向量減法

Vector operator*(Vector A, double p) {
  return Vector(A.x * p, A.y * p);
}  // 向量數乘

Vector operator/(Vector A, double p) {
  return Vector(A.x / p, A.y / p);
}  // 向量數除

int dcmp(double x) {
  if (fabs(x) < EPS)
    return 0;
  else
    return x < 0 ? -1 : 1;
}  // 與0的關係

double Dot(Vector A, Vector B) { return A.x * B.x + A.y * B.y; }  // 向量點乘

double Length(Vector A) { return sqrt(Dot(A, A)); }  // 向量長度

double Cross(Vector A, Vector B) { return A.x * B.y - A.y * B.x; }  // 向量叉乘

Point GetLineProjection(Point P, Point A, Point B) {
  Vector v = B - A;
  return A + v * (Dot(v, P - A) / Dot(v, v));
}  // 點在直線上投影

struct Circle {
  Point c;
  double r;

  Circle() : c(Point(0, 0)), r(0) {}

  Circle(Point c, double r = 0) : c(c), r(r) {}

  Point point(double a) {
    return Point(c.x + cos(a) * r, c.y + sin(a) * r);
  }  // 輸入極角返回點座標
};   // 圓

// a[i] 和 b[i] 分別是第i條切線在圓A和圓B上的切點
int getTangents(Circle A, Circle B, Point* a, Point* b) {
  int cnt = 0;
  if (A.r < B.r) {
    swap(A, B);
    swap(a, b);
  }
  double d2 =
      (A.c.x - B.c.x) * (A.c.x - B.c.x) + (A.c.y - B.c.y) * (A.c.y - B.c.y);
  double rdiff = A.r - B.r;
  double rsum = A.r + B.r;
  if (dcmp(d2 - rdiff * rdiff) < 0) return 0;  // 內含

  double base = atan2(B.c.y - A.c.y, B.c.x - A.c.x);
  if (dcmp(d2) == 0 && dcmp(A.r - B.r) == 0) return -1;  // 無限多條切線
  if (dcmp(d2 - rdiff * rdiff) == 0) {  // 內切,一條切線
    a[cnt] = A.point(base);
    b[cnt] = B.point(base);
    ++cnt;
    return 1;
  }
  // 有外公切線
  double ang = acos(rdiff / sqrt(d2));
  a[cnt] = A.point(base + ang);
  b[cnt] = B.point(base + ang);
  ++cnt;
  a[cnt] = A.point(base - ang);
  b[cnt] = B.point(base - ang);
  ++cnt;
  if (dcmp(d2 - rsum * rsum) == 0) {  // 一條內公切線
    a[cnt] = A.point(base);
    b[cnt] = B.point(PI + base);
    ++cnt;
  } else if (dcmp(d2 - rsum * rsum) > 0) {  // 兩條內公切線
    double ang = acos(rsum / sqrt(d2));
    a[cnt] = A.point(base + ang);
    b[cnt] = B.point(PI + base + ang);
    ++cnt;
    a[cnt] = A.point(base - ang);
    b[cnt] = B.point(PI + base - ang);
    ++cnt;
  }
  return cnt;
}  // 兩圓公切線 返回切線的條數,-1表示無窮多條切線

Circle Inversion_C2C(Point O, double R, Circle A) {
  double OA = Length(A.c - O);
  double RB = 0.5 * ((1 / (OA - A.r)) - (1 / (OA + A.r))) * R * R;
  double OB = OA * RB / A.r;
  double Bx = O.x + (A.c.x - O.x) * OB / OA;
  double By = O.y + (A.c.y - O.y) * OB / OA;
  return Circle(Point(Bx, By), RB);
}  // 點 O 在圓 A 外,求圓 A 的反演圓 B,R 是反演半徑

Circle Inversion_L2C(Point O, double R, Point A, Vector v) {
  Point P = GetLineProjection(O, A, A + v);
  double d = Length(O - P);
  double RB = R * R / (2 * d);
  Vector VB = (P - O) / d * RB;
  return Circle(O + VB, RB);
}  // 直線反演為過 O 點的圓 B,R 是反演半徑

bool theSameSideOfLine(Point A, Point B, Point S, Vector v) {
  return dcmp(Cross(A - S, v)) * dcmp(Cross(B - S, v)) > 0;
}  // 返回 true 如果 A B 兩點在直線同側

int main() {
  int T;
  scanf("%d", &T);
  while (T--) {
    Circle A, B;
    Point P;
    scanf("%lf%lf%lf", &A.c.x, &A.c.y, &A.r);
    scanf("%lf%lf%lf", &B.c.x, &B.c.y, &B.r);
    scanf("%lf%lf", &P.x, &P.y);
    Circle NA = Inversion_C2C(P, 10, A);
    Circle NB = Inversion_C2C(P, 10, B);
    Point LA[N], LB[N];
    Circle ansC[N];
    int q = getTangents(NA, NB, LA, LB), ans = 0;
    for (int i = 0; i < q; ++i)
      if (theSameSideOfLine(NA.c, NB.c, LA[i], LB[i] - LA[i])) {
        if (!theSameSideOfLine(P, NA.c, LA[i], LB[i] - LA[i])) continue;
        ansC[ans++] = Inversion_L2C(P, 10, LA[i], LB[i] - LA[i]);
      }
    printf("%d\n", ans);
    for (int i = 0; i < ans; ++i) {
      printf("%.8f %.8f %.8f\n", ansC[i].c.x, ansC[i].c.y, ansC[i].r);
    }
  }

  return 0;
}

練習

「ICPC 2017 南寧賽區網絡賽」Finding the Radius for an Inserted Circle

「CCPC 2017 網絡賽」The Designer

參考資料與拓展閲讀