Chương 6·Bài 69

Geometry

Hình học tính toán trong thi đấu gần như chỉ xoay quanh một phép tính: tích có hướng của hai vector, cho biết ba điểm quay trái, quay phải hay thẳng hàng. Từ đó ra giao đoạn thẳng, diện tích đa giác, điểm trong đa giác, bao lồi. Giữ mọi thứ trong số nguyên (long long, đôi khi __int128) khi tọa độ nguyên, chỉ dùng số thực khi bắt buộc, và xử lý cẩn thận các trường hợp thẳng hàng, điểm nằm trên biên.

Tích có hướng

Với u⃗=(x1,y1)\vec u = (x_1, y_1), v⃗=(x2,y2)\vec v = (x_2, y_2), tích có hướng u⃗×v⃗=x1y2−y1x2\vec u \times \vec v = x_1 y_2 - y_1 x_2. Với ba điểm, cross⁡(A,B,C)=(B−A)×(C−A)\operatorname{cross}(A, B, C) = (B - A) \times (C - A) dương khi CC nằm bên trái đường có hướng A→BA \to B (quay ngược chiều kim đồng hồ), âm khi bên phải, bằng 0 khi thẳng hàng. Giá trị tuyệt đối là hai lần diện tích tam giác ABCABC.

Tọa độ tới 10910^9 thì hiệu tới 2⋅1092 \cdot 10^9, mỗi tích tới 4⋅10184 \cdot 10^{18}, hiệu hai tích tới 8⋅10188 \cdot 10^{18}: vẫn vừa long long (tới khoảng 9.2⋅10189.2 \cdot 10^{18}) nhưng sát giới hạn. Ép kiểu sang long long trước khi nhân; tọa độ lớn hơn thì dùng __int128.

Kéo A, B, C: dấu của tích có hướng cho biết C ở bên trái, bên phải hay trên đường A → B.

Ví dụBên Nào Của Hướng

Đề. t≤105t \le 10^5 bộ ba điểm, tọa độ tới 10910^9. Cho biết P3P_3 ở bên trái, bên phải hay trên đường thẳng có hướng P1→P2P_1 \to P_2.

Lời giải. cross⁡(P1,P2,P3)=(P2−P1)×(P3−P1)\operatorname{cross}(P_1, P_2, P_3) = (P_2 - P_1) \times (P_3 - P_1) dương khi đi từ hướng P1→P2P_1 \to P_2 sang hướng P1→P3P_1 \to P_3 phải quay ngược chiều kim đồng hồ, tức P3P_3 ở bên trái; âm là bên phải; bằng 0 là thẳng hàng. Toàn bộ là số nguyên, không có sai số.

C++
ll cross(ll ax, ll ay, ll bx, ll by, ll cx, ll cy) {
    return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
}

Mỗi hiệu tọa độ tới 2⋅1092 \cdot 10^9, mỗi tích tới 4⋅10184 \cdot 10^{18}, hiệu hai tích tới 8⋅10188 \cdot 10^{18}, vẫn dưới giới hạn long long khoảng 9,2⋅10189{,}2 \cdot 10^{18}. Chỉ dùng dấu của kết quả, nên không cần chia hay căn.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll cross(ll ax, ll ay, ll bx, ll by, ll cx, ll cy) {
    return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int t;
    cin >> t;
    while (t--) {
        ll x1, y1, x2, y2, x3, y3;
        cin >> x1 >> y1 >> x2 >> y2 >> x3 >> y3;
        ll c = cross(x1, y1, x2, y2, x3, y3);
        cout << (c > 0 ? "LEFT" : c < 0 ? "RIGHT" : "TOUCH") << '\n';
    }
}
5
0 0 4 0 2 3
0 0 4 0 2 -3
-2 -1 3 4 8 9
7 2 7 9 6 5
7 2 7 9 8 5

Mỗi bộ O(1)O(1).

Giao hai đoạn thẳng

