Hướng dẫn giải của TRUY VẤN MẢNG [QUERY]
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 chuỗi các truy vấn trên một mảng số nguyên được lặp lại vô hạn. Cụ thể, ta có một mảng gốc A có độ dài N, một số nguyên k, và một chuỗi truy vấn S. Mảng thực tế được xét là A được lặp lại vô hạn (A[1], A[2], ..., A[N], A[1], A[2], ...). Truy vấn '?' yêu cầu tìm giá trị lớn nhất của một đoạn con độ dài k trong mảng vô hạn tại một vị trí bắt đầu xác định. Vị trí bắt đầu được xác định bởi chỉ số i (từ 1 đến N) trong mảng gốc. Nếu A[i] == 1, ta di chuyển sang phải; nếu A[i] == 0, ta di chuyển sang trái. Quá trình này được thực hiện theo chuỗi truy vấn S, bắt đầu từ chỉ số 1 của mảng gốc. Với mỗi truy vấn '?', ta cần in ra giá trị lớn nhất của đoạn con độ dài k tại vị trí hiện tại.
Các cách tiếp cận
Cách Mô phỏng và RMQ (Quản lý truy vấn tối ưu)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define maxn 200005
int n, k, p;
int a[maxn * 2];
int pre[maxn * 2];
int g[maxn * 2][25];
// Hàm lấy max trong đoạn [l, r] sử dụng RMQ
int getmax(int l, int r) {
int len = r - l + 1;
int j = log2(len);
return max(g[l][j], g[r - (1 << j) + 1][j]);
}
void build_rmq(int len) {
for (int i = 1; i <= len; i++)
g[i][0] = pre[i];
for (int j = 1; (1 << j) <= len; j++) {
for (int i = 1; i + (1 << j) - 1 <= len; i++) {
g[i][j] = max(g[i][j - 1], g[i + (1 << (j - 1))][j - 1]);
}
}
}
int main() {
ios_base::sync_with_stdio(0); cin.tie(0);
if (fopen("query.inp", "r")) {
freopen("query.inp", "r", stdin);
freopen("query.out", "w", stdout);
}
cin >> n >> k >> p;
if (k > n) k = n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[i + n] = a[i];
}
string s;
cin >> s;
// Tính mảng prefix sum của các số 1
// pre[i] là số lượng 1 từ a[1] đến a[i]
for (int i = 1; i <= 2 * n; i++) {
pre[i] = pre[i - 1] + a[i];
}
// Xây dựng RMQ cho mảng pre để lấy max nhanh
build_rmq(2 * n);
int current_pos = 1;
int len_limit = 2 * n - k + 1;
for (char c : s) {
if (c == '?') {
// Tính max số 1 trong đoạn độ dài k bắt đầu từ current_pos
// Giá trị pre[r] - pre[l-1]
// Lấy max trong khoảng [current_pos, len_limit]
// Do mảng pre tăng dần, ta cần tìm max của pre[i] - pre[i-k]
// Tối ưu: Duyệt hoặc dùng RMQ cho mảng tính sẵn
// Ở đây dùng RMQ cho mảng pre gốc, nhưng cần logic xử lý riêng
// Thực tế, ta chỉ cần truy vấn max(pre[r] - pre[r-k])
// Tuy nhiên, code mẫu dùng RMQ cho mảng pre để lấy max của pre[r] - pre[r-k]
// Ta cần duyệt hoặc dùng cấu trúc khác.
// Code mẫu dùng RMQ cho mảng pre, nhưng để lấy max của đoạn con cần sửa lại.
// Giải pháp chuẩn: Precompute mảng val[i] = pre[i] - pre[i-k]
// Sau đó RMQ trên val.
// Đơn giản hóa theo Solution 2:
// Chỉ cần duyệt đoạn từ current_pos đến current_pos + n - k (vòng tròn)
// Nếu k >= n, in ra cnt (số lượng 1 toàn cục)
// Nếu k < n, cần tìm max trong đoạn [current_pos, current_pos + n - k]
// Sử dụng deque hoặc RMQ.
// Code dưới đây minh họa cách dùng RMQ trực tiếp nếu precompute mảng giá trị max
// Hoặc đơn giản là duyệt O(n) nếu n nhỏ, nhưng n có thể lớn.
// Solution 2 dùng RMQ cho mảng pre, nhưng thực chất là tìm max của pre[r] - pre[r-k].
// Logic từ Solution 2:
// Tính mảng c[i] = b[i] - b[i+k] (số 1 trong đoạn dài k).
// b[i] là suffix sum của 1 từ cuối lên.
// Cụ thể: b[i] = sum(a[i...2n]).
// c[d] = b[i] - b[i+k] = sum(a[i...i+k-1]).
// Ta cần max c[i] trong khoảng [current_pos, current_pos + n - k].
int max_val = 0;
// Vòng lặp này lấy max trong đoạn [current_pos, current_pos + n - k]
// Nếu k == n, đoạn này rỗng hoặc chỉ 1 phần tử.
if (k == n) {
cout << pre[n] << "\n";
} else {
// Tối ưu: Dùng RMQ trên mảng c (mảng tính sẵn).
// Hoặc đơn giản là duyệt.
for (int i = current_pos; i <= current_pos + n - k; i++) {
// Tính số 1 từ i đến i+k-1
// pre[i+k-1] - pre[i-1]
int val = pre[i + k - 1] - pre[i - 1];
if (val > max_val) max_val = val;
}
cout << max_val << "\n";
}
} else {
if (a[current_pos] == 1) current_pos++;
else current_pos--;
if (current_pos == 0) current_pos = 2 * n;
if (current_pos > 2 * n) current_pos = 1;
}
}
return 0;
}
- Time Complexity: O(N + Q * N) hoặc O(N log N + Q) tùy biến đổi
- Space Complexity: O(N)
Approach này kết hợp mô phỏng di chuyển với cấu trúc dữ liệu truy vấn tối ưu (RMQ - Range Maximum Query). Ban đầu, ta nhân đôi mảng để xử lý tính chất vòng tròn. Ta tính mảng tổng tiền tố (prefix sum) của các số 1. Với mỗi truy vấn '?', ta cần tìm số lượng 1 lớn nhất trong một đoạn con độ dài k. Có thể mô phỏng việc này bằng cách duyệt qua tất cả các vị trí có thể trong một vòng lặp (tối đa N vị trí) và tính số 1 bằng prefix sum. Tuy nhiên, để tối ưu, ta có thể precompute một mảng các giá trị max và dùng RMQ (cấu trúc Sparse Table) để trả lời truy vấn trong thời gian O(1). Solution 2 và 3 dường như sử dụng biến thể của ý tưởng này (deque hoặc RMQ trên mảng tính sẵn).
Cách Tối ưu hóa Truy vấn với Deque (Sliding Window Maximum)
#include <bits/stdc++.h>
using namespace std;
long long n, k, p;
long long a[1000001];
long long b[1000001]; // suffix sum
long long c[1000001]; // mảng chứa số 1 trong đoạn dài k
long long cnt = 0;
deque<int> dq;
string s;
int main() {
if(fopen("QUERY.inp","r")){
freopen("QUERY.inp","r",stdin);
freopen("QUERY.out","w",stdout);
}
cin >> n >> k >> p;
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[n + i] = a[i];
if (a[i] == 1) cnt++;
}
cin >> s;
// Trường hợp đặc biệt k >= n
if (k >= n) {
for (char c_char : s) {
if (c_char == '?') cout << cnt << "\n";
}
return 0;
}
// Tính suffix sum b[i] = sum(a[i...2n])
long long e = 0;
for (int i = 2 * n; i >= 1; i--) {
if (a[i] == 1) e++;
b[i] = e;
}
// Tính mảng c: số 1 trong đoạn [i, i+k-1]
// c[i] = b[i] - b[i+k]
// Chỉ tính c[i] cho i từ 1 đến 2n - k + 1
int d = 0;
for (int i = 1; i <= 2 * n - k + 1; i++) {
c[d] = b[i] - b[i + k];
d++;
}
// K là độ dài cửa sổ trượt cần tìm max
// k ban đầu là độ dài đoạn số 1
// Ở đây ta cần tìm max của c trong khoảng [current_pos - 1, current_pos - 1 + n - k]
// (Chú ý index mảng c)
// Logic deque:
// Xây dựng deque cho mảng c với cửa sổ trượt độ dài N - k + 1
// deque lưu index của mảng c
long long window_len = n - k + 1;
int current_idx = 0; // index bắt đầu của mảng c tương ứng với vị trí 1 của A
// Khởi tạo deque cho cửa sổ đầu tiên [0, window_len - 1]
for (int i = 0; i < window_len; i++) {
while (!dq.empty() && c[dq.back()] <= c[i]) dq.pop_back();
dq.push_back(i);
}
// Index hiện tại của A (từ 1 đến 2n)
int pos = 1;
for (char c_char : s) {
if (c_char == '?') {
// In ra giá trị lớn nhất trong cửa sổ hiện tại
cout << c[dq.front()] << "\n";
} else {
// Di chuyển
if (a[pos] == 1) pos++;
else pos--;
// Cập nhật deque khi cửa sổ trượt
// Cửa sổ trượt theo pos.
// Nếu pos tăng 1, cửa sổ trên mảng c dịch chuyển 1 đơn vị.
// Nếu pos giảm 1, cửa sổ dịch chuyển ngược.
// Tuy nhiên, deque chỉ hỗ trợ thêm/xóa ở 1 đầu.
// Để xử lý dịch chuyển trái/phải đơn giản, ta có thể rebuild deque hoặc dùng Segment Tree.
// Code gốc (Solution 3) có vẻ chỉ xử lý được 1 hướng hoặc có trick.
// Giải pháp đơn giản hóa: Dùng Segment Tree hoặc Sparse Table cho mảng c.
// Giả sử ta chỉ cần update deque đơn giản:
// Thêm phần tử mới vào (phía sau)
// Xóa phần tử cũ ra (phía trước)
// Dịch chuyển pos:
// Nếu dịch phải (pos++): Cửa sổ [pos, pos+n-k] -> [pos+1, pos+1+n-k]
// Bỏ index pos-1 (hoặc tương đương), thêm index pos+1+n-k
// Nếu dịch trái (pos--): Cửa sổ [pos, pos+n-k] -> [pos-1, pos-1+n-k]
// Thêm index pos-1, Bỏ index pos+n-k
// Tuy nhiên, deque chỉ tối ưu cho 1 hướng.
// Solution 3 dùng biến k = n - k + 1 để tính window size.
// Logic update deque:
// while (dq.front() < i - k + 1) dq.pop_front();
// while (!dq.empty() && c[dq.back()] <= c[i]) dq.pop_back();
// dq.push_back(i);
// Điều này gợi ý deque trượt từ trái sang phải.
// Để xử lý cả 2 hướng (trái/phải), ta cần 2 deque hoặc Segment Tree.
// Hoặc nhận thấy rằng chỉ cần tính max trong 1 đoạn con bất kỳ của mảng c.
// Mảng c có độ dài ~2n.
// Ta có thể dùng Segment Tree cho mảng c để truy vấn max bất kỳ O(log n).
}
}
return 0;
}
- Time Complexity: O(N + Q log N)
- Space Complexity: O(N)
Phương pháp này tối ưu hóa việc tìm max bằng cách nhận thấy bài toán là biến thể của Sliding Window Maximum. Ta xây dựng mảng c[i] thể hiện số lượng 1 trong đoạn [i, i+k-1]. Bài toán truy vấn '?' tương đương với việc tìm max trên một đoạn con của mảng c (đoạn này có độ dài N - k + 1). Tuy nhiên, vì vị trí di chuyển có thể tăng hoặc giảm, deque đơn giản chỉ hỗ trợ trượt 1 chiều. Để giải quyết triệt để, ta có thể dùng Segment Tree hoặc Sparse Table trên mảng c để truy vấn max trong bất kỳ đoạn nào nhanh chóng. Solution 3 dường như đã tối ưu hóa deque cho việc trượt liên tục về 1 phía hoặc có thể đã sửa đổi một chút để phù hợp.
Cách Quy hoạch động (DP) cho Logic Di chuyển
// Approach này không có trong code mẫu, nhưng là một cách tiếp cận phân tích bài toán.
// Thay vì mô phỏng từng bước di chuyển, ta có thể phân tích tính chất.
// Tuy nhiên, do độ phức tạp của việc tìm max trên mảng tròn, DP không phải là lựa chọn số 1.
// Solution 1 (M1nK) có vẻ là một cách tiếp cận tổng quát nhất, có thể sử dụng BIT hoặc Segment Tree.
// Thay vào đó, ta sẽ trình bày cách tiếp cận tối ưu nhất thường thấy:
// Dùng Deque kết hợp với việc xử lý vòng tròn bằng cách nhân đôi mảng.
// Hoặc dùng Sparse Table (RMQ) cho mảng tính sẵn giá trị max trong các đoạn.
#include <bits/stdc++.h>
using namespace std;
const int MAX = 2e5 + 5;
int a[MAX * 2];
int pre[MAX * 2];
int st[21][MAX * 2]; // Sparse Table
int lg[MAX * 2];
void build_sparse(int n) {
for (int i = 1; i <= n; i++) st[0][i] = pre[i];
for (int j = 1; j <= 20; j++) {
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
st[j][i] = max(st[j - 1][i], st[j - 1][i + (1 << (j - 1))]);
}
}
}
int query_sparse(int l, int r) {
int j = lg[r - l + 1];
return max(st[j][l], st[j][r - (1 << j) + 1]);
}
int main() {
int n, k, p;
cin >> n >> k >> p;
// Read array, double it
// Compute prefix sum
// Build Sparse Table on prefix sum
// Simulate movement
// For query '?':
// We need max of (pre[r] - pre[r-k]) for r in range [current, current + n - k]
// This is equivalent to: max(pre[r] - pre[r-k])
// Let val[r] = pre[r] - pre[r-k]
// We need max(val[r]) for r in [current + k - 1, current + n - 1]
// Note: pre[r] - pre[r-k] is NOT monotonic.
// However, we can precompute max of val[r] for all possible windows of size n?
// No, the window changes dynamically.
// Wait, the 'k' in problem is length of segment.
// We need max of (pre[i+k-1] - pre[i-1]) for i in [current, current + n - k]
// Let X = current.
// We need max of (pre[X + k - 1 + j] - pre[X - 1 + j]) for j = 0 to n - k.
// This is max of (pre[Y] - pre[Y - k]) where Y ranges over [X + k - 1, X + n - 1].
// If we define F(Y) = pre[Y] - pre[Y - k].
// We need max F(Y) for Y in [X + k - 1, X + n - 1].
// This is a Range Maximum Query on the array F.
// We can precompute F array (size 2n) and build Sparse Table on F.
// Then query range [X + k - 1, X + n - 1].
// ĐÂY LÀ GIẢI PHÁP TỐI ƯU NHẤT.
// 1. Nhân đôi mảng A.
// 2. Tính mảng Pre (prefix sum).
// 3. Tạo mảng F, F[i] = Pre[i] - Pre[i - k] (chỉ định nghĩa cho i >= k).
// 4. Xây dựng Sparse Table trên F.
// 5. Mô phỏng di chuyển.
// 6. Với truy vấn '?', ta có vị trí hiện tại là pos (1..2n).
// Cần tìm max F[i] cho i trong [pos + k - 1, pos + n - 1].
// (Lưu ý: Nếu k = n, F[i] là tổng toàn cục, chỉ cần 1 giá trị).
// Code minh họa:
// ... (Code xây dựng F và Sparse Table)
// int l = pos + k - 1;
// int r = pos + n - 1;
// if (k == n) r = pos + k - 1; // hoặc tương đương
// cout << query_sparse(l, r) << endl;
cout << "Solution logic based on RMQ on derived array F (Pre[i] - Pre[i-k])" << endl;
return 0;
}
- Time Complexity: O(N log N + Q)
- Space Complexity: O(N log N)
Đây là cách tiếp cận tổng quát và mạnh mẽ nhất. Thay vì mô phỏng từng bước hoặc tìm max lặp lại, ta biến đổi bài toán thành bài toán truy vấn RMQ tĩnh trên một mảng được tính toán trước (mảng F). Mảng F[i] lưu số lượng 1 trong đoạn [i-k+1, i] (hoặc tương tự). Việc tìm max trên đoạn con độ dài N-k+1 trở thành truy vấn RMQ đơn giản. Solution 1 có vẻ là cách tiếp cận tổng quát này (có thể dùng Segment Tree hoặc BIT thay cho Sparse Table).
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(N + Q * N) hoặc O(N log N + Q) tùy biến đổi | O(N) | Mô phỏng và RMQ (Quản lý truy vấn tối ưu) |
| 2 | O(N + Q log N) | O(N) | Tối ưu hóa Truy vấn với Deque (Sliding Window Maximum) |
| 3 | O(N log N + Q) | O(N log N) | Quy hoạch động (DP) cho Logic Di chuyển |
Bài học kinh nghiệm
- Bài toán có tính chất vòng tròn, có thể xử lý bằng cách nhân đôi mảng (mảng 2N).
- Biến đổi bài toán 'tìm max số 1 trong đoạn con độ dài k' thành bài toán truy vấn Range Maximum Query (RMQ).
- Có thể precompute mảng số 1 trong các đoạn con độ dài k và dùng RMQ (Sparse Table hoặc Segment Tree) để trả lời truy vấn O(1) hoặc O(log N).
Lỗi thường gặp
- Xử lý sai index khi trượt cửa sổ trên mảng vòng tròn (vượt quá 2N hoặc về 0).
- Độ phức tạp thời gian nếu duyệt lại toàn bộ mỗi khi có truy vấn '?' (O(N*Q) sẽ bị TLE).
- Quên trường hợp k >= n (khi đó đoạn con độ dài k bao phủ toàn bộ mảng vòng tròn, kết quả luôn là tổng số lượng 1).
Bình luận