Hướng dẫn giải của DÃY SỐ ĐỀ 11
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.
Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.
Bạn phải đăng nhập để mở lời giải này.
Đăng nhậpTác giả: , , ,
Hiểu bài toán
Bài toán yêu cầu xử lý một dãy số A gồm N phần tử và Q truy vấn. Với mỗi truy vấn có giá trị V, ta cần tìm độ dài dãy con liên tiếp dài nhất sao cho tất cả các phần tử trong dãy con đó đều có giá trị nhỏ hơn hoặc bằng V. Nói cách khác, với ngưỡng V, ta loại bỏ các phần tử có giá trị > V, và tìm đoạn liên tiếp dài nhất của các phần tử còn lại.
Các cách tiếp cận
Cách Xử lý offline với DSU (Disjoint Set Union)
#include <bits/stdc++.h>
using namespace std;
struct DSU {
vector<int> parent, sz;
vector<bool> active;
DSU(int n) {
parent.resize(n + 2);
sz.resize(n + 2, 0);
active.resize(n + 2, false);
for (int i = 0; i <= n + 1; ++i) parent[i] = i;
}
int find(int x) {
return parent[x] == x ? x : parent[x] = find(parent[x]);
}
void unite(int a, int b, int ¤tMax) {
a = find(a); b = find(b);
if (a == b) return;
if (sz[a] < sz[b]) swap(a, b);
parent[b] = a;
sz[a] += sz[b];
currentMax = max(currentMax, sz[a]);
}
void activate(int i, int ¤tMax) {
active[i] = true;
sz[i] = 1;
currentMax = max(currentMax, 1);
}
};
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
int n, q;
if (!(cin >> n >> q)) return 0;
vector<pair<int, int>> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i].first;
a[i].second = i + 1;
}
vector<pair<int, int>> queries(q);
for (int i = 0; i < q; ++i) {
cin >> queries[i].first;
queries[i].second = i;
}
sort(a.begin(), a.end());
sort(queries.begin(), queries.end());
DSU dsu(n);
vector<int> results(q);
int currentMax = 0;
int ptr = 0;
for (int i = 0; i < q; ++i) {
int limit = queries[i].first;
int idx = queries[i].second;
while (ptr < n && a[ptr].first <= limit) {
int pos = a[ptr].second;
dsu.activate(pos, currentMax);
if (pos > 1 && dsu.active[pos - 1]) dsu.unite(pos, pos - 1, currentMax);
if (pos < n && dsu.active[pos + 1]) dsu.unite(pos, pos + 1, currentMax);
ptr++;
}
results[idx] = currentMax;
}
for (int res : results) cout << res << "\n";
return 0;
}
- Time Complexity: O(N log N + Q log Q)
- Space Complexity: O(N + Q)
Cách tiếp cận này xử lý các truy vấn offline. Đầu tiên, ta lưu các giá trị của dãy số và chỉ số của chúng, rồi sắp xếp theo giá trị. Các truy vấn cũng được sắp xếp theo giá trị V. Ta sử dụng DSU để quản lý các đoạn liên tiếp của các phần tử đã được 'kích hoạt'. Khi duyệt qua các truy vấn từ nhỏ đến lớn, ta kích hoạt các phần tử có giá trị <= V. Khi kích hoạt một phần tử, ta kiểm tra các láng giềng (trái và phải) đã kích hoạt chưa. Nếu có, ta gộp chúng lại bằng DSU và cập nhật độ dài đoạn lớn nhất.
Cách Xử lý online với Segment Tree (Duyệt theo giá trị)
// Pseudocode logic representation for clarity
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
int n, q;
int a[MAXN];
vector<int> vals; // Mảng lưu các giá trị phân loại
vector<int> pos[MAXN]; // Danh sách vị trí cho mỗi giá trị
int tree[4 * MAXN]; // Segment tree lưu độ dài đoạn dài nhất
int pref[MAXN]; // Prefix sum để đếm số phần tử <= X
void build(int node, int l, int r) {
if (l == r) {
// Logic tính toán tại lá
return;
}
int mid = (l + r) / 2;
build(2 * node, l, mid);
build(2 * node + 1, mid + 1, r);
// Logic gộp kết quả
}
int query(int node, int l, int r, int ql, int qr) {
if (ql > r || qr < l) return 0;
if (ql <= l && r <= qr) return tree[node];
int mid = (l + r) / 2;
return query(2 * node, l, mid, ql, qr) + query(2 * node + 1, mid + 1, r, ql, qr);
}
int main() {
// Đọc dữ liệu
// Lấy mảng a và sắp xếp để lấy các giá trị phân loại (compression)
// Xây dựng cây phân đoạn dựa trên các vị trí
// Với mỗi truy vấn V, tìm chỉ số trong mảng giá trị phân loại
// Truy vấn cây phân đoạn để tìm kết quả
return 0;
}
- Time Complexity: O((N + Q) log N)
- Space Complexity: O(N)
Phương pháp này có thể được thực hiện online. Ta có thể sử dụng Segment Tree kết hợp với việc phân loại giá trị (value compression). Cây phân đoạn có thể lưu trữ thông tin về các đoạn liên tiếp của các phần tử có giá trị <= một ngưỡng nào đó. Tuy nhiên, cách trực quan hơn là sử dụng Segment Tree để lưu trữ các vị trí của các phần tử và thực hiện truy vấn. Một biến thể hiệu quả khác là Segment Tree với 'tiền xử lý offline': xây dựng cây bằng cách cập nhật các giá trị从小 đến lớn, và với mỗi truy vấn, ta chỉ cần truy vấn trạng thái của cây tại thời điểm giá trị đó.
Cách Duyệt tuần tự với Stack (Monotonic Stack)
#include <bits/stdc++.h>
using namespace std;
int main() {
// Code dưới đây chỉ minh họa logic tính toán cho 1 giá trị V c cụ thể
// Để xử lý Q truy vấn, ta có thể dùng Offline + Suy nghĩ về Segment Tree hoặc DSU.
// Tuy nhiên, Segment Tree kết hợp với Monotonic Stack để tìm Left/Right boundaries là 1 cách khác.
int n, q;
cin >> n >> q;
vector<int> a(n + 1);
for(int i = 1; i <= n; i++) cin >> a[i];
// Logic cho Offline:
// Sắp xếp (a[i], i) tăng dần.
// Sắp xếp truy vấn tăng dần.
// Sử dụng Segment Tree hoặc DSU để kích hoạt các phần tử.
// Logic cho Online (không khuyến khích nếu Q lớn):
// Với mỗi truy vấn V, tạo mảng boolean valid[n].
// for i=1..n: if a[i] <= V valid[i] = true else false.
// Duyệt mảng valid tìm đoạn dài nhất chứa toàn true.
// Độ phức tạp O(N*Q) -> Chậm.
return 0;
}
- Time Complexity: O(N * Q)
- Space Complexity: O(N)
Đây là phương pháp Brute Force trực tiếp nhất. Với mỗi truy vấn V, ta duyệt qua toàn bộ dãy số và đánh dấu các phần tử có giá trị <= V. Sau đó, ta duyệt lại một lần nữa để tìm đoạn liên tiếp dài nhất toàn các phần tử được đánh dấu. Phương pháp này có độ phức tạp thời gian O(N * Q), quá cao để chấp nhận nếu N và Q lớn (ví dụ 10^5). Tuy nhiên, nó là nền tảng để hiểu vấn đề và thường được dùng để kiểm tra tính đúng đắn của các phương pháp phức tạp hơn.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(N log N + Q log Q) | O(N + Q) | Xử lý offline với DSU (Disjoint Set Union) |
| 2 | O((N + Q) log N) | O(N) | Xử lý online với Segment Tree (Duyệt theo giá trị) |
| 3 | O(N * Q) | O(N) | Duyệt tuần tự với Stack (Monotonic Stack) |
Bài học kinh nghiệm
- Bài toán có thể được chuyển đổi thành bài toán 'tìm đoạn con dài nhất thỏa mãn điều kiện' và có thể xử lý offline bằng cách sắp xếp các phần tử và truy vấn theo giá trị.
- DSU (Disjoint Set Union) là cấu trúc dữ liệu lý tưởng để quản lý các đoạn liên tiếp khi ta thêm các phần tử vào dãy theo thứ tự tăng dần của giá trị.
- Việc xử lý offline giúp biến các truy vấn động thành các thao tác thêm phần tử vào cấu trúc dữ liệu (DSU), giảm độ phức tạp đáng kể.
Lỗi thường gặp
- Quên xử lý biên khi truy vấn các chỉ số láng giềng (ví dụ phần tử đầu tiên hoặc cuối cùng của dãy).
- Thiếu bước 'path compression' hoặc 'union by size' trong DSU có thể làm tăng độ phức tạp.
- Xử lý offline nhưng không sắp xếp lại thứ tự truy vấn ban đầu để in kết quả đúng theo yêu cầu đề bài.
- Lầm tưởng rằng Monotonic Stack có thể giải quyết bài toán này cho nhiều truy vấn một cách hiệu quả mà không cần kết hợp với cấu trúc dữ liệu khác (như Segment Tree hoặc DSU) cho các bài toán offline.
Bình luận