Hai đoạn ABAB, CDCD cắt nhau “thật sự” khi CC, DD ở hai phía của đường ABAB và AA, BB ở hai phía của đường CDCD: d1=cross⁡(A,B,C)d_1 = \operatorname{cross}(A, B, C) và d2=cross⁡(A,B,D)d_2 = \operatorname{cross}(A, B, D) trái dấu, tương tự d3,d4d_3, d_4. Các trường hợp còn lại là có một tích bằng 0: điểm đó thẳng hàng với đoạn kia, và chung điểm khi nó nằm trong hộp bao của đoạn kia. Kiểm đủ bốn điểm như vậy là phủ hết các trường hợp, kể cả hai đoạn cùng nằm trên một đường thẳng.

So dấu bằng cách so từng tích với 0, đừng nhân d1⋅d2d_1 \cdot d_2 vì tràn số.

Kéo bốn đầu mút; thử cả trường hợp một đầu nằm đúng trên đoạn kia.

Hỏi nhanh

Đoạn (0,0)(0,0) tới (2,0)(2,0) và đoạn (3,0)(3,0) tới (5,0)(5,0). Bốn tích có hướng đều bằng 0. Hai đoạn có chung điểm không?

Ví dụHai Đoạn Có Gặp Nhau?

Đề. t≤105t \le 10^5 cặp đoạn thẳng đóng, tọa độ tới 10910^9. Cho biết hai đoạn có điểm chung không.

Lời giải. Hai đoạn cắt nhau thật sự khi mỗi đoạn có hai đầu nằm ở hai phía của đường thẳng chứa đoạn kia. Còn lại chỉ có thể chung điểm khi một đầu mút thẳng hàng với đoạn kia và nằm trong hộp bao của nó; kiểm đủ bốn đầu mút như vậy là phủ mọi trường hợp chạm, kể cả hai đoạn chồng nhau trên cùng một đường thẳng.

Bước 1: so dấu. sgn trả −1-1, 00, 11. So dấu bằng d1 * d2 < 0 trên các số đã lấy dấu, không nhân hai tích có hướng thật vì sẽ tràn.

C++
int d1 = sgn(cross(ax, ay, bx, by, cx, cy)), d2 = sgn(cross(ax, ay, bx, by, dx, dy));
int d3 = sgn(cross(cx, cy, dx, dy, ax, ay)), d4 = sgn(cross(cx, cy, dx, dy, bx, by));
bool ok = d1 * d2 < 0 && d3 * d4 < 0;

Bước 2: trường hợp chạm. onSeg(A, B, P) chỉ kiểm PP có nằm trong hộp bao của ABAB; nó được gọi khi tích có hướng bằng 0, tức PP thẳng hàng với ABAB. Thẳng hàng và trong hộp bao nghĩa là nằm trên đoạn.

C++
if (!d1 && onSeg(ax, ay, bx, by, cx, cy)) ok = 1;
if (!d2 && onSeg(ax, ay, bx, by, dx, dy)) ok = 1;
if (!d3 && onSeg(cx, cy, dx, dy, ax, ay)) ok = 1;
if (!d4 && onSeg(cx, cy, dx, dy, bx, by)) ok = 1;

Hai đoạn cùng nằm trên một đường mà rời nhau: cả bốn tích bằng 0 nhưng không đầu mút nào trong hộp bao của đoạn kia, kết quả NO.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll cross(ll ax, ll ay, ll bx, ll by, ll cx, ll cy) {
    return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
}

int sgn(ll x) {
    return (x > 0) - (x < 0);
}

