🔷 Геометрия на Полигони
Полигонът е затворена верига от отсечки. В състезанията върховете обикновено се дават в ред (по или обратно на часовниковата стрелка).
Полигонът се нарича прост, ако неговите страни не се пресичат една с друга (освен в общите върхове). Полигонът се нарича изпъкнал, ако всички вътрешни ъгли са по-малки от 180° и всеки отрезък между две точки в полигона лежи изцяло вътре в него.
0. Основни Дефиниции и Структури
struct Point {
long long x, y;
Point(long long x = 0, long long y = 0) : x(x), y(y) {}
Point operator-(const Point& p) const { return Point(x - p.x, y - p.y); }
Point operator+(const Point& p) const { return Point(x + p.x, y + p.y); }
};
// Векторно произведение (Cross Product)
long long cross(const Point& a, const Point& b) {
return (long long)a.x * b.y - (long long)a.y * b.x;
}
// Скаларно произведение (Dot Product)
long long dot(const Point& a, const Point& b) {
return (long long)a.x * b.x + (long long)a.y * b.y;
}
// Дължина на вектор на квадрат
long long len2(const Point& a) {
return a.x * a.x + a.y * a.y;
}
1. Лице на Полигон (Shoelace Formula)
Площта на произволен прост полигон с върхове \((x_1, y_1), \dots, (x_n, y_n)\) може да се пресметне чрез формулата на обущарския връзка (Shoelace Formula). Тя е базирана на ориентираните лица на трапеци (или триъгълници).
\(2 \cdot S = \left| \sum_{i=1}^{n} (x_i y_{i+1} - x_{i+1} y_i) \right|\)
където \((x_{n+1}, y_{n+1}) = (x_1, y_1)\).
Геометрична Интуиция
Формулата работи, като разглежда всяка страна на полигона и изчислява "ориентираното лице" на трапеца, образуван между тази страна и x-оста. Ако върховете са в обратен ред на часовниковата стрелка, сумата дава положителна площ; ако са по часовниковата стрелка – отрицателна.
Имплементация
// Връща удвоеното лице (2 * S)
long long polygonArea2(const vector<Point>& p) {
long long area = 0;
int n = p.size();
for (int i = 0; i < n; i++) {
area += cross(p[i], p[(i + 1) % n]);
}
return abs(area); // Абсолютна стойност за положително лице
}
// Връща действителното лице (може да е дробно)
double polygonArea(const vector<Point>& p) {
return polygonArea2(p) / 2.0;
}
// Знаково лице (позитивно ако върховете са counter-clockwise)
long long signedArea2(const vector<Point>& p) {
long long area = 0;
int n = p.size();
for (int i = 0; i < n; i++) {
area += cross(p[i], p[(i + 1) % n]);
}
return area;
}
Пример
За квадрат с върхове \((0, 0), (2, 0), (2, 2), (0, 2)\): - \(2S = |0 \cdot 0 - 2 \cdot 0 + 2 \cdot 2 - 2 \cdot 0 + 2 \cdot 2 - 0 \cdot 2 + 0 \cdot 0 - 0 \cdot 2| = |0 + 4 + 4 + 0| = 8\) - \(S = 4\) (коректно за квадрат със страна 2)
2. Периметър на Полигон
Периметърът е сумата от дължините на всички страни на полигона.
// Периметър (дробно число)
double polygonPerimeter(const vector<Point>& p) {
double perimeter = 0;
int n = p.size();
for (int i = 0; i < n; i++) {
Point diff = p[(i + 1) % n] - p[i];
perimeter += sqrt(len2(diff));
}
return perimeter;
}
// За целочислени координати - на квадрат
long long polygonPerimeter2(const vector<Point>& p) {
long long perimeter2 = 0;
int n = p.size();
for (int i = 0; i < n; i++) {
Point diff = p[(i + 1) % n] - p[i];
perimeter2 += len2(diff);
}
return perimeter2;
}
3. Теорема на Пик (Pick's Theorem)
За полигон, чиито върхове са целочислени координати (lattice polygon): \(S = I + \frac{B}{2} - 1\)
- \(S\): Лице на полигона.
- \(I\): Брой целочислени точки вътре в полигона.
- \(B\): Брой целочислени точки по границата (boundary).
Намиране на B (Boundary Points)
\(B\) може да се намери, като се сумират НОД на разликите в координатите за всяка страна: \(B = \sum_{i=1}^{n} \gcd(|x_i - x_{i+1}|, |y_i - y_{i+1}|)\)
long long gcd(long long a, long long b) {
return b ? gcd(b, a % b) : a;
}
// Брой целочислени точки по границата
long long boundaryPoints(const vector<Point>& p) {
long long B = 0;
int n = p.size();
for (int i = 0; i < n; i++) {
Point diff = p[(i + 1) % n] - p[i];
B += gcd(abs(diff.x), abs(diff.y));
}
return B;
}
// Брой целочислени точки вътре (използвайки Pick's Theorem)
long long interiorPoints(const vector<Point>& p) {
long long area2 = polygonArea2(p);
long long B = boundaryPoints(p);
// S = I + B/2 - 1 => I = S - B/2 + 1
// 2S = 2I + B - 2 => I = (2S - B + 2) / 2
return (area2 - B + 2) / 2;
}
Пример
За триъгълник с върхове \((0, 0), (3, 0), (0, 4)\): - Лице: \(S = \frac{3 \cdot 4}{2} = 6\) - \(B = \gcd(3, 0) + \gcd(3, 4) + \gcd(0, 4) = 3 + 1 + 4 = 8\) - \(I = S - \frac{B}{2} + 1 = 6 - 4 + 1 = 3\)
4. Точка в Полигон (Point in Polygon)
4.1. Изпъкнал полигон
За изпъкнал полигон може да се реши за \(O(\log N)\) с двоично търсене по ъглите.
// Проверка дали точка е в изпъкнал полигон (O(log n))
bool inConvexPolygon(const vector<Point>& p, Point pt) {
int n = p.size();
// Проверка дали точката е от правилната страна на всички страни
bool pos = false, neg = false;
for (int i = 0; i < n; i++) {
long long cp = cross(p[(i + 1) % n] - p[i], pt - p[i]);
if (cp > 0) pos = true;
if (cp < 0) neg = true;
}
return !(pos && neg); // Вътре, ако всички cross products са с един знак
}
4.2. Произволен полигон (Ray Casting)
За произволен (не непременно изпъкнал) полигон използваме алгоритъма Ray Casting.
Идея: Пускаме лъч от точката \(P\) надясно (към \(+\infty\)). Броим колко пъти лъчът пресича страните на полигона. * Нечетен брой пресичания \(\implies\) Точката е вътре. * Четен брой (включително 0) \(\implies\) Точката е вън.
Гранични случаи: - Точката лежи точно на ръб - Лъчът минава през връх на полигона - Лъчът е успореден на някоя страна
// Ray Casting алгоритъм - произволен полигон
bool isInside(const vector<Point>& p, Point pt) {
int n = p.size();
bool inside = false;
for (int i = 0, j = n - 1; i < n; j = i++) {
// Проверяваме дали лъчът пресича ръба (j, i)
if (((p[i].y > pt.y) != (p[j].y > pt.y)) &&
(pt.x < (p[j].x - p[i].x) * (pt.y - p[i].y) / (double)(p[j].y - p[i].y) + p[i].x)) {
inside = !inside;
}
}
return inside;
}
// Проверка дали точка лежи на границата на полигона
bool onBoundary(const vector<Point>& p, Point pt) {
int n = p.size();
for (int i = 0; i < n; i++) {
Point a = p[i], b = p[(i + 1) % n];
// Проверка дали pt лежи на отсечката [a, b]
if (cross(pt - a, b - a) == 0 &&
min(a.x, b.x) <= pt.x && pt.x <= max(a.x, b.x) &&
min(a.y, b.y) <= pt.y && pt.y <= max(a.y, b.y)) {
return true;
}
}
return false;
}
// Пълна проверка: вътре, вън или на границата
// Връща: 0 = вън, 1 = на границата, 2 = вътре
int pointInPolygon(const vector<Point>& p, Point pt) {
if (onBoundary(p, pt)) return 1;
return isInside(p, pt) ? 2 : 0;
}
4.3. Winding Number Algorithm
Алтернативен метод, който брои колко пъти полигонът "обвива" точката.
// Winding number - по-робустен за деликатни случаи
int windingNumber(const vector<Point>& p, Point pt) {
int wn = 0;
int n = p.size();
for (int i = 0; i < n; i++) {
Point a = p[i], b = p[(i + 1) % n];
if (a.y <= pt.y) {
if (b.y > pt.y && cross(b - a, pt - a) > 0)
wn++;
} else {
if (b.y <= pt.y && cross(b - a, pt - a) < 0)
wn--;
}
}
return wn; // != 0 означава вътре
}
5. Изпъкналост (Convexity Check)
Полигон е изпъкнал, ако всички завои са в една и съща посока (само наляво или само надясно). Това означава, че всички вътрешни ъгли са по-малки от 180°.
Полигон е вдлъбнат (concave), ако има поне един вътрешен ъгъл по-голям от 180°.
Проверка за Изпъкналост
Използваме векторно произведение (cross product) за всеки три последователни върха. Ако всички имат един и същи знак, полигонът е изпъкнал.
// Проверка дали полигон е изпъкнал
bool isConvex(const vector<Point>& p) {
int n = p.size();
if (n < 3) return false;
bool hasPos = false, hasNeg = false;
for (int i = 0; i < n; i++) {
Point a = p[i];
Point b = p[(i + 1) % n];
Point c = p[(i + 2) % n];
long long cp = cross(b - a, c - b);
if (cp > 0) hasPos = true;
if (cp < 0) hasNeg = true;
}
return !(hasPos && hasNeg); // Изпъкнал, ако всички са с един знак
}
// Определяне на ориентацията (по или обратно на часовниковата стрелка)
// Връща: 1 = counter-clockwise, -1 = clockwise, 0 = дегенериран
int orientation(const vector<Point>& p) {
long long area = signedArea2(p);
if (area > 0) return 1; // CCW
if (area < 0) return -1; // CW
return 0; // Дегенериран
}
// Обръщане на ориентацията на полигон
void reversePolygon(vector<Point>& p) {
reverse(p.begin(), p.end());
}
Детекция на Изпъкли и Вдлъбнати Върхове
// Класификация на всеки връх: изпъкнал (convex) или вдлъбнат (reflex)
// Връща вектор: true = изпъкнал връх, false = вдлъбнат връх
vector<bool> classifyVertices(const vector<Point>& p) {
int n = p.size();
vector<bool> isConvexVertex(n);
for (int i = 0; i < n; i++) {
Point a = p[(i - 1 + n) % n];
Point b = p[i];
Point c = p[(i + 1) % n];
long long cp = cross(b - a, c - b);
// За CCW полигон: cp > 0 => изпъкнал връх
isConvexVertex[i] = (cp >= 0);
}
return isConvexVertex;
}
6. Триангулация на Полигон
Всеки прост полигон може да бъде разделен на триъгълници. Това е полезно за много алгоритми и визуализации.
6.1. Ear Clipping Method
Метод "отрязване на уши" - намира "ухо" (триъгълник, чиито диагонал е вътре в полигона) и го премахва.
// Проверка дали триъгълник (a, b, c) съдържа точка p
bool triangleContains(Point a, Point b, Point c, Point p) {
long long cp1 = cross(b - a, p - a);
long long cp2 = cross(c - b, p - b);
long long cp3 = cross(a - c, p - c);
return (cp1 >= 0 && cp2 >= 0 && cp3 >= 0) ||
(cp1 <= 0 && cp2 <= 0 && cp3 <= 0);
}
// Проверка дали диагоналът от i до k е валиден (не пресича полигона)
bool isValidDiagonal(const vector<Point>& p, int i, int k) {
int n = p.size();
// Проверка дали диагоналът е вътре в полигона
Point mid = Point((p[i].x + p[k].x) / 2, (p[i].y + p[k].y) / 2);
if (!isInside(p, mid)) return false;
// Проверка дали някой връх е вътре в триъгълника
for (int j = 0; j < n; j++) {
if (j == i || j == k || j == (i + 1) % n) continue;
if (triangleContains(p[i], p[(i + 1) % n], p[k], p[j]))
return false;
}
return true;
}
// Ear Clipping триангулация (O(n^2) за прости полигони)
vector<array<int, 3>> triangulate(vector<Point> p) {
vector<array<int, 3>> triangles;
int n = p.size();
vector<int> indices(n);
for (int i = 0; i < n; i++) indices[i] = i;
while (indices.size() > 3) {
bool found = false;
for (int i = 0; i < indices.size(); i++) {
int prev = (i - 1 + indices.size()) % indices.size();
int next = (i + 1) % indices.size();
Point a = p[indices[prev]];
Point b = p[indices[i]];
Point c = p[indices[next]];
// Проверка дали това е "ухо"
if (cross(b - a, c - b) > 0) {
bool isEar = true;
for (int j = 0; j < indices.size(); j++) {
if (j == prev || j == i || j == next) continue;
if (triangleContains(a, b, c, p[indices[j]])) {
isEar = false;
break;
}
}
if (isEar) {
triangles.push_back({indices[prev], indices[i], indices[next]});
indices.erase(indices.begin() + i);
found = true;
break;
}
}
}
if (!found) break; // Не може да се триангулира
}
if (indices.size() == 3) {
triangles.push_back({indices[0], indices[1], indices[2]});
}
return triangles;
}
6.2. Монотонна Триангулация (за напреднали)
За изпъкнали полигони триангулацията е тривиална - всички диагонали от един връх.
// Триангулация на изпъкнал полигон от връх 0
vector<array<int, 3>> triangulateConvex(int n) {
vector<array<int, 3>> triangles;
for (int i = 1; i < n - 1; i++) {
triangles.push_back({0, i, i + 1});
}
return triangles;
}
7. Разлагане на Полигон (Decomposition)
7.1. Разлагане на Изпъкнали Части
Вдлъбнат полигон може да се раздели на изпъкнали части.
// Намиране на всички вдлъбнати върхове (reflex vertices)
vector<int> findReflexVertices(const vector<Point>& p) {
vector<int> reflex;
int n = p.size();
for (int i = 0; i < n; i++) {
Point a = p[(i - 1 + n) % n];
Point b = p[i];
Point c = p[(i + 1) % n];
if (cross(b - a, c - b) < 0) { // За CCW полигон
reflex.push_back(i);
}
}
return reflex;
}
8. Практически Приложения и Специални Случаи
8.1. Център на Тежест (Centroid)
// Център на тежест на полигон
Point centroid(const vector<Point>& p) {
long long cx = 0, cy = 0, area = 0;
int n = p.size();
for (int i = 0; i < n; i++) {
long long cp = cross(p[i], p[(i + 1) % n]);
cx += (p[i].x + p[(i + 1) % n].x) * cp;
cy += (p[i].y + p[(i + 1) % n].y) * cp;
area += cp;
}
// Делим на 3 * площта (за да избегнем деление по време на натрупване)
return Point(cx / (3 * area), cy / (3 * area));
}
8.2. Най-далечна Двойка Точки (Rotating Calipers)
За изпъкнал полигон можем да намерим най-далечната двойка точки за O(n).
8.3. Минковски Сума
Сумата на два полигона \(P + Q = \{p + q : p \in P, q \in Q\}\).
8.4. Пресичане на Полигони
Проверка дали два полигона се пресичат или един съдържа друг.
// Проверка дали два изпъкнали полигона се пресичат
bool convexPolygonsIntersect(const vector<Point>& p1, const vector<Point>& p2) {
// Проверка дали някой връх на p1 е в p2
for (const Point& pt : p1) {
if (inConvexPolygon(p2, pt)) return true;
}
// Проверка дали някой връх на p2 е в p1
for (const Point& pt : p2) {
if (inConvexPolygon(p1, pt)) return true;
}
// Проверка дали някои страни се пресичат
for (int i = 0; i < p1.size(); i++) {
for (int j = 0; j < p2.size(); j++) {
// Проверка за пресичане на отсечки [p1[i], p1[i+1]] и [p2[j], p2[j+1]]
// (изисква функция за пресичане на отсечки)
}
}
return false;
}
9. Специални Случаи и Грешки
Често Срещани Грешки:
- Деление с нула при проверка за пресичане
- Overflow - винаги използвайте
long longза координати - Грешки при закръгляне - работете с удвоено лице (2*S) докато е възможно
- Дегенерирани случаи - колинеарни точки, полигон с площ 0
- Гранични случаи при Point-in-Polygon - точка на ръб или връх
Проверки за Валидност:
// Проверка дали полигон е валиден (не се самопересича)
bool isSimplePolygon(const vector<Point>& p) {
int n = p.size();
// Проверка за самопресичане на страни
for (int i = 0; i < n; i++) {
for (int j = i + 2; j < n; j++) {
if (j == (i + n - 1) % n) continue; // Съседни страни
// Проверка дали отсечки [i, i+1] и [j, j+1] се пресичат
// (изисква функция за пресичане на отсечки)
}
}
return true;
}
// Проверка за дегенериран полигон (с площ 0)
bool isDegenerate(const vector<Point>& p) {
return polygonArea2(p) == 0;
}
10. Задачи
- Codeforces 166B: Polygon (Point inside convex polygon).
- POJ 1265: Area (Pick's theorem).
- SPOJ GSSQ: General queries.
11. Сложност и Оптимизации
Времева Сложност:
- Лице на полигон: O(n)
- Периметър: O(n)
- Point in Polygon (Ray Casting): O(n)
- Point in Polygon (изпъкнал, двоично търсене): O(log n)
- Проверка за изпъкналост: O(n)
- Триангулация (Ear Clipping): O(n²)
- Триангулация (оптимална): O(n log n)
Съвети за Оптимизация:
- Използвайте
long longза координати, за да избегнете overflow - Работете с удвоено лице (2*S) докато е възможно
- Кеширайте резултати като лице и периметър, ако се използват многократно
- За изпъкнали полигони използвайте специализирани алгоритми
- Избягвайте операции с плаваща запетая, когато е възможно
🏁 Заключение
Геометрията на полигони е фундаментална тема в състезателното програмиране. Основните концепции включват:
✅ Винаги работете с \(2 \cdot S\) (удвоено лице), за да останете в целите числа (long long) до последния момент.
✅ Внимавайте за overflow - използвайте long long за всички междинни изчисления.
✅ Тествайте гранични случаи - дегенерирани полигони, колинеарни точки, точки на границата.
✅ Използвайте векторно произведение - то е основен инструмент за повечето операции с полигони.
✅ Разбирайте разликата между изпъкнали и вдлъбнати полигони - различни алгоритми имат различна ефективност.
Практикувайте с реални задачи, за да затвърдите познанията си!