Nguời viết:
Reviewer:
Trong bài viết này, chúng ta sẽ đi tìm hiểu về thuật toán đường quét - một thuật toán khá hữu ích trong hình học tính toán. Thuật toán này được xây dựa trên một ý tưởng mạnh mà đơn giản: sử dụng một đường thẳng dọc và "quét" qua mặt phẳng.
Tuy vậy, trên thực tế, việc "quét" toàn bộ các điểm trên mặt phẳng là bất khả thi nên chúng ta chỉ có thể xét một vài điểm cần thiết mà thôi.
Các khái niệm sau được đề cập xuyên suốt bài viết:
Với hai điểm và :
Các đường màu đỏ, xanh lam, vàng biểu diễn khoảng cách Manhattan có cùng độ dài , trong khi đường màu xanh lục biểu diễn khoảng cách Euclid với độ dài .
Như đã đề cập ở phía trên, khi ứng dụng thuật toán đường quét, việc quét qua tất cả các điểm trên mặt phẳng là bất khả thi, và chúng ta cần sử dụng một số kĩ thuật hay cấu trúc dữ liệu khác - chẳng hạn, kĩ thuật hai con trỏ, kĩ thuật nén số, cây phân đoạn, cây Fenwick (cây chỉ số nhị phân) - để lọc ra và xử lí các điểm thiết yếu cần quan tâm. Do đó, độc giả nên nắm kĩ các chủ đề liên quan nêu trên trước khi đọc bài viết.
Ngoài ra, bài viết gốc có đề cập tới bài toán bao lồi, nhưng chủ đề này sẽ không được đề cập trong bài viết này này bởi VNOI Wiki đã có một bài viết khá hoàn thiện ở đây.
Link bài: SPOJ - CLOPPAIR
Cho một danh sách điểm. Tìm khoảng cách Euclid ngắn nhất tạo bởi hai trong số các điểm đó.
Giới hạn:
Ta có thể dễ dàng nhận thấy bài này có thể giải quyết với độ phức tạp , nhưng sẽ không thể qua được giới hạn thời gian 1 giây. Tuy vậy, áp dụng thuật toán đường quét, chúng ta có thể giảm độ phức tạp xuống .
Trước tiên, chúng ta sẽ sắp xếp lại danh sách điểm theo thứ tự hoành độ các điểm tăng dần. Lần lượt duyệt qua từng điểm trong danh sách đã sắp xếp. Ý tưởng chính của thuật toán cải tiến so với thuật toán trâu bò đó là thay vì phải duyệt qua tất cả cặp điểm, với mỗi điểm, ta chỉ phải xét điểm đó với một số ít các điểm khác đáng quan tâm, chi phí để tìm các điểm đáng quan tâm là , do đó thuật toán cải tiến có độ phức tạp .
Giả sử chúng ta đã xử lí xong điểm đầu tiên và khoảng cách ngắn nhất hiện có là . Như vậy từ điểm thứ về sau, ta chỉ quan tâm đến các cặp điểm có khoảng cách bé hơn . Gọi điểm thứ (cũng là điểm đang xét) là điểm .
Bổ đề 1
Tại mỗi bước trong thuật toán, để tìm một cặp điểm có khoảng cách bé hơn , ta chỉ cần quan tâm đến tối đa điểm khác.
Chứng minh
Từ , vẽ hình vuông xung quanh, mỗi hình vuông có cạnh đúng bằng , như hình dưới (điểm màu xanh là ).
Hiển nhiên tất cả các điểm từ đến đều nằm về phía bên trái của điểm đang xét do ta duyệt danh sách theo thứ tự tăng dần về hoành độ. Nhận xét rằng từ , ta không cần quan tâm đến những điểm không nằm trong hình vuông bên trên do khi đó khoảng cách giữa và lớn hơn .
Xét hình vuông ta vừa vẽ. Do là khoảng cách ngắn nhất giữa hai cặp điểm bất kì trong điểm đầu tiên nên trong mỗi hình vuông, có nhiều nhất một điểm. Do đó, tối đa ta chỉ cần xét điểm trong hình vuông để cập nhật kết quả.
Từ bổ đề 1, ta nhận thấy cần duy trì một danh sách các điểm có hoành độ chênh lệch không quá so với điểm đang xét. Do giảm dần nên ta có thể thực hiện việc này bằng kĩ thuật hai con trỏ. Đồng thời, tại mỗi bước ta cần tìm các điểm có tung độ lân cận với điểm đang xét. Do đó các điểm trong cần được sắp xếp theo thứ tự tung độ tăng dần. Các tiêu chí trên có thể thoả mãn bằng cách cài đặt một cây nhị phân tìm kiếm như std::set
.
Thuật toán của chúng ta cụ thể như sau. Đầu tiên sắp xếp danh sách điểm theo thứ tự hoành độ tăng dần. Lần lượt duyệt qua các điểm trong danh sách, đồng thời duy trì một cây nhị phân tìm kiếm . Gọi là khoảng cách nhỏ nhất giữa hai điểm bất kì đã xét. Tại mỗi bước:
Ta nhận thấy mỗi điểm được thêm vào và xoá khỏi đúng một lần. Do đó tổng chi phí cho các thao tác thêm và xoá điểm là . Tại mỗi bước, chi phí tìm kiếm là và có điểm ta cần xét. Tóm lại, độ phức tạp thời gian của thuật toán là .
Trong phần cài đặt này, các khoảng cách được lưu dưới dạng bình phương để tránh bị sai số.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
struct Point{
ll x, y;
int id;
bool operator < (const Point& other) {
if (x != other.x) return x < other.x;
return y < other.y;
}
};
struct cmp{
bool operator () (const Point& a, const Point& b) const {
if (a.y != b.y) return a.y < b.y;
return a.x < b.x;
}
};
int n;
vector<Point> points; // Vector chứa tất cả các điểm
set<Point, cmp> T;
ll squared_dist(Point a, Point b) { // Nhận vào hai điểm, trả vể
// bình phương khoảng cách giữa hai điểm
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
}
signed main() {
ios_base::sync_with_stdio(false); cin.tie(NULL);
cin >> n;
for (int i = 0; i < n; i++) {
ll x, y;
cin >> x >> y;
points.push_back({x, y, i});
}
ll squared_d = squared_dist(points[0], points[1]); // Lưu bình phương của d
int res_id1 = 0, res_id2 = 1;
sort(points.begin(), points.end()); // Sắp xếp các điểm theo hoành độ
for (auto p : points) {
ll x = p.x, y = p.y;
int id = p.id;
ll d = sqrt(squared_d);
Point cur = {-1000001, y - d, id};
while (1) { // Tìm tất cả các điểm có tung độ trong khoảng [y - d, y + d]
auto it = T.upper_bound(cur);
if (it == T.end()) break;
cur = *it;
if (cur.y > y + d) break; // Dừng lại nếu điểm có tung độ lớn hơn y + d
if (cur.x < x - d) {
T.erase(it);
continue;
} // Xóa điểm nếu điểm này có hoành độ bé hơn x - d
if (squared_dist(p, cur) < squared_d) {
squared_d = squared_dist(p, cur);
res_id1 = id; res_id2 = cur.id;
} // Gán đáp án mới nếu tìm được d nhỏ hơn
}
T.insert(p); // Thêm điểm hiện tại vào T
}
if (res_id1 > res_id2) swap(res_id1, res_id2);
cout << res_id1 << " " << res_id2 << " ";
cout << fixed << setprecision(6) << sqrt(squared_d);
}
Link bài: SPOJ - CS345A1
Cho các đoạn thẳng song song trục hoặc trục , yêu cầu trả lại số các giao điểm.
Giới hạn:
Ta bắt đầu với ý tưởng quét qua tất cả đoạn thẳng tương tự như bài toán tìm cặp điểm gần nhất.
Tuy nhiên, ta sẽ gặp phải vấn đề là một đoạn thẳng nằm ngang sẽ bao gồm các điểm có hoành độ khác nhau nên chúng ta khó có thể sắp xếp các đoạn thẳng theo một thứ tự nào đó liên quan đến hoành độ - giả sử có một đoạn thẳng nối và và một đoạn thẳng nối và , chúng ta nên ưu tiên đoạn thẳng nào trước?
Để giải quyết vấn đề trên, một ý tưởng thường được dùng là thay vì xem một đoạn thẳng nằm ngang là một phần tử duy nhất trong danh sách, ta sẽ tách nó ra thành hai phần tử riêng biệt - một phần tử biểu thị cho đầu mút bên trái và một phần tử biểu thị cho đầu mút bên phải của đoạn thẳng. Như vậy trong danh sách của chúng ta sẽ có ba loại phần tử:
Khi này ta sẽ có thể sắp xếp danh sách của chúng ta theo thứ tự tăng dần về hoành độ của các phần tử.
Chúng ta sẽ quét từ trái sang phải. Khi đoạn quét di chuyển, ta duy trì một tập hợp các đoạn thẳng nằm ngang bị cắt ngang theo đoạn quét - tức là đoạn quét đang ở giữa hai đầu mút của đoạn ngang đó. Tập này được sắp xếp theo thứ tự , và được tô màu đỏ trong hình dưới.
Với mỗi đoạn thẳng nằm dọc , để đếm là số đoạn thẳng nằm ngang cắt , ta chỉ việc tìm trong tập những đoạn thẳng có tung độ nằm giữa hai đầu mút của . Hiển nhiên ta thấy rằng tổng của các cũng là số giao điểm ta cần tìm.
Như vậy ta thấy tập là một yếu tố quan trọng để giải quyết bài toán. Tập cần hỗ trợ các thao tác cụ thể sau:
Để thoả mãn các yêu cầu trên, ta sẽ cài đặt tập bằng kĩ thuật nén số và cây Fenwick. Ta sẽ nén tung độ của các đoạn thẳng thành điểm. Dựng cây Fenwick dựa trên điểm này, mỗi nút trong cây cho biết có bao nhiêu đoạn thẳng ngang đang cắt các điểm trong đoạn con mà nút quản lý. Như vậy, mỗi thao tác thêm một đoạn thẳng vào tập tương ứng với một thao tác cộng vào giá trị lưu ở các nút tương ứng và mỗi thao tác xoá một đoạn thẳng khỏi tương ứng với một thao tác giảm giá trị lưu ở các nút tương ứng đi . Chi phí để thực hiện cả hai thao tác trên là . Để tìm số lượng đoạn thẳng ngang có tung độ trong khoảng , ta có thể tính tổng các điểm trong khoảng trong cây Fenwick trong . Chi tiết tham khảo ở cài đặt mẫu.
Dễ thấy thuật toán của chúng ta duyệt qua một danh sách có phần tử, với mỗi phần tử chi phí xử lí là . Do đó độ phức tạp thời gian của thuật toán là . Độ phức tạp bộ nhớ là .
Minh họa thuật toán:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5 + 5;
const double EPS = 1e-9;
struct Event{
double x;
int y, y2, type;
bool operator < (const Event& other) const {
return x < other.x;
}
};
struct FenwickTree{
int n;
vector<int> s;
FenwickTree(int n) : n(n), s(n + 5) {
for (int i = 1; i <= n; i++) s[i] = 0;
}
void update(int i, int val) {
for (; i <= n; i += i & -i) s[i] += val;
}
int getsum(int i) {
int res = 0;
for (; i; i -= i & -i) res += s[i];
return res;
}
int query(int l, int r) {
return getsum(r) - getsum(l - 1);
}
};
int n;
double blue_x1[N], blue_x2[N], blue_y[N], red_y1[N], red_y2[N], red_x[N];
vector<double> compress_y;
vector<Event> events;
signed main() {
ios_base::sync_with_stdio(false); cin.tie(NULL);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> blue_x1[i] >> blue_x2[i] >> blue_y[i];
if (blue_x1[i] > blue_x2[i]) swap(blue_x1[i], blue_x2[i]);
compress_y.push_back(blue_y[i]);
}
for (int i = 1; i <= n; i++) {
cin >> red_y1[i] >> red_y2[i] >> red_x[i];
if (red_y1[i] > red_y2[i]) swap(red_y1[i], red_y2[i]);
compress_y.push_back(red_y1[i]);
compress_y.push_back(red_y2[i]);
}
sort(compress_y.begin(), compress_y.end());
compress_y.erase(unique(compress_y.begin(), compress_y.end()), compress_y.end());
// Rời rạc hóa các tung độ
for (int i = 1; i <= n; i++) {
int yy = lower_bound(compress_y.begin(), compress_y.end(), blue_y[i]) - compress_y.begin() + 1;
events.push_back({blue_x1[i] - EPS, yy, 0, 1}); // Phần tử loại 1
events.push_back({blue_x2[i] + EPS, yy, 0, 2}); // Phần tử loại 2
}
for (int i = 1; i <= n; i++) {
int l = lower_bound(compress_y.begin(), compress_y.end(), red_y1[i]) - compress_y.begin() + 1;
int r = lower_bound(compress_y.begin(), compress_y.end(), red_y2[i]) - compress_y.begin() + 1;
events.push_back({red_x[i], l, r, 3}); // Phần tử loại 3
}
sort(events.begin(), events.end());
FenwickTree FT(n * 3); // Có tối đa n * 3 tung độ khác nhau
ll res = 0;
for (auto e : events) {
if (e.type == 1) FT.update(e.y, 1);
else if (e.type == 2) FT.update(e.y, -1);
else res += FT.query(e.y, e.y2);
}
cout << res;
}
Link bài: VNOJ - AREA
Trên mặt phẳng toạ độ, ta vẽ hình chữ nhật có các cạnh song song với trục toạ độ. Tính tổng diện tích che phủ bởi hình chữ nhật này.
Giới hạn:
Tương tự như bài toán tìm giao điểm của các đoạn thẳng, chúng ta có thể xử lí bằng cách biểu diễn mỗi hình chữ nhật thành hai "sự kiện" - một biểu thị cho cạnh bên trái và một biểu thị cho cạnh bên phải của hình chữ nhật - và duy trì một tập chứa các hình chữ nhật mà đoạn thẳng quét của chúng ta đang cắt qua. Khi chúng ta quét qua cạnh bên trái, ta thêm hình chữ nhật đó vào , khi quét qua cạnh bên phải thì ta xoá hình chữ nhật tương ứng khỏi .
Ta biết được những hình chữ nhật nào đang bị cắt bởi đường quét của chúng ta (màu đỏ). Để tìm tổng diện tích được bao phủ, ta sẽ tìm diện tích từng phần bị bao phủ giữa mỗi cặp hai "sự kiện" liền nhau và tính tổng của chúng. Để tìm phần diện tích được bao phủ giữa hai sự kiện liền nhau, ta cần biết tổng độ dài phần đường quét đi qua chúng (nét liền màu xanh trong hình trên). Nhân độ dài này với khoảng cách giữa hai sự kiện liền nhau, ta được diện tích của phần hình chữ nhật giữa hai "sự kiện" đó.
Vấn đề đặt ra là làm thế nào để tìm tổng độ dài của các phần "nét liền màu xanh" như trên hình. Nhớ rằng tại mỗi bước ta duy trì một tập các hình chữ nhật mà đường thẳng quét cắt qua. Hiển nhiên ta thấy tổng độ dài của phần "nét liền màu xanh" chính là hợp của tất cả các hình chữ nhật trong tập tại mỗi bước.
Để tính hợp của tất cả các hình chữ nhật trong tập , một thuật toán đơn giản là ta sẽ duyệt qua hết tất cả các hình chữ nhật hiện có trong . Độ phức tạp thời gian để giải bài toán khi này là - chúng ta duyệt qua sự kiện, tại mỗi "sự kiện", ta lại duyệt qua hình chữ nhật mà đường quét của chúng ta đang cắt (dĩ nhiên chúng ta cũng phải thêm các hình mới và xoá bớt những hình mà đường quét không còn cắt khỏi tập ).
Minh họa thuật toán:
Để tối ưu thuật toán, chúng ta có thể sử dụng kĩ thuật nén số và cây phân đoạn. Ta sẽ nén hoành độ của các "sự kiện" thành điểm, các điểm này chia đường thẳng quét của chúng ta thành đoạn thẳng con. Ta dựng cây phân đoạn với đoạn thẳng con này là các nút lá. Tại mỗi nút trong cây phân đoạn ta sẽ lưu hai giá trị để trả lời cho hai câu hỏi:
Chi tiết việc cập nhật hai giá trị tại mỗi bước độc giả có thể tham khảo trong cài đặt mẫu.
#include <bits/stdc++.h>
using namespace std;
const int MX = 30000;
struct Segment{
int x, y1, y2, type;
bool operator < (const Segment& other) const {
return x < other.x;
}
};
struct SegmentTree{
vector<pair<int, int>> s;
SegmentTree(int n) : s(n * 4 + 5) {
for (int i = 1; i <= 4 * n; i++) s[i] = {0, 0};
}
void update(int id, int l, int r, int tl, int tr, int val) {
if (l > tr || r < tl) return;
if (l >= tl && r <= tr) {
s[id].second += val;
if (s[id].second != 0) s[id].first = r - l + 1;
else if (l != r) s[id].first = s[id * 2].first + s[id * 2 + 1].first;
else s[id].first = 0;
return;
}
int m = (l + r) >> 1;
update(id * 2, l, m, tl, tr, val);
update(id * 2 + 1, m + 1, r, tl, tr, val);
if (s[id].second != 0) s[id].first = r - l + 1;
else s[id].first = s[id * 2].first + s[id * 2 + 1].first;
}
};
int n;
vector<Segment> segments;
signed main() {
ios_base::sync_with_stdio(false); cin.tie(NULL);
cin >> n;
for (int i = 1; i <= n; i++) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
segments.push_back({x1, y1, y2, 1});
segments.push_back({x2, y1, y2, -1});
}
sort(segments.begin(), segments.end());
SegmentTree ST(MX);
int res = 0;
for (int i = 0; i < (int)segments.size() - 1; i++) {
ST.update(1, 0, MX, segments[i].y1, segments[i].y2 - 1, segments[i].type);
res += (segments[i + 1].x - segments[i].x) * ST.s[1].first;
}
cout << res;
}
Link bài: Kattis - GRIDMST
Cho điểm (có thể trùng nhau). Trọng số giữa mỗi đỉnh là khoảng cách Manhattan giữa chúng. Tìm trọng số của cây khung nhỏ nhất qua điểm đó.
Giới hạn:
Ý tưởng chính của thuật giải là nếu như số cạnh ta cần xét là thì ta có thể sử dụng các thuật toán tìm cây khung nhỏ nhất như Kruskal hay Prim để giải bài toán trong .
Bổ đề 2
Xét một điểm cho trước bất kì. Lấy gốc toạ độ tại . Chia mặt phẳng toạ độ thành phần bằng nhau như hình dưới. Với mỗi phần tám, nối với một điểm bất kì trong phần tám đó có khoảng cách Manhattan gần nhất với (nếu có). Chẳng hạn trong ví dụ ở hình dưới, ta sẽ nối với .
Thực hiện thao tác trên với tất cả các điểm được cho, ta thu được một đồ thị có cạnh. Ta sẽ chứng minh rằng cây khung nhỏ nhất trên đồ thị là một đáp án cho bài toán.
Chứng minh
Ta sẽ chứng minh rằng các cạnh trong cây khung nhỏ nhất của tập điểm ban đầu cũng thuộc đồ thị .
Xét cạnh . Không mất tính tổng quát, giả sử thuộc phần tám thứ nhất so với . Giả sử tồn tại một điểm trong tập điểm ban đầu sao cho . Đồng thời ta biết rằng (nhìn hình minh hoạ bên dưới). Do đó ta sẽ có một cây khung nhỏ hơn nếu ta bỏ và thay bằng một trong hai cạnh hay . Điều này trái giả thiết là cây khung nhỏ nhất. Do đó, không tồn tại điểm sao cho - tức là điểm trong phần tám thứ nhất của có khoảng cách Manhattan gần nhất. Chứng minh tương tự với các trường hợp thuộc các phần tám còn lại của .
thuộc vùng màu xanh,
Qua bổ đề 2, ta thấy bài toán đặt ra hiện tại là làm sao để dựng được đồ thị hiệu quả. Dễ thấy để dựng đồ thị , với mỗi điểm cho trước, ta cần tìm điểm có khoảng cách Manhattan gần nhất cho mỗi phần tám. Dưới đây bài viết sẽ mô tả thuật toán tìm điểm có khoảng cách Manhattan gần nhất trong phần tám thứ cho điểm. Như vậy có thể tìm điểm khoảng cách Manhattan gần cho phần tám còn lại bằng cách tương tự.
Bổ để 3
Gọi là khoảng cách Manhattan giữa hai điểm và . Gọi , , , là bốn điểm trên mặt phẳng sao cho và . Ta có tương đương với .
Chứng minh
Ta sẽ sử dụng phương pháp chia để trị để giải quyết bài toán. Đầu tiên ta sẽ sắp xếp điểm theo thứ tự tăng dần về hoành độ. Tại mỗi bước ta chia điểm thành tập con và . Gọi đệ quy giải bài toán với từng tập con. Nhận xét rằng lời giải cho tập cũng chính là lời giải đúng, do đó ta chỉ cần cập nhật lời giải cho các điểm trong tập .
Sắp xếp các điểm trong và theo chiều giảm dần của tung độ. Ta sẽ duy trì con trỏ: con trỏ 1 duyệt các điểm trong theo chiều giảm dần về tung độ, con trỏ 2 và 3 duyệt các điểm trong cũng theo chiều giảm dần về tung độ.
Dùng con trỏ 1 để duyệt các điểm trong . Gọi điểm đang được con trỏ 1 trỏ đến là . Dùng con trỏ 2 để duyệt các điểm trong sao cho các điểm đi qua luôn có tung độ không bé hơn . Con trỏ 3 trỏ đến điểm có khoảng cách Manhattan nhỏ nhất với hiện tại (gọi điểm con trỏ 3 đang trỏ đến là ). Tại mỗi bước trong quá trình lặp, có khả năng:
Nhờ có tính chất được đề cập trong bổ đề 3, ta nhận thấy vòng lặp trên sẽ cho chúng ta đáp án chính xác. Cả ba con trỏ đều "thăm" mỗi điểm trong hoặc đúng một lần nên độ phức tập của mỗi "tầng" trong cây đệ quy của chúng ta sẽ là . Do ta sẽ có , độ phức tạp của thuật toán trên là .
Cảm ơn bạn Trần Xuân Bách (HUS High School for Gifted Students) đã đóng góp vào đoạn code này.
#include <bits/stdc++.h>
using namespace std;
const int INF = 1e8;
struct Point {
int x, y, id;
int diff_yx() const { return y - x; }
int sum_xy() const { return x + y; }
};
int manhattan_dist(const Point& u, const Point& v) {
return abs(u.x - v.x) + abs(u.y - v.y);
}
struct DSU {
vector<int> par;
DSU() {}
DSU(int n): par(n, -1) {}
int find_set(int u) {
return par[u] < 0 ? u : par[u] = find_set(par[u]);
}
bool join(int u, int v) {
u = find_set(u);
v = find_set(v);
if (u == v) return false;
if (-par[u] < -par[v]) swap(u, v);
par[u] += par[v];
par[v] = u;
return true;
}
};
struct Edge {
int u, v, cost;
};
bool operator<(const Edge& u, const Edge& v) {
return u.cost < v.cost;
}
Edge edge_from_point(const Point& u, const Point& v) {
return {u.id, v.id, manhattan_dist(u, v)};
}
vector<Edge> potential_edges; // Các cạnh tối ưu cần xét
vector<Point> solve_single_recur(vector<Point> p) {
if (p.size() <= 1) return p;
int upper_size = (int)p.size() / 2;
auto upper = solve_single_recur({p.begin(), p.begin() + upper_size});
auto lower = solve_single_recur({p.begin() + upper_size, p.end()});
vector<Point> res;
Point min_diff_yx{0, INF, -1};
int upper_ptr = 0;
for (auto lo : lower) {
while (upper_ptr < upper_size and upper[upper_ptr].sum_xy() <= lo.sum_xy()) {
if (min_diff_yx.diff_yx() > upper[upper_ptr].diff_yx()) {
min_diff_yx = upper[upper_ptr];
}
res.push_back(upper[upper_ptr]);
++upper_ptr;
}
if (min_diff_yx.id != -1) {
potential_edges.push_back(edge_from_point(min_diff_yx, lo));
}
res.push_back(lo);
}
res.insert(res.end(), upper.begin() + upper_ptr, upper.end());
return res;
}
void solve_single(vector<Point> p) { // Giải bài toán với một góc phần tám
sort(p.begin(), p.end(), [](const Point& u, const Point& v) {
return u.y == v.y ? u.x < v.x : u.y > v.y;
});
solve_single_recur(p);
}
void rotate_90(vector<Point>& p) { // Xoay tất cả các điểm 90 độ
for (auto& cur: p) {
int x = cur.x, y = cur.y;
cur.x = -y;
cur.y = x;
}
}
void flip(vector<Point>& p) { // Đối xứng các điểm qua trục Ox
for (auto& cur: p) {
cur.y = -cur.y;
}
}
int solve(vector<Point> p) {
for (int ft = 0; ft < 2; ++ft) {
for (int i = 0; i < 4; ++i) {
solve_single(p);
rotate_90(p);
}
flip(p);
}
int ans = 0;
DSU dsu((int)p.size());
sort(potential_edges.begin(), potential_edges.end());
for (const auto& e : potential_edges) {
if (dsu.join(e.u, e.v)) {
ans += e.cost;
}
}
// Thuật toán kruskal
return ans;
}
int main() {
ios_base::sync_with_stdio(false); cin.tie(NULL);
int n;
cin >> n;
vector<Point> p(n);
for (int i = 0; i < n; ++i) {
cin >> p[i].x >> p[i].y;
p[i].id = i;
}
cout << solve(p);
}
Giống như quy hoạch động, thuật toán đường quét là một công cụ mạnh vì nó không chỉ là một thuật toán, mà còn là một dạng thuật toán mà có thể dùng để giải một phạm vi các bài toán rất rộng, bao gồm một số bài toán không được nêu ở đây (như phép tam giác hóa Delaunay) và cả các dạng mới chỉ xuất hiện lần đầu trong một cuộc thi.