bool onSeg(ll ax, ll ay, ll bx, ll by, ll px, ll py) {
    return min(ax, bx) <= px && px <= max(ax, bx) && min(ay, by) <= py && py <= max(ay, by);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int t;
    cin >> t;
    while (t--) {
        ll ax, ay, bx, by, cx, cy, dx, dy;
        cin >> ax >> ay >> bx >> by >> cx >> cy >> dx >> dy;
        int d1 = sgn(cross(ax, ay, bx, by, cx, cy)), d2 = sgn(cross(ax, ay, bx, by, dx, dy));
        int d3 = sgn(cross(cx, cy, dx, dy, ax, ay)), d4 = sgn(cross(cx, cy, dx, dy, bx, by));
        bool ok = d1 * d2 < 0 && d3 * d4 < 0;
        if (!d1 && onSeg(ax, ay, bx, by, cx, cy)) ok = 1;
        if (!d2 && onSeg(ax, ay, bx, by, dx, dy)) ok = 1;
        if (!d3 && onSeg(cx, cy, dx, dy, ax, ay)) ok = 1;
        if (!d4 && onSeg(cx, cy, dx, dy, bx, by)) ok = 1;
        cout << (ok ? "YES" : "NO") << '\n';
    }
}
5
0 0 4 4 0 4 4 0
0 0 4 0 4 0 7 3
0 0 6 0 2 0 8 0
0 0 2 0 3 0 5 0
-2 -2 2 2 0 0 0 5

Mỗi cặp O(1)O(1).

Diện tích đa giác và định lý Pick

Công thức dây giày: với đa giác đơn P1,…,PnP_1, \dots, P_n, hai lần diện tích có dấu là ∑iPi×Pi+1\sum_i P_i \times P_{i+1} (với Pn+1=P1P_{n+1} = P_1); dương khi các đỉnh đi ngược chiều kim đồng hồ. Với tọa độ nguyên, hai lần diện tích là số nguyên, nên in nó để khỏi số thực.

Kéo các đỉnh trên lưới: diện tích tính bằng dây giày, số điểm biên bằng tổng gcd các cạnh, còn lại là số điểm bên trong.

Hỏi nhanh

Hình vuông có các đỉnh (0,0),(4,0),(4,4),(0,4)(0,0), (4,0), (4,4), (0,4). Có bao nhiêu điểm nguyên nằm hẳn bên trong?

Định lý Pick: đa giác đơn có đỉnh nguyên thỏa A=I+B/2−1A = I + B/2 - 1, với II là số điểm nguyên bên trong, BB là số điểm nguyên trên biên. Cạnh từ PP tới QQ chứa gcd⁡(∣Qx−Px∣,∣Qy−Py∣)\gcd(|Q_x - P_x|, |Q_y - P_y|) điểm nguyên không tính một đầu, nên BB là tổng các gcd⁡\gcd đó; suy ra I=(2A−B+2)/2I = (2A - B + 2)/2.

Ví dụDiện Tích Gấp Đôi

Đề. Đa giác đơn n≤1000n \le 1000 đỉnh, tọa độ tới 10910^9. In hai lần diện tích.

Lời giải. Công thức dây giày: hai lần diện tích có dấu là ∑i(xiyi+1−xi+1yi)\sum_i (x_i y_{i+1} - x_{i+1} y_i), với đỉnh sau đỉnh cuối là đỉnh đầu. Dấu phụ thuộc chiều đi của các đỉnh, nên lấy trị tuyệt đối.

Mỗi số hạng tới 2⋅10182 \cdot 10^{18}, cộng 10001000 số hạng có thể vượt long long giữa chừng dù kết quả cuối thì không, nên cộng bằng __int128. Ép từng thừa số trước khi nhân; viết (__int128)(x[i] * y[j]) là sai, vì phép nhân đã tràn trong long long trước khi ép.

C++
for (int i = 0; i < n; i++) {
    int j = (i + 1) % n;
    s += (__int128)x[i] * y[j] - (__int128)x[j] * y[i];
}

Kết quả không quá 8⋅10188 \cdot 10^{18}, nên ép về long long để in.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll n, x[1005], y[1005];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (int i = 0; i < n; i++) cin >> x[i] >> y[i];
    __int128 s = 0;
    for (int i = 0; i < n; i++) {
        int j = (i + 1) % n;
        s += (__int128)x[i] * y[j] - (__int128)x[j] * y[i];
    }
    if (s < 0) s = -s;
    cout << (ll)s << '\n';
}
4
0 0
0 4
4 4
4 0

