Hình học tính toán (phần 2) - Sự giao nhau của các đường thẳng và Tính toán diện tích
Trong bài viết phần 1 về chủ đề Hình học tính toán, chúng ta đã cùng nghiên cứu về cách sử dụng vector trong các bài toán hình học. Còn trong bài viết này, tôi sẽ giới thiệu những vấn đề liên quan tới đường thẳng, giao điểm và sử dụng kiến thức về đại số tuyến tính để tính toán giao điểm, tính toán diện tích, qua đó áp dụng trong những bài toán hình học của lập trình thi đấu.
I. Biểu diễn đường thẳng
Trong Toán học phổ thông, các bạn đã biết có nhiều cách để biểu diễn một đường thẳng, vì thế cũng có nhiều cách biểu diễn đường thẳng trong máy tính, mà tùy vào dữ kiện đề bài ta sẽ chọn cách biểu diễn phù hợp. Dưới đây là một số cách biểu diễn đường thẳng thường dùng trong các bài toán Tin học:
- Dạng : Mỗi đường thẳng được đặc trưng bởi một cặp hệ số và tuy nhiên dạng biểu diễn này không thể hiện được các đường thẳng song song với trục .
- Dạng tổng quát: . Mỗi đường thẳng có thể được biểu diễn bởi bộ ba hệ số . Cách làm này gần gũi với đồ thị hàm số, nên nó thường xuyên được sử dụng nhất kể cả trong Toán học hay Tin học. Vector pháp tuyến có thể được dùng để viết một phương trình đường thẳng tương đương ngắn gọn hơn: với mọi nằm trên đường thẳng. Do đó, dạng tổng quát này còn được kí hiệu là .
- Dạng tham số: trong đó là một điểm trên đường thẳng còn là một vector chỉ phương của đường thẳng với tọa độ là . Như vậy, một đường thẳng được xác định bởi bộ bốn số trong đó là tọa độ điểm còn là tọa độ của vector chỉ phương . Biểu diễn này mang ý nghĩa: Đường thẳng là tập các điểm có tọa độ chính là tọa độ của vector với mọi .
Khi cần chuyển đổi giữa các biểu diễn, ta thực hiện biến đổi tương đương đại số, kết hợp với công thức chuyển đổi giữa vector chỉ phương và vector pháp tuyến .
II. Giao điểm giữa các đường thẳng và ứng dụng
1. Tìm giao điểm của hai đường thẳng
Đây là bài toán phổ biến nhất trong các bài toán về giao điểm, nhưng không phải ai cũng có thể thực hiện thành thạo.
Trong trường hợp lý tưởng, ta sẽ có được hai đường thẳng đều được biểu diễn ở dạng với là các hệ số xác định đường thẳng. Tuy nhiên, không phải lúc nào bài toán cũng tuyệt đẹp như vậy. Nhưng may mắn rằng, ta có thể dễ dàng tìm được biểu diễn tổng quát trên dựa vào hai điểm cho trước thuộc đường thẳng. Ví dụ có hai điểm phân biệt và và để tìm cho phương trình trên, ta có công thức:
Đường thẳng dù ở dạng nào, ta cũng có thể chọn được hai điểm phân biệt thuộc đường thẳng và dùng công thức trên để tính .
Giờ, coi như ta đã thu được hai đường thẳng được cho bởi hai phương trình:
Để tìm giao điểm của hai đường thẳng, ta chỉ cần giải hệ phương trình hai ẩn . Cách làm như sau:
- Nhân phương trình với và nhân phương trình với :
- Lấy phương trình thứ nhất trừ hai vế cho phương trình thứ hai, ta được:
- Cuối cùng, chia cả hai vế cho ta sẽ có phương trình giải ẩn :
- Phương trình giải ẩn cũng thu được theo cách biến đổi tương tự:
Như vậy, ta thu được giao điểm của hai đường thẳng đã cho.
Cài đặt
struct Line
{
double A, B, C;
};
// Trả về giao điểm hai đường thẳng.
// Riêng hai trường hợp song song hoặc trùng nhau thì ném ngoại lệ.
pair < int, int > find_line_intersection(Line line1, Line line2)
{
double det = line1.A * line2.B - line2.A * line1.B;
// Hai đường thẳng song song hoặc trùng nhau.
if (det == 0)
{
if (line1.A * line2.C == line2.A * line1.C)
{
throw "Coincident lines";
}
else
{
throw "Parallel lines";
}
}
else
{
double x = line2.B * line1.C - line1.B * line2.C / det;
double y = line1.A * line2.C - line2.A * line1.C / det;
return {x, y};
}
}
2. Tìm giao điểm của hai đoạn thẳng
Bài toán tiếp theo là cho bốn điểm trên mặt phẳng, cần xác định xem hai đoạn thẳng và có giao điểm duy nhất hay không, nếu có thì đưa ra tọa độ của giao điểm đó.
Sử dụng những kiến thức về tích có hướng, ta có thể thực hiện công việc này không mấy khó khăn. Tuy nhiên, trước tiên cần phải xem xét liệu trong số bốn điểm đã cho, có tồn tại ba điểm nào thẳng hàng hay không.
Nhắc lại về hàm CW và CCW
Kiến thức các bạn cần lưu ý ở đây là: Với góc thỏa mãn thì nên nếu góc ngược chiều kim đồng hồ thì tích có hướng dương, ngược lại tích có hướng âm (đã đề cập ở bài viết trước).
Xét ba điểm Giả sử ta đi từ điểm sang điểm theo đường thằng và đi tiếp sang điểm theo đường thẳng, khi đó:
- Tích có hướng sẽ là số dương nếu như chỗ rẽ tại là "rẽ trái" (bẻ góc ngược chiều kim đồng hồ). Đây gọi là công thức CCW.
- Tích có hướng sẽ là số âm nếu như chỗ rẽ tại là "rẽ phải" (bẻ góc ngược thuận kim đồng hồ). Đây gọi là công thức CW.
- Tích có hướng sẽ bằng nếu như ba điểm thẳng hàng.
Kiểm tra giao điểm của hai đoạn thẳng
Trường hợp tồn tại ba điểm thẳng hàng
Nếu tồn tại trong điểm đầu mút thẳng hàng, ta kiểm tra xem có tồn tại đầu mút của đoạn thẳng này thuộc đoạn thẳng kia hay không:
-
Nếu có thì rõ ràng là đoạn thẳng giao nhau tại ít nhất điểm (tại đầu mút vừa xét):
-
Nếu không thì rõ ràng là 2 đoạn thẳng không thể giao nhau:
Trường hợp không tồn tại ba điểm thẳng hàng
Nếu không tồn tại trong điểm đầu mút thẳng hàng thì đoạn thẳng và giao nhau khi:
- và nằm khác phía đối với đường thẳng và
- và nằm khác phía đối với đường thẳng .
Để và nằm khác phía đối với đường thẳng AB thì:
- ngược chiều kim đồng hồ và cùng chiều kim đồng hồ hoặc
- cùng chiều kim đồng hồ và ngược chiều kim đồng hồ.
Từ đó, ta có hệ sau:
Hình minh họa:
Cài đặt
const double eps = 1e-9;
// Xác định dấu của tích có hướng.
int sign(double x)
{
if (x > eps)
return 1;
if (x < -eps)
return -1;
return 0;
}
double cross(Vec AB, Vec AC)
{
return AB.x * AC.y - AC.x * AB.y;
}
double dot(Vec AB, Vec AC)
{
return AB.x * AC.x + AB.y * AC.y;
}
bool intersect(Point A, Point B, Point C, Point D)
{
int ABxAC = sign(cross(B - A, C - A));
int ABxAD = sign(cross(B - A, D - A));
int CDxCA = sign(cross(D - C, A - C));
int CDxCB = sign(cross(D - C, B - C));
if (ABxAC == 0 || ABxAD == 0 || CDxCA == 0 || CDxCB == 0)
{
// C on segment AB if ABxAC = 0 and CA.CB <= 0.
if (ABxAC == 0 && sign(dot(A - C, B - C)) <= 0)
return true;
if (ABxAD == 0 && sign(dot(A - D, B - D)) <= 0)
return true;
if (CDxCA == 0 && sign(dot(C - A, D - A)) <= 0)
return true;
if (CDxCB == 0 && sign(dot(C - B, D - B)) <= 0)
return true;
return false;
}
return (ABxAC * ABxAD < 0 && CDxCA * CDxCB < 0);
}
3. Tìm đường tròn đi qua ba điểm
Ta biết rằng, từ ba điểm không thẳng hàng, luôn luôn tồn tại duy nhất một đường tròn đi qua ba điểm đó. Để tìm được tâm của đường tròn này, ta ứng dụng chính bài toán xác định giao điểm đường thẳng.
Quan sát hình vẽ dưới đây:
Giả sử ba điểm cho trước là ta sẽ tìm đường trung trực của hai đoạn và sau đó tìm giao điểm của hai đường trung trực này, đó sẽ chính là tâm của đường tròn đi qua ba điểm .
Đường trung trực của một đoạn thẳng chính là đường thẳng vuông góc với đoạn thẳng tại trung điểm của nó. Để tìm đường trung trực của đoạn thẳng ta làm như sau:
-
Bước 1: Viết phương trình đường thẳng giả sử nó là .
-
Bước 2: Tìm trung điểm của đoạn bằng cách lấy trung bình cộng của hoành độ và trung bình cộng của tung độ.
-
Bước 3: Viết phương trình đường thẳng của đường thẳng vuông góc với đường thẳng có dạng là (xem hình vẽ).
-
Bước 4: Thay tọa độ của trung điểm vào phương trình đường thẳng ở bước để tìm và xác định đường trung trực.
Ví dụ
Với điểm và để tìm đường trung trực của đoạn ta thực hiện như sau:
- Bước 1: Viết phương trình đường thẳng theo đúng công thức:\begin{cases}A = y_Y - y_X = 0 - (-3) = 3 \\ B = x_X - x_Y = 2 - 1 = 1 \\ C = Ax_X + By_X = 3.2 + 1.(-3) = 2 \end{cases}$$ Vậy phương trình đường thẳng $XY$ có dạng: $3x + y = 3$.
- Bước 2: Đặt là trung điểm đoạn thẳng ta có:
- Bước 3: Phương trình đường thẳng của đường thẳng vuông góc với đường thằng có dạng: .
- Bước 4: Thay tọa độ của điểm vào phương trình :
Vậy phương trình đường trung trực của đoạn thẳng là: .
Làm tương tự với đoạn thẳng ta sẽ có hai phương trình của hai đường trung trực, và có thể tìm giao điểm của chúng như đã đề cập ở trên.
Cài đặt
struct Point
{
double x, y;
Point() { x = y = 0.0; }
Point(double x, double y) : x(x), y(y) {}
Point operator + (const Point &a) const { return Point(x + a.x, y + a.y); }
Point operator - (const Point &a) const { return Point(x - a.x, y - a.y); }
Point operator * (double k) const { return Point(x * k, y * k); }
Point operator / (double k) const { return Point(x / k, y / k); }
};
// Ax + By = C.
struct Line
{
double a, b, c;
Line(double a = 0, double b = 0, double c = 0) : a(a), b(b), c(c) {}
Line(Point A, Point B)
{
a = B.y - A.y;
b = A.x - B.x;
c = a * A.x + b * A.y;
}
};
Line perpendicular_bisector(Point A, Point B)
{
Point M = (A + B) / 2;
Line d = Line(A, B);
// the equation of a perpendicular line has the form: -Bx + Ay = D
double D = -d.b * M.x + d.a * M.y;
return Line(-d.b, d.a, D);
}
III. Tính toán diện tích
1. Diện tích tam giác
Có khá nhiều công thức giải tích để tính diện tích tam giác, tùy vào dữ kiện đề bài cho mà ta có thể lựa chọn cách tính phù hợp. Dưới đây là một số cách thường dùng:
- Nếu biết độ dài một cạnh của tam giác là và chiều cao tương ứng với cạnh đó là thì diện tích tam giác tính bởi công thức:
- Nếu biết độ dài hai cạnh của tam giác là và đồng thời góc xen giữa hai cạnh đó có số đo là thì diện tích tam giác tính bởi công thức:
- Nếu biết độ dài của ba cạnh tam giác lần lượt là thì diện tích tam giác có thể tính bởi công thức Heron:
trong đó là nửa chu vi của tam giác.
- Nếu biết hai vector và thì diện tích tam giác có thể tính bằng công thức tích chéo:S = \left|\frac{\overrightarrow{AB} \times \overrightarrow{AC}}{2}\right|$$ Tuy nhiên, trên máy tính thì ta cần biết trước tọa độ ba điểm $A = (x_A, y_A), B = (x_B, y_B), C = (x_C, y_C)$ để tính tích chéo (tích có hướng); khi đó công thức trên có thể biến đổi như sau:
Cài đặt
2. Diện tích đa giác
Ta đã biết rằng, đa giác là một đường gấp khúc khép kín. Trong lập trình, một đa giác sẽ được biểu diễn bằng một dãy các đỉnh liên tiếp nhau với tọa độ xác định trên hệ tọa độ Descartes.
Khi đó, diện tích đại số (có thể âm hoặc dương) của một đa giác không tự cắt được tính bằng công thức:
Và diện tích của đa giác sẽ chính bằng giá trị .
Việc xét diện tích đại số của đa giác có thể giúp chúng ta xác định được các đỉnh của đa giác này đang được liệt kê theo chiều nào. Nếu diện tích đại số là số dương () thì dãy các đỉnh được liệt kê theo chiều ngược chiều kim đồng hồ (và ngược lại).
Cài đặt
// Tính diện tích đa giác với dãy đỉnh truyền vào bởi một vector kiểu pair.
double polygon_area(vector < pair < double, double > > vertices)
{
// Thêm đỉnh đầu tiên vào cuối vector để xét tích cuối cùng trên tử số.
vertices.push_back(vertices[0]);
int n = vertices.size() - 1;
double area = 0;
for (int i = 0; i <= n; ++i)
area += (vertices[i].first - vertices[i + 1].first)
* (vertices[i].second + vertices[i + 1].second);
return abs(area) / 2.0;
}
IV. Một số công thức biến đổi tọa độ
1. Phép tịnh tiến
Xét một phép tịnh tiến theo vector . Phép tịnh tiến này sẽ biến một điểm thành điểm điểm sẽ biến thành điểm và điểm sẽ biến thành điểm .
Tổng quát, phép tịnh tiến theo vector sẽ biến điểm thành điểm trong đó:
2. Phép quay
Cho trước điểm để quay điểm ngược chiều kim đồng hồ một góc quanh gốc tọa độ, ta sử dụng công thức:
Tuy nhiên, cần lưu ý trong các ngôn ngữ lập trình, số đo góc sử dụng trong các hàm lượng giác sẽ là radian (rad) chứ không phải độ, do đó các bạn cần lưu ý công thức chuyển đổi giữa hai đơn vị này:
Ngoài ra, chúng ta cũng có thể xây dựng công thức quay một điểm quanh một điểm bất kì khác gốc tọa độ, bằng cách tịnh tiến hệ tọa độ sao cho điểm trùng với gốc tọa độ, rồi lại thực hiện phép quay như đã nêu ở trên.
Ví dụ
Cho điểm và để quay ngược chiều kim đồng hồ một góc quanh ta thực hiện như sau:
- Bước 1: Tịnh tiến hệ tọa độ sao cho trùng với gốc tọa độ. Lúc này, điểm có tọa độ mới là .
- Bước 2: Quay ngược chiều kim đồng hồ một góc quanh gốc tọa độ được điểm :
- Bước 3: tịnh tiến hệ tọa độ về vị trí ban đầu. Điểm có tọa độ mới là: .
Vậy quay ngược chiều kim đồng hồ một góc quanh ta được điểm .
Cài đặt
struct Point
{
double x, y;
Point() { x = y = 0.0; }
Point(double x, double y) : x(x), y(y) {}
Point operator + (const Point &a) const { return Point(x + a.x, y + a.y); }
Point operator - (const Point &a) const { return Point(x - a.x, y - a.y); }
Point operator * (double k) const { return Point(x * k, y * k); }
Point operator / (double k) const { return Point(x / k, y / k); }
};
Point rotations(Point A, Point C, double rad)
{
Point A2 = A - C;
Point B2 = Point(A2.x * cos(rad) - A2.y * sin(rad), A2.x * sin(rad) + A2.y * cos(rad));
Point B = B2 + C;
return B;
}
3. Phép đối xứng
Để lấy đối xứng một điểm qua một đường thẳng (trục đối xứng), ta tìm giao điểm của trục đối xứng và đường thẳng vuông góc với trục đối xứng đi qua sau đó lấy đối xứng với X qua .
Ví dụ
Cho điểm và đường thẳng để tìm điểm đối xứng với qua ta thực hiện như sau:
- Bước 1: Gọi đường thẳng đi qua và vuông góc với trục đối xứng có dạng .
- Bước 2: Để tìm ta thay tọa độ của vào phương trình:
- Bước 3: Xác định giao điểm của hai đường thẳng và :
- Bước 4: Xác định đối xứng với qua bằng công thức: (bởi vì là trung điểm của đoạn nối nên ):
Hình minh họa:
Cài đặt
struct Point
{
double x, y;
Point() { x = y = 0.0; }
Point(double x, double y) : x(x), y(y) {}
Point operator + (const Point &a) const { return Point(x + a.x, y + a.y); }
Point operator - (const Point &a) const { return Point(x - a.x, y - a.y); }
Point operator * (double k) const { return Point(x * k, y * k); }
Point operator / (double k) const { return Point(x / k, y / k); }
};
// Ax + By = C.
struct Line
{
double a, b, c;
Line(double a = 0, double b = 0, double c = 0) : a(a), b(b), c(c) {}
};
Point intersect(Line d1, Line d2)
{
double det = d1.a * d2.b - d2.a * d1.b;
// det != 0 because d1 is perpendicular to d2
return Point((d2.b * d1.c - d1.b * d2.c) / det, (d1.a * d2.c - d2.a * d1.c) / det);
}
Point symmetry(Point X, Line d)
{
// the equation of a perpendicular line has the form: -Bx + Ay = D.
double D = -d.b * X.x + d.a * X.y;
Line d2 = Line(-d.b, d.a, D);
Point Y = intersect(d, d2);
Point X2 = Point(2 * Y.x - X.x, 2 * Y.y - X.y);
return X2;
}
V. Tài liệu tham khảo
- Tài liệu giáo khoa chuyên Tin quyển 2 - Thầy Hồ Sĩ Đàm.
- https://en.wikipedia.org/wiki/Computational_geometry
- https://vnoi.info/wiki/algo/geometry/basic-geometry-2.md
©️ Tác giả: Vũ Quế Lâm từ Viblo
All rights reserved