Tác giả:
Reviewers:
Tiếp nối hai phần trước của series Hình học tính toán, trong bài viết này, ta sẽ tập trung vào các bài toán liên quan đến đa giác, diện tích đa giác và tính chất của các điểm thuộc đa giác.
Các bài viết trước thuộc series Hình học tính toán:
Phần dưới đây liệt kê một số thuật ngữ quen thuộc với đa giác:
Lưu ý: Trong bài viết này, nếu không có chú thích gì thêm, khi nhắc đến "đa giác" chúng ta sẽ hiểu đó là đa giác lồi.
Bất đẳng thức cũng có thể được biểu diễn sử dụng cạnh lớn nhất trong đa giác:
Để có thể giải quyết các bài toán hình học tính toán một cách hiệu quả, ta nên xây dựng các struct riêng cho từng đối tượng.
Trong bài này, các điểm, vector và đa giác sẽ được cài đặt bằng các CTDL sau:
#include <vector>
using namespace std;
struct Point{
int x;
int y;
// Khởi tạo mặc định
Point();
// Khởi tạo điểm từ toạ độ
Point(int __x, int __y): x(__x), y(__y) {}
};
struct Vector {
int x;
int y;
// Khởi tạo mặc định vector
Vector();
// Khởi tạo vector từ điểm đầu và điểm cuối
Vector(Point p, Point q): x(q.x - p.x), y(q.y - p.y) {}
// Tích có hướng của vector hiện tại và một vector khác
long long operator ^ (const Vector &v2) const {
return 1ll * x * v2.y - 1ll * y * v2.x;
}
};
struct Polygon {
// Số đỉnh thuộc đa giác
int nVertices;
// Danh sách các đỉnh thuộc đa giác
vector <Point> vertices;
// Khởi tạo mặc định
Polygon();
// Khởi tạo tam giác từ các đỉnh
Polygon(Point A, Point B, Point C) {
vertices.push_back(A);
vertices.push_back(B);
vertices.push_back(C);
}
// Khởi tạo đa giác từ danh sách đỉnh
Polygon(vector <Point> __vertices) {
vertices = __vertices;
}
// Diện tích, được cài đặt tại phần Diện tích đa giác
double area();
// Hai lần diện tích, được cài đặt tại phần Diện tích đa giác
long long area2();
};
Độ lớn của phần mặt phẳng bị chiếm chỗ bởi một đa giác được gọi là diện tích của đa giác đó. Người ta thường dùng chữ để ký hiệu diện tích, chẳng hạn diện tích ngũ giác có thể được ký hiệu là .
Bài toán: VNOJ - Diện tích đa giác
Tóm tắt đề bài: Cho đa giác lồi được tạo thành từ điểm cho trước theo đúng thứ tự đó, điểm thứ trong số đó có toạ độ là . Tính diện tích đa giác trên.
Gọi điểm thuộc đa giác theo đúng thứ tự trên là , trong đó là điểm có tung độ nhỏ nhất trong số các điểm cùng có hoành độ nhỏ nhất. Để tiện biểu diễn các công thức, trong bài viết này ta sẽ quy ước thêm .
Các công thức tính diện tích cho tam giác và một số dạng tứ giác đặc biệt vốn đã rất phổ biến. Tuy vậy, gần như chẳng có công thức nào cho phép ta tính ngay diện tích của một đa giác bất kỳ (ngay cả tứ giác đã khó rồi). Chỉ có một cách duy nhất để giải bài toán này: chia hình này thành những hình có thể tính được diện tích.
Nếu bài toán trên xuất hiện trong đề thi vào THPT, với một hình vẽ chẳng có tính chất gì đặc biệt cả, ta đành phải chọn cách chia đơn giản nhất: đường chéo. Tại một đỉnh của đa giác, vẽ các đường chéo đến tất cả các đỉnh không kề với nó:
Bây giờ, hình đa giác đã chuyển thành một loạt các tam giác. Mà tam giác thì số công thức tính diện tích nhiều vô kể:
Sau khi áp dụng các công thức để tính diện tích tam giác, việc còn lại chỉ là cộng các kết quả lại. Tổng nhận được chắc chắn là diện tích của đa giác cần tính.
Trong các công thức trên, hai công thức đầu ít nhiều phải sử dụng phép căn bậc hai, còn công thức thứ ba với phép toán vector thì đơn giản hơn cả. Khi các đỉnh của đa giác được cho ngược chiều kim đồng hồ, ta có thể bỏ giá trị tuyệt đối của công thức trên:
Đến đây, ta đã có một công thức khá đẹp để tính diện tích đa giác. Tuy nhiên, ta vẫn có thể tiếp tục biến đổi
Ta áp dụng lại phép chia đa giác và tính diện tích, nhưng lần này với các đỉnh từ
Suy ra:
Trong trường hợp các đỉnh được cho theo thứ tự ngược lại,
Hoàn toàn tương tự, ta biến đổi được thành công thức giống như
Như vậy, diện tích của một đa giác với các đỉnh được cho lần lượt theo thứ tự, không phân biệt cùng chiều hay ngược chiều kim đồng hồ là:
Công thức này được gọi là công thức tam giác.
Người ta cũng hay viết
Do khi tính định thức, ta lấy tổng của các hiệu giữa hai phần tử chéo nhau, người ta cũng gọi công thức tam giác là công thức Shoelace (Shoelace Formula) (shoelace nghĩa là dây giày).
Công thức tam giác
trong đó
Bằng một hướng tiếp cận khác, chúng ta cũng có công thức sau:
Định lý: Diện tích đa giác
Công thức
Về cơ bản, nếu ta khai triển công thức hình thang, ta vẫn sẽ thu được công thức tam giác. Tuy nhiên, có một cách chứng minh chỉ sử dụng phép chia hình và diện tích của những hình cơ bản, bạn đọc tham khảo bên dưới.
Xét trường hợp các đỉnh được cho ngược chiều kim đồng hồ. Với mọi
Trường hợp 1:
Xét hình thang
Hoàn toàn tương tự, ta tính được diện tích của các hình thang
Ta có thể thấy trên đa giác, tồn tại một điểm
Trường hợp 2:
Giả sử giá trị
Ta cũng biết rằng, phép tịnh tiến tạo ra hình mới bằng với hình cũ, và diện tích của chúng là giống nhau. Vậy diện tích hình
Như vậy, trong cả hai trường hợp, diện tích của đa giác với các đỉnh được cho ngược chiều kim đồng hồ được tính theo công thức sau:
Hoàn toàn tương tự, khi các đỉnh được cho cùng chiều kim đồng hồ, diện tích của đa giác là:
Hai công thức trên đều cho kết quả không âm. Vậy ta có điều phải chứng minh.
Chú ý: Cách chứng minh trên mô phỏng phép tính tích phân. Sử dụng phép tính tích phân, chứng minh trên sẽ còn đơn giản hơn nữa.
Dưới đây là cài đặt của công thức tam giác
double Polygon::area() {
long long s = 0;
for (int i = 0; i < nVertices; i ++) {
int i1 = (i + 1) % nVertices;
s += 1ll * vertices[i].x * vertices[i1].y
- 1ll * vertices[i].y * vertices[i1].x;
}
return abs(1.0 * s / 2);
}
Nếu nộp thử đoạn code trên, ta sẽ thấy kết quả chưa được đẹp lắm. Về mặt lý thuyết, phương pháp này không có gì sai. Vấn đề nằm ở kiểu dữ liệu double
. Đối với kiểu này, các số được biểu diễn dưới dạng dấu phẩy động double
được.long double
có vẻ cũng là một lựa chọn, tuy nhiên trên phần lớn hệ máy, đó cũng chỉ là double
mà thôi.
Để ý thấy hai lần diện tích của đa giác là một số nguyên. Lợi dụng điều đó, với bài toán yêu cầu in ra chính xác diện tích đa giác, ta làm như sau:
long long Polygon::area2() {
long long s = 0;
for (int i = 0; i < nVertices; i ++) {
int i1 = (i + 1) % nVertices;
s += 1ll * vertices[i].x * vertices[i1].y
- 1ll * vertices[i].y * vertices[i1].x;
}
return abs(s);
}
int main() {
Polygon plg;
// ...
auto s = plg.area2();
cout << s / 2 << "." << 5 * (s % 2);
return 0;
}
Nếu bạn thích dùng tích có hướng như công thức
long long Polygon::area2() {
long long s = 0;
Point o(0, 0);
for (int i = 0; i < nVertices; i ++) {
int i1 = (i + 1) % nVertices;
Vector vi(o, vertices[i]);
Vector vi1(o, vertices[i1]);
s += (vi ^ vi1);
}
return abs(s);
}
Rất dễ dàng để nhận ra rằng độ phức tạp về cả không gian và thời gian cần để cài đặt tất cả các công thức đã nói trên là
C.Jordan đã chứng minh rằng: Mọi đa giác không tự cắt đều chia mặt phẳng thành hai miền, một miền bên trong đa giác và một miền bên ngoài đa giác. Như vậy, một điểm có thể nằm ở một trong ba vị trí tương đối sau với một đa giác:
enum PointPolygonPosition {
INSIDE, // điểm nằm trong đa giác
OUTSIDE, // điểm nằm ngoài đa giác
BOUNDARY // điểm nằm trên cạnh đa giác
};
Bài toán: Cho đa giác
a) Đa giác đã cho là đa giác lồi
b) Đa giác đã cho là đa giác không tự cắt bất kỳ (tức là có thể không lồi)
Để thực hành trường hợp này, bạn đọc tham khảo bài Codeforces - Theodore Roosevelt.
Kiểm tra bằng diện tích là phương pháp đơn giản nhất để xem một điểm có nằm trong đa giác không.
Với mỗi điểm
PointPolygonPosition position(Polygon plg, Point p) {
long long sSumTris = 0;
for (int i = 0; i < plg.nVertices; i++) {
int i1 = (i + 1) % plg.nVertices;
Polygon tri(p, plg.vertices[i], plg.vertices[i1]);
auto sTri = tri.area2();
if (!sTri) {
return BOUNDARY;
}
sSumTris += sTri;
}
return (sSumTris == plg.area2() ? INSIDE : OUTSIDE);
}
Cách kiểm tra này có độ phức tạp không gian là
Do đa giác đã cho là đa giác lồi, nên nếu một điểm
Ta có thể dễ dàng kiểm tra trường hợp thứ hai bằng cách kiểm tra xem hai vector
Xét trường hợp thứ nhất. Ta biết tất cả các đỉnh của đa giác được sắp xếp theo thứ tự ngược chiều kim đồng hồ. Do vậy, nếu điểm
Về kiểm tra tính chất "trái, phải", ta sử dụng phép nhân có hướng vector (xem phần 2 của series này)
Tóm lại, để kiểm tra vị trí tương đối của một điểm
bool isCW(Point a, Point b, Point c) {
return (Vector(a, b) ^ Vector(a, c)) < 0;
}
PointPolygonPosition position(Polygon plg, Point p) {
// Kiểm tra P có thuộc A1An không
Vector pa1(p, plg.vertices[0]);
Vector pan(p, plg.vertices[plg.nVertices - 1]);
if (pa1 ^ pan == 0) {
if (1ll * pa1.x * pan.x <= 0) {
return BOUNDARY;
}
return OUTSIDE;
}
// Tìm kiếm nhị phân điểm k
int l = 1, r = plg.nVertices;
while (r - l > 1) {
int mid = (l + r) >> 1;
if (isCW(plg.vertices[0], p, plg.vertices[mid])) {
l = mid;
} else {
r = mid;
}
}
int k = l;
if (k == plg.nVertices - 1) {
return OUTSIDE;
}
// Kiểm tra xem P có thuộc tam giác không
if (Vector(p, plg.vertices[k]) ^ Vector(p, plg.vertices[k + 1]) == 0) {
return BOUNDARY;
}
long long ss = 0;
ss += Polygon(p, plg.vertices[0], plg.vertices[k]).area2();
ss += Polygon(p, plg.vertices[k], plg.vertices[k + 1]).area2();
ss += Polygon(p, plg.vertices[k + 1], plg.vertices[0]).area2();
if (ss == Polygon(plg.vertices[0], plg.vertices[k],
plg.vertices[k + 1]).area2()) {
return INSIDE;
}
return OUTSIDE;
}
Cách làm trên có độ phức tạp không gian là
Để thực hành trường hợp này, bạn đọc tham khảo bài CSES - Point in Polygon.
Nếu đa giác có "góc lõm", hai phương pháp trên sẽ không sử dụng được nữa.
Đề kiểm tra điểm thuộc miền trong hay ngoài đa giác đơn (không tự cắt), có một thuật toán rất nổi tiếng. Đó là thuật toán chiếu tia (ray casting). Theo đó, từ mỗi điểm
Với dữ kiện đầu vào của bài toán, các đỉnh của đa giác đã được cho theo thứ tự. Như vậy, ta chỉ cần duyệt qua toàn bộ các cặp đỉnh để đểm số giao điểm. Về phần điểm
Giả sử ta có tia
Khi chiếu tia
PointPolygonPosition position(Polygon plg, Point p) {
bool isIn = false;
for (int i = 0; i < plg.nVertices; i++) {
int i1 = (i + 1) % plg.nVertices;
auto vpi = Vector(p, plg.vertices[i]);
auto vpj = Vector(p, plg.vertices[i1]);
// Trường hợp nằm trên cạnh
if ((vpi ^ vpj) == 0) {
int xl = min(plg.vertices[i].x, plg.vertices[i1].x);
int xh = max(plg.vertices[i].x, plg.vertices[i1].x);
int yl = min(plg.vertices[i].y, plg.vertices[i1].y);
int yh = max(plg.vertices[i].y, plg.vertices[i1].y);
if (p.x >= xl && p.x <= xh && p.y >= yl && p.y <= yh) {
return BOUNDARY;
}
}
// Trường hợp các đỉnh của đa giác ngược chiều kim đồng hồ
// Chú ý rằng chỉ có một bên của bất đẳng thức có dấu bằng
isIn ^= (plg.vertices[i].y <= p.y
&& p.y < plg.vertices[i1].y && (vpi ^ vpj) > 0);
// Trường hợp các đỉnh của đa giác cùng chiều kim đồng hồ
isIn ^= (plg.vertices[i1].y <= p.y
&& p.y < plg.vertices[i].y && (vpj ^ vpi) > 0);
}
if (isIn) {
return INSIDE;
}
return OUTSIDE;
}
Độ phức tạp của cài đặt trên là
Bài toán: CSES - Polygon Lattice Points
Tóm tắt đề bài: Cho đa giác
Xét đường thẳng đi qua hai điểm
Khi
Như vậy, số điểm nguyên nằm trên mỗi cạnh
Lưu ý: Về mặt định nghĩa, ta vẫn có __gcd()
. Do đó, để chắc chắn có một kết quả dương, ta sẽ sử dụng giá trị
Định lý Pick: Cho một đa giác không tự cắt với các đỉnh có toạ độ nguyên và diện tích khác không. Gọi diện tích đa giác là
Do độ dài của bài viết cũng như độ dài của chứng minh, tác giả sẽ không trình bày phần chứng minh trong bài viết này. Bạn đọc tham khảo chứng minh ở đây.
Trong
Tác giả cũng sẽ không trình bày cài đặt cho bài toán này do độ dài của bài viết.
Độ phức tạp không gian cần cho bài toán này là