O(n)O(n).

Ví dụĐiểm Lưới Của Đa Giác

Đề. Đa giác đơn n≤105n \le 10^5 đỉnh nguyên, tọa độ tới 10910^9. In số điểm nguyên bên trong và trên biên.

Lời giải. Tính hai lần diện tích 2A2A bằng dây giày. Số điểm nguyên trên biên BB là tổng gcd⁡(∣Δx∣,∣Δy∣)\gcd(|\Delta x|, |\Delta y|) của các cạnh, vì cạnh đó chứa đúng chừng ấy điểm nguyên nếu không tính một đầu. Định lý Pick A=I+B/2−1A = I + B/2 - 1 cho I=(2A−B+2)/2I = (2A - B + 2)/2.

Bước 1: một vòng cho cả hai. Cạnh ngang hay dọc cho gcd⁡(d,0)=d\gcd(d, 0) = d, đúng số đoạn đơn vị trên cạnh.

C++
for (int i = 0; i < n; i++) {
    int j = (i + 1) % n;
    s += (__int128)x[i] * y[j] - (__int128)x[j] * y[i];
    b += __gcd(llabs(x[j] - x[i]), llabs(y[j] - y[i]));
}

Bước 2: Pick. 2A−B+22A - B + 2 luôn chẵn theo định lý Pick, nên phép chia 2 là chia hết.

C++
if (s < 0) s = -s;
ll inside = (ll)((s - b + 2) / 2);
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int N = 1e5 + 5;
ll n, x[N], y[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (int i = 0; i < n; i++) cin >> x[i] >> y[i];
    __int128 s = 0;
    ll b = 0;
    for (int i = 0; i < n; i++) {
        int j = (i + 1) % n;
        s += (__int128)x[i] * y[j] - (__int128)x[j] * y[i];
        b += __gcd(llabs(x[j] - x[i]), llabs(y[j] - y[i]));
    }
    if (s < 0) s = -s;
    ll inside = (ll)((s - b + 2) / 2);
    cout << inside << ' ' << b << '\n';
}
6
0 0
6 0
6 4
2 4
2 2
0 2

O(nlog⁡V)O(n \log V).

Điểm trong đa giác

Bắn tia từ điểm PP sang phải; PP ở trong khi tia cắt biên một số lẻ lần. Đếm cạnh (A,B)(A, B) có đúng một đầu nằm cao hơn PP (quy ước nửa mở (A.y > P.y) != (B.y > P.y) để đỉnh nằm đúng độ cao của tia chỉ được đếm một lần) và PP nằm bên trái giao điểm. Điều kiện “bên trái” kiểm bằng dấu tích có hướng, không cần chia: nếu cạnh đi lên (Ay<ByA_y < B_y) thì PP bên trái khi cross⁡(A,B,P)>0\operatorname{cross}(A, B, P) > 0, cạnh đi xuống thì ngược lại.

Trước khi đếm, kiểm PP có nằm trên một cạnh (thẳng hàng và trong hộp bao) để trả lời “trên biên”. Mỗi truy vấn O(n)O(n).

Cạnh cam là cạnh bị đếm; kéo P qua các đỉnh lõm để thấy số lần cắt đổi chẵn lẻ.

Ví dụBa Miền Của Đa Giác

Đề. Đa giác đơn n≤1000n \le 1000 đỉnh, m≤1000m \le 1000 điểm truy vấn, tọa độ tới 10910^9. In INSIDE, OUTSIDE hoặc BOUNDARY.

Lời giải. Với mỗi điểm PP: trước hết kiểm PP có nằm trên cạnh nào không. Nếu không, bắn tia từ PP sang phải và đếm số cạnh cắt tia; lẻ là ở trong.

