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 , , tích có hướng . Với ba điểm, dương khi nằm bên trái đường có hướng (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 .
Tọa độ tới thì hiệu tới , mỗi tích tới , hiệu hai tích tới : vẫn vừa long long (tới khoảng ) 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.
Ví dụBên Nào Của Hướng
Đề. bộ ba điểm, tọa độ tới . Cho biết ở bên trái, bên phải hay trên đường thẳng có hướng .
Lời giải. dương khi đi từ hướng sang hướng phải quay ngược chiều kim đồng hồ, tức ở 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ố.
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 , mỗi tích tới , hiệu hai tích tới , vẫn dưới giới hạn long long khoảng . Chỉ dùng dấu của kết quả, nên không cần chia hay căn.
#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
LEFT
RIGHT
TOUCH
LEFT
RIGHT
Mỗi bộ .
Giao hai đoạn thẳng
Hai đoạn , cắt nhau “thật sự” khi , ở hai phía của đường và , ở hai phía của đường : và trái dấu, tương tự . 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 vì tràn số.
Hỏi nhanh
Đoạn tới và đoạn tới . Bốn tích có hướng đều bằng 0. Hai đoạn có chung điểm không?
Thẳng hàng chưa đủ: có hoành độ ngoài , ngoài , nên không đầu mút nào nằm trên đoạn kia và hai đoạn rời nhau.
Ví dụHai Đoạn Có Gặp Nhau?
Đề. cặp đoạn thẳng đóng, tọa độ tới . 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ả , , . 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.
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 có nằm trong hộp bao của ; nó được gọi khi tích có hướng bằng 0, tức thẳng hàng với . Thẳng hàng và trong hộp bao nghĩa là nằm trên đoạn.
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.
#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
YES
YES
YES
NO
YES
Mỗi cặp .
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 , hai lần diện tích có dấu là (với ); 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.
Hỏi nhanh
Hình vuông có các đỉnh . Có bao nhiêu điểm nguyên nằm hẳn bên trong?
, trên biên có điểm nguyên. Pick cho , đúng là các điểm có hai tọa độ trong .
Định lý Pick: đa giác đơn có đỉnh nguyên thỏa , với là số điểm nguyên bên trong, là số điểm nguyên trên biên. Cạnh từ tới chứa điểm nguyên không tính một đầu, nên là tổng các đó; suy ra .
Ví dụDiện Tích Gấp Đôi
Đề. Đa giác đơn đỉnh, tọa độ tới . 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à , 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 , cộng 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.
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á , nên ép về long long để in.
#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
32
.
Ví dụĐiểm Lưới Của Đa Giác
Đề. Đa giác đơn đỉnh nguyên, tọa độ tới . 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 bằng dây giày. Số điểm nguyên trên biên là tổng 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 cho .
Bước 1: một vòng cho cả hai. Cạnh ngang hay dọc cho , đúng số đoạn đơn vị trên cạnh.
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. luôn chẵn theo định lý Pick, nên phép chia 2 là chia hết.
if (s < 0) s = -s;
ll inside = (ll)((s - b + 2) / 2);
#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
11 20
.
Điểm trong đa giác
Bắn tia từ điểm sang phải; ở trong khi tia cắt biên một số lẻ lần. Đếm cạnh có đúng một đầu nằm cao hơn (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à 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 () thì bên trái khi , cạnh đi xuống thì ngược lại.
Trước khi đếm, kiểm 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 .
Ví dụBa Miền Của Đa Giác
Đề. Đa giác đơn đỉnh, điểm truy vấn, tọa độ tới . In INSIDE, OUTSIDE hoặc BOUNDARY.
Lời giải. Với mỗi điểm : trước hết kiểm có nằm trên cạnh nào không. Nếu không, bắn tia từ 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ẻ.
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ì ở bên trái giao điểm khi ở bên trái cạnh; cạnh đi xuống thì ngược lại.
if ((y[i] > py) != (y[j] > py) && ((y[i] < y[j]) == (c > 0))) inside = !inside;
#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
INSIDE
INSIDE
OUTSIDE
BOUNDARY
BOUNDARY
OUTSIDE
Mỗi điểm , tổng .
Bao lồi
Chuỗi đơn điệu (Andrew): sắp các điểm theo . 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 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.
Ví dụĐường Biên Ngoài Cùng
Đề. đ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 ; 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.
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ồ.
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.
#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
6
0 0
2 0
4 0
4 3
2 3
0 3
Sắp , 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 , quét từ trái sang phải và giữ trong set (sắp theo ) các điểm có hoành độ cách điểm hiện tại không quá , với 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 : trong hình chữ nhật đó, các điểm đôi một cách nhau ít nhất nên chỉ có hằng số điểm. Tổng .
Giữ dạng số nguyên, dùng để cắt cửa sổ lấy bằng căn làm tròn lên.
Ví dụBình Phương Khoảng Cách Gần Nhất
Đề. điểm phân biệt, tọa độ tới . In bình phương khoảng cách nhỏ nhất giữa hai điểm.
Lời giải. Sắp theo 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á , sắp theo ; 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 . Những điểm đó nằm trong hình chữ nhật và đôi một cách nhau ít nhất , nên chỉ có hằng số điểm.
Bước 1: cửa sổ theo . best là bình phương khoảng cách nhỏ nhất, giữ dạng nguyên (tới , 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.
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 . set lưu cặp để sắp theo ; lower_bound nhảy tới điểm đầu tiên có tung độ , rồi duyệt tới khi vượt .
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.
#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
1
.
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,__int128khi tổng có thể vượt ; đừ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 .
Tóm tắt
- 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: .
- Bao lồi Andrew ; cặp gần nhất bằng quét với cửa sổ .
Bài tập
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 đó.Đọc thêm
- Hình học tính toán phần 1VNOI WikiVector, tích có hướng, giao đoạn bằng tiếng Việt.
- Basic Geometrycp-algorithmsTích vô hướng, tích có hướng, giao đường thẳng.
- Convex Hull constructioncp-algorithmsGraham và chuỗi đơn điệu, giữ điểm thẳng hàng.
- Geometry PrimitivesUSACO GuideGiao đoạn, điểm trong đa giác, bài luyện.