🧹 Метод на Замитащата Права (Sweep Line)
Методът на замитащата права е една от най-мощните техники в изчислителната геометрия. Основната идея е да преместим вертикална (или хоризонтална) права през равнината и да обработваме геометричните обекти в момента, в който правата ги "докосне".
1. Концепция
Замитащата права превръща 2D статичен проблем в 1D динамичен проблем.
- Події (Events): Точките, в които се случва нещо интересно (начало/край на отсечка, позиция на точка). Сортираме тези събития по \(x\)-координата.
- Статус (Status): Структура от данни (обикновено
std::setили Segment Tree), която пази информация за обектите, които в момента се пресичат от правата.
Общ шаблон
struct Event {
int x;
int type; // Тип на събитието
// Допълнителна информация
bool operator<(const Event& other) const {
if (x != other.x) return x < other.x;
return type < other.type;
}
};
void sweepLine(vector<Event>& events) {
sort(events.begin(), events.end());
set<int> status; // Или друга подходяща структура
for (const Event& e : events) {
// Обработка според типа на събитието
if (e.type == START) {
status.insert(/* ... */);
} else if (e.type == END) {
status.erase(/* ... */);
}
// Изчисления базирани на текущия статус
}
}
2. Най-близка двойка точки (Closest Pair of Points)
Дадени са \(N\) точки. Търсим двете с минимално разстояние помежду си. Сложност: \(O(N \log N)\).
Алгоритъм с Divide and Conquer + Sweep
struct Point {
long long x, y;
double dist(const Point& other) const {
long long dx = x - other.x;
long long dy = y - other.y;
return sqrt(dx * dx + dy * dy);
}
};
double closestPair(vector<Point>& pts) {
int n = pts.size();
if (n < 2) return 1e18;
// Сортиране по x
sort(pts.begin(), pts.end(), [](const Point& a, const Point& b) {
return a.x < b.x;
});
set<pair<long long, long long>> s; // {y, x}
double minDist = 1e18;
int j = 0;
for (int i = 0; i < n; i++) {
// Премахваме точки извън прозореца
while (j < i && pts[i].x - pts[j].x >= minDist) {
s.erase({pts[j].y, pts[j].x});
j++;
}
// Търсим в диапазона [y - minDist, y + minDist]
auto it1 = s.lower_bound({pts[i].y - (long long)minDist - 1, LLONG_MIN});
auto it2 = s.upper_bound({pts[i].y + (long long)minDist + 1, LLONG_MAX});
for (auto it = it1; it != it2; it++) {
Point p2 = {it->second, it->first};
minDist = min(minDist, pts[i].dist(p2));
}
s.insert({pts[i].y, pts[i].x});
}
return minDist;
}
3. Площ на обединение на правоъгълници
Имаме \(N\) правоъгълника. Търсим площта, покрита от поне един от тях.
Алгоритъм със Segment Tree
struct Rectangle {
int x1, y1, x2, y2;
};
struct Event {
int x, y1, y2, type; // type: +1 за лява, -1 за дясна страна
bool operator<(const Event& other) const {
return x < other.x;
}
};
class SegmentTree {
vector<int> cnt; // Колко пъти е покрит
vector<long long> len; // Обща дължина на покритите части
vector<int> coord; // Компресирани координати
void update(int v, int l, int r, int ql, int qr, int val) {
if (qr <= l || r <= ql) return;
if (ql <= l && r <= qr) {
cnt[v] += val;
} else {
int mid = (l + r) / 2;
update(2*v, l, mid, ql, qr, val);
update(2*v+1, mid, r, ql, qr, val);
}
if (cnt[v] > 0) {
len[v] = coord[r] - coord[l];
} else if (r - l == 1) {
len[v] = 0;
} else {
len[v] = len[2*v] + len[2*v+1];
}
}
public:
SegmentTree(vector<int>& y_coords) : coord(y_coords) {
int n = y_coords.size();
cnt.resize(4 * n);
len.resize(4 * n);
}
void update(int y1, int y2, int val) {
int n = coord.size();
int pos1 = lower_bound(coord.begin(), coord.end(), y1) - coord.begin();
int pos2 = lower_bound(coord.begin(), coord.end(), y2) - coord.begin();
update(1, 0, n - 1, pos1, pos2, val);
}
long long getLength() {
return len[1];
}
};
long long rectangleUnionArea(vector<Rectangle>& rects) {
vector<Event> events;
set<int> y_set;
for (auto& r : rects) {
events.push_back({r.x1, r.y1, r.y2, 1});
events.push_back({r.x2, r.y1, r.y2, -1});
y_set.insert(r.y1);
y_set.insert(r.y2);
}
sort(events.begin(), events.end());
vector<int> y_coords(y_set.begin(), y_set.end());
SegmentTree tree(y_coords);
long long totalArea = 0;
for (int i = 0; i < events.size(); i++) {
if (i > 0) {
totalArea += tree.getLength() * (events[i].x - events[i-1].x);
}
tree.update(events[i].y1, events[i].y2, events[i].type);
}
return totalArea;
}
4. Изпъкнала Обвивка (Convex Hull) - Andrew's Algorithm
struct Point {
long long x, y;
Point operator-(const Point& other) const {
return {x - other.x, y - other.y};
}
long long cross(const Point& other) const {
return x * other.y - y * other.x;
}
};
// Проверка за ляв завой (counter-clockwise)
long long ccw(const Point& a, const Point& b, const Point& c) {
Point ab = b - a;
Point ac = c - a;
return ab.cross(ac);
}
vector<Point> convexHull(vector<Point> points) {
int n = points.size();
if (n <= 1) return points;
sort(points.begin(), points.end(), [](const Point& a, const Point& b) {
return a.x < b.x || (a.x == b.x && a.y < b.y);
});
vector<Point> hull;
// Долна верига
for (int i = 0; i < n; i++) {
while (hull.size() >= 2 &&
ccw(hull[hull.size()-2], hull[hull.size()-1], points[i]) <= 0) {
hull.pop_back();
}
hull.push_back(points[i]);
}
// Горна верига
int lowerSize = hull.size();
for (int i = n - 2; i >= 0; i--) {
while (hull.size() > lowerSize &&
ccw(hull[hull.size()-2], hull[hull.size()-1], points[i]) <= 0) {
hull.pop_back();
}
hull.push_back(points[i]);
}
hull.pop_back(); // Премахваме дублираната начална точка
return hull;
}
5. Пресичане на отсечки (Bentley-Ottmann)
Намира всички \(K\) пресечни точки на \(N\) отсечки.
struct Segment {
Point a, b;
int id;
double yAtX(double x) const {
if (a.x == b.x) return a.y;
return a.y + (b.y - a.y) * (x - a.x) / (b.x - a.x);
}
};
struct SegmentComparator {
double sweepX;
bool operator()(const Segment& s1, const Segment& s2) const {
double y1 = s1.yAtX(sweepX);
double y2 = s2.yAtX(sweepX);
if (abs(y1 - y2) > 1e-9) return y1 < y2;
return s1.id < s2.id;
}
};
bool segmentsIntersect(const Segment& s1, const Segment& s2, Point& intersection) {
long long d1 = ccw(s1.a, s1.b, s2.a);
long long d2 = ccw(s1.a, s1.b, s2.b);
long long d3 = ccw(s2.a, s2.b, s1.a);
long long d4 = ccw(s2.a, s2.b, s1.b);
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
// Изчисляване на точката на пресичане
return true;
}
return false;
}
6. Брой точки под права
Използва sweep line за броене на точки под дадена права:
long long pointsBelowLine(vector<Point>& points, Point lineStart, Point lineEnd) {
sort(points.begin(), points.end(), [](const Point& a, const Point& b) {
return a.x < b.x || (a.x == b.x && a.y < b.y);
});
long long count = 0;
for (const Point& p : points) {
// Проверяваме дали точката е под правата
if (ccw(lineStart, lineEnd, p) < 0) {
count++;
}
}
return count;
}
7. Вътрешни точки в многоъгълник
bool pointInPolygon(const Point& p, const vector<Point>& polygon) {
int n = polygon.size();
int crossings = 0;
for (int i = 0; i < n; i++) {
Point a = polygon[i];
Point b = polygon[(i + 1) % n];
if ((a.y <= p.y && p.y < b.y) || (b.y <= p.y && p.y < a.y)) {
double x = a.x + (p.y - a.y) * (b.x - a.x) / (b.y - a.y);
if (p.x < x) {
crossings++;
}
}
}
return crossings % 2 == 1;
}
8. Максимален периметър
Намиране на триъгълник с максимален периметър от дадени точки:
double maxTrianglePerimeter(vector<Point>& points) {
int n = points.size();
double maxPerimeter = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
for (int k = j + 1; k < n; k++) {
double p = points[i].dist(points[j]) +
points[j].dist(points[k]) +
points[k].dist(points[i]);
maxPerimeter = max(maxPerimeter, p);
}
}
}
return maxPerimeter;
}
9. Практически съвети
- Координати: Използвайте
long longза точни изчисления,doubleсамо когато е необходимо - Епсилон: При сравнение на
doubleизползвайте толеранс:abs(a - b) < 1e-9 - Дегенерирани случаи: Внимавайте с колинеарни точки, вертикални линии
- Сортиране: Винаги сортирайте събитията правилно (по x, после по тип)
- Статус: Изберете правилната структура (set, multiset, segment tree)
10. Задачи за упражнение
- SPOJ CLOPPAIR: Closest Pair of Points
- USACO Window Area: Union of Rectangles
- Codeforces 1027D: Cycle Detection
- CSES Convex Hull: Basic Convex Hull
- POJ 1113: Wall (Convex Hull + Circle)
- UVA 10256: The Great Divide (Convex Hull Separation)
🏁 Заключение
Sweep Line е мощна техника, която превръща сложни 2D проблеми в по-прости 1D задачи. Ключът е правилният избор на събития и структура за статус. С практика ще развиете интуиция за това кога и как да приложите тази техника. Винаги мислете какво се променя когато правата премине през дадена точка и как можете ефективно да актуализирате статуса! 🚀