Bước 1: trên biên. Tích có hướng bằng 0 và nằm trong hộp bao của cạnh. Biên được ưu tiên khi in, nên điểm trên cạnh không bị xếp nhầm vì phép đếm chẵn lẻ.

C++
if (!c && min(x[i], x[j]) <= px && px <= max(x[i], x[j]) && min(y[i], y[j]) <= py && py <= max(y[i], y[j])) boundary = 1;

Bước 2: tia sang phải. (y[i] > py) != (y[j] > py): cạnh có đúng một đầu nằm hẳn phía trên tia. Đầu nằm đúng trên tia được coi là “không cao hơn”, nên khi tia đi qua một đỉnh, chỉ một trong hai cạnh kề được đếm (hoặc cả hai không, nếu đỉnh là chỗ lồi lên hay lõm xuống), đúng như số lần tia thật sự đi xuyên biên. (y[i] < y[j]) == (c > 0): cạnh đi lên thì PP ở bên trái giao điểm khi PP ở bên trái cạnh; cạnh đi xuống thì ngược lại.

C++
if ((y[i] > py) != (y[j] > py) && ((y[i] < y[j]) == (c > 0))) inside = !inside;
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int n, m;
ll x[1005], y[1005];

ll cross(ll ax, ll ay, ll bx, ll by, ll cx, ll cy) {
    return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    for (int i = 0; i < n; i++) cin >> x[i] >> y[i];
    while (m--) {
        ll px, py;
        cin >> px >> py;
        bool boundary = 0, inside = 0;
        for (int i = 0; i < n; i++) {
            int j = (i + 1) % n;
            ll c = cross(x[i], y[i], x[j], y[j], px, py);
            if (!c && min(x[i], x[j]) <= px && px <= max(x[i], x[j]) && min(y[i], y[j]) <= py && py <= max(y[i], y[j])) boundary = 1;
            if ((y[i] > py) != (y[j] > py) && ((y[i] < y[j]) == (c > 0))) inside = !inside;
        }
        cout << (boundary ? "BOUNDARY" : inside ? "INSIDE" : "OUTSIDE") << '\n';
    }
}
6 6
0 0
8 0
8 6
5 6
5 2
0 2
1 1
6 4
3 4
5 4
8 6
-1 1

Mỗi điểm O(n)O(n), tổng O(nm)O(nm).

Bao lồi

Chuỗi đơn điệu (Andrew): sắp các điểm theo (x,y)(x, y). Dựng nửa dưới bằng cách duyệt từ trái sang phải với một ngăn xếp: khi điểm mới làm ba điểm cuối quay phải, bỏ điểm giữa. Dựng nửa trên tương tự khi duyệt ngược lại. Ghép hai nửa, bỏ điểm đầu mút bị lặp. Tổng O(nlog⁡n)O(n \log n) do sắp xếp.

Điều kiện bỏ quyết định số điểm thẳng hàng trên biên: bỏ khi tích có hướng âm thì giữ mọi điểm biên, bỏ khi không dương thì chỉ giữ các đỉnh. Đề đòi mọi điểm trên biên thì dùng điều kiện thứ nhất.

Xanh lá là ngăn xếp hiện tại, cam là điểm đang xét, đỏ là điểm vừa bị bỏ. Bật giữ điểm thẳng hàng để so hai điều kiện bỏ.

Ví dụĐường Biên Ngoài Cùng

Đề. n≤2⋅105n \le 2 \cdot 10^5 điểm phân biệt, không thẳng hàng hết. In mọi điểm nằm trên biên bao lồi.

Lời giải. Chuỗi đơn điệu của Andrew. Sắp các điểm theo (x,y)(x, y); nửa dưới duyệt từ trái sang phải, nửa trên duyệt từ phải sang trái, mỗi nửa giữ một ngăn xếp và bỏ điểm giữa khi ba điểm cuối quay phải. Đề đòi mọi điểm trên biên, nên chỉ bỏ khi tích có hướng âm, giữ các điểm thẳng hàng.

