Hướng dẫn cho LQDOJ Cup 2025 - Round #5 - Diện tích chung lớn nhất
Chỉ sử dụng khi thực sự cần thiết như một cách tôn trọng tác giả và người viết hướng dẫn này.
Chép code từ bài hướng dẫn để nộp bài là hành vi có thể dẫn đến khóa tài khoản.
Chép code từ bài hướng dẫn để nộp bài là hành vi có thể dẫn đến khóa tài khoản.
Tóm tắt đề bài
Cho \(n\) hình chữ nhật có các cạnh song song với trục tọa độ. Với mỗi truy vấn \(k_j\), tìm diện tích lớn nhất của một vùng đất (cũng là hình chữ nhật) sao cho vùng đất đó nằm trong ít nhất \(k_j\) hình chữ nhật đã cho.
Phân tích
- Giới hạn: \(n \le 333\), số truy vấn \(q \le 11\). Tọa độ các đỉnh rất lớn (lên đến \(10^9\)).
- Nhận xét quan trọng:
- Diện tích chung lớn nhất của một tập hợp các hình chữ nhật (nếu có) luôn là một hình chữ nhật có các cạnh nằm trên các đường thẳng \(x = x_i\) hoặc \(y = y_i\) từ các hình chữ nhật ban đầu.
- Do tọa độ lớn nhưng số lượng hình chữ nhật nhỏ, ta có thể sử dụng kỹ thuật rời rạc hóa (coordinate compression) hoặc duyệt trên các biên tọa độ.
- Một vùng đất thuộc ít nhất \(k\) hình chữ nhật có thể được xác định bởi một dải ngang \([y_{start}, y_{end}]\) và một dải dọc \([x_{start}, x_{end}]\).
Hướng giải quyết
1. Rời rạc hóa tọa độ
- Thu thập tất cả các tọa độ \(x\) xuất hiện trong đề bài (\(x_1^{(i)}\) và \(x_2^{(i)}\)), sắp xếp tăng dần và loại bỏ trùng lặp. Gọi tập này là
compressX. - Tương tự với tọa độ \(y\), ta có các giá trị biên \(y\) tiềm năng.
2. Duyệt dải ngang (Y-axis)
- Ta cố định một dải ngang giới hạn bởi hai tọa độ \(y\) bất kỳ trong tập các tọa độ \(y\) đã biết: \(y_{bot}\) và \(y_{top}\).
- Chiều cao của dải này là \(H = y_{top} - y_{bot}\).
- Một hình chữ nhật \(i\) được coi là "bao phủ" dải ngang này nếu \(y_1^{(i)} \le y_{bot}\) và \(y_{top} \le y_2^{(i)}\).
3. Giải bài toán 1 chiều trên trục X
- Với mỗi dải ngang \([y_{bot}, y_{top}]\) đã cố định, ta lọc ra danh sách các hình chữ nhật thỏa mãn điều kiện bao phủ dải ngang đó.
- Bài toán trở thành: Tìm đoạn \([x_{start}, x_{end}]\) dài nhất sao cho đoạn này nằm trong ít nhất \(k\) hình chữ nhật (trong danh sách đã lọc).
- Để giải quyết bài toán 1 chiều này hiệu quả:
- Sử dụng kỹ thuật Two Pointers kết hợp với mảng đếm.
- Sắp xếp các hình chữ nhật theo \(x_{start}\).
- Duyệt qua từng \(x_{start}\) tiềm năng, mở rộng \(x_{end}\) xa nhất có thể bằng cách đếm số lượng hình chữ nhật bao phủ đoạn \([x_{start}, x_{end}]\).
- Cụ thể: Với mỗi \(x_{start}\) cố định, ta cần tìm \(x_{end}\) sao cho số lượng hình chữ nhật bao phủ toàn bộ \([x_{start}, x_{end}]\) ít nhất là \(k\).
4. Tối ưu hóa
- Với \(n = 333\), số cặp \((y_{bot}, y_{top})\) là \(O(n^2)\).
- Với mỗi cặp, việc xử lý trên trục \(X\) mất \(O(n \log n)\) hoặc \(O(n)\).
- Tổng độ phức tạp khoảng \(O(q \cdot n^3)\), phù hợp với giới hạn thời gian khi \(n=333\) và \(q=11\).
Độ phức tạp
- Thời gian: \(O(q \cdot n^3)\), trong đó \(n\) là số hình chữ nhật và \(q\) là số truy vấn.
- Bộ nhớ: \(O(n)\) để lưu trữ tọa độ và danh sách hình chữ nhật.
Code tham khảo
C++
#include <bits/stdc++.h>
using namespace std;
#define ii pair<int, int>
#define fi first
#define se second
#define siz(v) (int)(v).size()
#define all(v) begin(v), end(v)
const int MAXN = 345;
const int infINT = 1e9 + 7;
struct Rect {
int xS, yS, xE, yE;
} rect[MAXN];
int n, q, cnt[MAXN * 2];
vector<int> compressX, coordsY;
// Tìm độ dài lớn nhất trên trục X sao cho đoạn đó thuộc ít nhất req hình chữ nhật
int getMaxWidth(const vector<ii>& activeRects, int req) {
if (activeRects.size() < req) return 0;
int bestWidth = 0;
int m = compressX.size();
// Duyệt qua từng điểm bắt đầu xS
for (int i = 0; i < activeRects.size(); ++i) {
int startIdx = activeRects[i].fi; // Chỉ số trong compressX
// Reset mảng đếm cho mỗi startIdx mới
for (int j = 1; j <= m; ++j) cnt[j] = 0;
// Đếm xem các hình chữ nhật kết thúc tại đâu
for (int j = i; j < activeRects.size(); ++j) {
cnt[rect[activeRects[j].se].xE]++;
}
int currentCover = activeRects.size() - i;
int j = 1;
// Two pointers: tìm vị trí j xa nhất mà vẫn có ít nhất req hình bao phủ
while (j < m && currentCover >= req) {
currentCover -= cnt[j];
if (currentCover >= req) j++;
else break;
}
if (j >= startIdx) {
bestWidth = max(bestWidth, compressX[j - 1] - compressX[startIdx - 1]);
}
}
return bestWidth;
}
long long solveQuery(int k) {
long long maxArea = 0;
for (int i = 0; i < coordsY.size(); ++i) {
for (int j = i + 1; j < coordsY.size(); ++j) {
int yBot = coordsY[i];
int yTop = coordsY[j];
int height = yTop - yBot;
vector<ii> activeRects;
for (int r = 1; r <= n; ++r) {
if (rect[r].yS <= yBot && rect[r].yE >= yTop) {
activeRects.push_back({rect[r].xS, r});
}
}
if (activeRects.size() < k) continue;
sort(all(activeRects));
maxArea = max(maxArea, 1LL * height * getMaxWidth(activeRects, k));
}
}
return maxArea;
}
int main() {
ios_base::sync_with_stdio(0); cin.tie(0);
if (!(cin >> n >> q)) return 0;
for (int i = 1; i <= n; ++i) {
cin >> rect[i].xS >> rect[i].yS >> rect[i].xE >> rect[i].yE;
if (rect[i].xS > rect[i].xE) swap(rect[i].xS, rect[i].xE);
if (rect[i].yS > rect[i].yE) swap(rect[i].yS, rect[i].yE);
compressX.push_back(rect[i].xS);
compressX.push_back(rect[i].xE);
coordsY.push_back(rect[i].yS);
coordsY.push_back(rect[i].yE);
}
sort(all(compressX));
compressX.erase(unique(all(compressX)), compressX.end());
sort(all(coordsY));
coordsY.erase(unique(all(coordsY)), coordsY.end());
for (int i = 1; i <= n; ++i) {
rect[i].xS = lower_bound(all(compressX), rect[i].xS) - compressX.begin() + 1;
rect[i].xE = lower_bound(all(compressX), rect[i].xE) - compressX.begin() + 1;
}
for (int i = 0; i < q; ++i) {
int k; cin >> k;
cout << solveQuery(k) << (i == q - 1 ? "" : " ");
}
cout << endl;
return 0;
}
Bình luận