Bước 1: nửa dưới. Điểm mới làm cross(lo[-2], lo[-1], p) < 0 thì lo[-1] nằm phía trong, bỏ. Bỏ xong mới đẩy điểm mới vào.

C++
for (int i = 0; i < n; i++) {
    while (lo.size() >= 2 && cross(lo[lo.size() - 2], lo.back(), p[i]) < 0) lo.pop_back();
    lo.push_back(p[i]);
}

Bước 2: nửa trên cùng vòng đó nhưng duyệt ngược. Hai nửa có chung hai đầu mút (điểm nhỏ nhất và lớn nhất), nên mỗi nửa bỏ điểm cuối trước khi ghép; ghép lo rồi hi được biên theo chiều ngược kim đồng hồ.

C++
lo.pop_back(), hi.pop_back();

Đề bảo đảm các điểm không thẳng hàng hết. Nếu thẳng hàng hết, giữ điểm thẳng hàng sẽ làm hai nửa trùng nhau và mỗi điểm bị in hai lần.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int n;
vector<pair<ll, ll>> p;

ll cross(pair<ll, ll> a, pair<ll, ll> b, pair<ll, ll> c) {
    return (b.first - a.first) * (c.second - a.second) - (b.second - a.second) * (c.first - a.first);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    p.resize(n);
    for (int i = 0; i < n; i++) cin >> p[i].first >> p[i].second;
    sort(p.begin(), p.end());
    vector<pair<ll, ll>> lo, hi;
    for (int i = 0; i < n; i++) {
        while (lo.size() >= 2 && cross(lo[lo.size() - 2], lo.back(), p[i]) < 0) lo.pop_back();
        lo.push_back(p[i]);
    }
    for (int i = n - 1; i >= 0; i--) {
        while (hi.size() >= 2 && cross(hi[hi.size() - 2], hi.back(), p[i]) < 0) hi.pop_back();
        hi.push_back(p[i]);
    }
    lo.pop_back(), hi.pop_back();
    cout << lo.size() + hi.size() << '\n';
    for (int i = 0; i < (int)lo.size(); i++) cout << lo[i].first << ' ' << lo[i].second << '\n';
    for (int i = 0; i < (int)hi.size(); i++) cout << hi[i].first << ' ' << hi[i].second << '\n';
}
9
0 0
2 0
4 0
4 3
2 3
0 3
2 1
1 2
3 2

Sắp O(nlog⁡n)O(n \log n), mỗi điểm vào và ra mỗi ngăn xếp tối đa một lần.

Cặp điểm gần nhất

Sắp điểm theo xx, quét từ trái sang phải và giữ trong set (sắp theo yy) các điểm có hoành độ cách điểm hiện tại không quá dd, với dd là khoảng cách nhỏ nhất tìm được tới giờ. Điểm mới chỉ cần so với các điểm trong set có tung độ trong [y−d,y+d][y - d, y + d]: trong hình chữ nhật d×2dd \times 2d đó, các điểm đôi một cách nhau ít nhất dd nên chỉ có hằng số điểm. Tổng O(nlog⁡n)O(n \log n).

Giữ d2d^2 dạng số nguyên, dd dùng để cắt cửa sổ lấy bằng căn làm tròn lên.

Dải xanh là các điểm trong set (x cách không quá d); khung cam là dải y ± d, chỉ các điểm xanh lá trong đó được so.

Ví dụBình Phương Khoảng Cách Gần Nhất

Đề. n≤2⋅105n \le 2 \cdot 10^5 điểm phân biệt, tọa độ tới 10910^9. In bình phương khoảng cách nhỏ nhất giữa hai điểm.

Lời giải. Sắp theo xx rồi quét. set chứa các điểm đã qua có hoành độ cách điểm hiện tại không quá dd, sắp theo yy; dd là khoảng cách nhỏ nhất tìm được tới giờ. Điểm mới chỉ cần so với các điểm trong set có tung độ trong [y−d,y+d][y - d, y + d]. Những điểm đó nằm trong hình chữ nhật d×2dd \times 2d và đôi một cách nhau ít nhất dd, nên chỉ có hằng số điểm.

Bước 1: cửa sổ theo xx. best là bình phương khoảng cách nhỏ nhất, giữ dạng nguyên (tới 8⋅10188 \cdot 10^{18}, vừa long long). d là căn làm tròn lên, chỉ dùng để cắt cửa sổ, nên lấy rộng hơn một chút không làm sai. Con trỏ j xóa các điểm có hoành độ kém điểm hiện tại hơn d; mỗi điểm vào và ra một lần.

C++
ll d = (ll)sqrtl((long double)best) + 1;
while (p[i].first - p[j].first > d) s.erase({p[j].second, p[j].first}), j++;

Bước 2: dải theo yy. set lưu cặp (y,x)(y, x) để sắp theo yy; lower_bound nhảy tới điểm đầu tiên có tung độ ≥y−d\ge y - d, rồi duyệt tới khi vượt y+dy + d.

C++
set<pair<ll, ll>>::iterator it = s.lower_bound({p[i].second - d, LLONG_MIN});
for (; it != s.end() && it->first <= p[i].second + d; it++) {
    ll dx = p[i].first - it->second, dy = p[i].second - it->first;
    best = min(best, dx * dx + dy * dy);
}

Lúc đầu best = LLONG_MAX nên cửa sổ chứa mọi điểm; sau vài cặp best nhỏ đi và cửa sổ hẹp lại.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int n;
vector<pair<ll, ll>> p;
set<pair<ll, ll>> s;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    p.resize(n);
    for (int i = 0; i < n; i++) cin >> p[i].first >> p[i].second;
    sort(p.begin(), p.end());
    ll best = LLONG_MAX;
    int j = 0;
    for (int i = 0; i < n; i++) {
        ll d = (ll)sqrtl((long double)best) + 1;
        while (p[i].first - p[j].first > d) s.erase({p[j].second, p[j].first}), j++;
        set<pair<ll, ll>>::iterator it = s.lower_bound({p[i].second - d, LLONG_MIN});
        for (; it != s.end() && it->first <= p[i].second + d; it++) {
            ll dx = p[i].first - it->second, dy = p[i].second - it->first;
            best = min(best, dx * dx + dy * dy);
        }
        s.insert({p[i].second, p[i].first});
    }
    cout << best << '\n';
}
5
0 0
7 7
3 4
10 1
4 4

O(nlog⁡n)O(n \log n).

Khi đi thi

  • Tọa độ nguyên: mọi phép thử bằng tích có hướng trong long long, ép kiểu trước khi nhân, __int128 khi tổng có thể vượt 9⋅10189 \cdot 10^{18}; đừng so dấu bằng cách nhân hai tích.
  • Đọc kỹ quy ước biên: điểm trên cạnh, đoạn chạm đầu mút, điểm thẳng hàng trên bao lồi; viết riêng hàm kiểm điểm trên đoạn.
  • Diện tích in gấp đôi để khỏi số thực; điểm nguyên dùng Pick; số thực chỉ dùng khi đề bắt buộc, kèm ε\varepsilon.

Tóm tắt

  • cross⁡(A,B,C)\operatorname{cross}(A, B, C) dương là trái, âm là phải, 0 là thẳng hàng; trị tuyệt đối là hai lần diện tích tam giác.
  • Giao đoạn: trái dấu cả hai phía, hoặc một đầu thẳng hàng nằm trong hộp bao đoạn kia.
  • Dây giày cho diện tích; Pick: 2A=2I+B−22A = 2I + B - 2.
  • Bao lồi Andrew O(nlog⁡n)O(n \log n); cặp gần nhất bằng quét với cửa sổ dd.

Bài tập

1
Khoảng Quét Nhỏ Nhấtcanuc80k6906Sắp góc các điểm bằng atan2, tìm khoảng trống lớn nhất giữa hai góc kề (kể cả vòng qua 360); đáp án là 360 trừ khoảng đó.
LQDOJ
2
Biên Dịch Chuyển An Toàncanuc80k6907DD lớn nhất là nửa khoảng cách nhỏ nhất từ mỗi đỉnh tới đường thẳng qua hai đỉnh kề nó.
LQDOJ
3
Vùng Quét Khi Quaycanuc80k6908Bán kính lớn nhất là khoảng cách tới đỉnh xa nhất; nhỏ nhất là khoảng cách nhỏ nhất từ OO tới các cạnh, tính khoảng cách điểm tới đoạn.
LQDOJ
4
Một Bước Đến Góc Vuôngcanuc80k6910Tam giác vuông khi một tích vô hướng bằng 0; thử dịch mỗi đỉnh một đơn vị theo bốn hướng và kiểm lại, nhớ loại tam giác suy biến.
LQDOJ
5
Bốn Đoạn Khép Kíncanuc80k6911Hình chữ nhật cạnh song song trục: đúng hai đoạn ngang, hai đoạn dọc độ dài dương, các đầu mút ghép thành đúng bốn góc.
LQDOJ
6
Bốn Góc Có Cùng Quay Trái?canuc80k6919Tứ giác lồi khi bốn tích có hướng của các bộ ba đỉnh liên tiếp cùng dương.
LQDOJ
7
Đường Kính Manhattan Sau Mỗi Điểmcanuc80k6921Khoảng cách Manhattan lớn nhất là cực đại của hiệu x+yx + y và hiệu x−yx - y; giữ cực trị của hai đại lượng khi thêm điểm.
LQDOJ
8
Tổng Khoảng Cách Manhattan Của Mọi Cặpcanuc80k6922Tách hai tọa độ; sắp từng tọa độ và cộng đóng góp của mỗi điểm với các điểm đứng trước bằng tổng tiền tố.
LQDOJ
9
Đếm Giao Điểm Của Các Đoạn Vuông Góccanuc80k6923Quét theo xx: đoạn ngang mở và đóng là cộng, trừ trên Fenwick theo yy; đoạn dọc hỏi tổng trên đoạn yy của nó.
LQDOJ
10
Các Đường Chéo Chung Trung Điểmcanuc80k6925Hai đường chéo của hình bình hành có chung trung điểm: nhóm các cặp điểm theo tổng tọa độ, mỗi nhóm kk cặp góp (k2)\binom{k}{2}.
LQDOJ
11
Phủ Điểm Bằng Hai Đườngcanuc80k6926Nếu phủ được bằng hai đường thì một đường đi qua hai trong ba điểm đầu; thử ba đường đó, kiểm phần còn lại thẳng hàng.
LQDOJ
12
Đa Giác Nằm Trọn Bên Trongcanuc80k6933AA lồi: mỗi đỉnh của BB phải nằm nghiêm ngặt trong AA; kiểm bằng chặt nhị phân theo góc quanh một đỉnh của AA, O(log⁡n)O(\log n) mỗi điểm.
LQDOJ
13
Diện Tích Được Phủcanuc80k6934Quét theo xx, cây phân đoạn trên yy đã nén giữ số lần phủ và độ dài đang được phủ của mỗi nút.
LQDOJ
14
Những Đường Qua Nhiều Điểmcanuc80k6938K=1K = 1 thì vô số; ngược lại chuẩn hóa đường qua mọi cặp điểm (chia gcd, cố định dấu), đếm điểm trên mỗi đường.
LQDOJ
15
Đếm Tam Giác Không Suy Biếncanuc80k6941Đếm bộ ba thẳng hàng: quanh mỗi điểm nhóm các điểm khác theo hướng đã chuẩn hóa bằng gcd; lấy (n3)\binom{n}{3} trừ đi.
LQDOJ

Đọc thêm