Hướng dẫn giải của Truy vấn mảng_VP9


Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
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ập

Tác giả: Hiếu Nguyễn, kimtuan15, itachivsshisui, M1nK_08

Hiểu bài toán

Bài toán yêu cầu xử lý các truy vấn trên một mảng số nguyên $A$ có độ dài $N$. Mảng này có tính chất vòng tròn (kết thúc nối với đầu). Có hai loại thao tác:

  1. Cập nhật giá trị tại một vị trí (thường là xoay vòng mảng).
  2. Truy vấn: Tìm giá trị lớn nhất trong một đoạn con độ dài $K$ (đoạn này có thể nằm trên hai phần của mảng do tính chất vòng tròn).

Đầu vào bao gồm $N$ (kích thước mảng), $K$ (kích thước đoạn con), và $Q$ (số lượng truy vấn). Sau đó là $N$ phần tử của mảng. Cuối cùng là một chuỗi ký tự độ dài $Q$, trong đó:

  • Ký tự '!' đại diện cho thao tác cập nhật (xoay mảng sang phải 1 đơn vị).
  • Ký tự '?' đại diện cho truy vấn.

Đầu ra là kết quả của các truy vấn '?'.

Các cách tiếp cận

Cách Mô phỏng với mảng sao chép (Simulation with Extended Array)
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);

    int N, K, Q;
    if (!(cin >> N >> K >> Q)) return 0;

    // Sử dụng mảng mở rộng để xử lý tính chất vòng tròn
    vector<int> A(2 * N);
    for (int i = 0; i < N; ++i) {
        cin >> A[i];
        A[i + N] = A[i]; // Sao chép phần tử vào nửa sau
    }

    string S;
    cin >> S;

    // Vị trí bắt đầu của mảng hiện tại (ban đầu là 0)
    int start_idx = 0;

    for (char c : S) {
        if (c == '!') {
            // Xoay sang phải: tăng chỉ số bắt đầu lên 1
            // Nếu vượt quá N thì trừ đi N để quay về
            start_idx = (start_idx + 1) % N;
        } else {
            // Truy vấn: Tìm max trong đoạn [start_idx, start_idx + K - 1]
            // Do mảng mở rộng, ta có thể truy cập trực tiếp
            int max_val = -1e9;
            for (int i = start_idx; i < start_idx + K; ++i) {
                if (A[i] > max_val) max_val = A[i];
            }
            cout << max_val << "\n";
        }
    }

    return 0;
}
  • Time Complexity: O(Q * K)
  • Space Complexity: O(N)

Cách tiếp cận đơn giản nhất là sao chép mảng gốc vào một mảng lớn hơn (độ dài $2N$) để xử lý hiện tượng tràn chỉ số khi truy vấn.

  • Xoay mảng: Thay vì thực sự xoay mảng (tốn $O(N)$), ta chỉ cần duy trì một biến start_idx để đánh dấu vị trí bắt đầu của mảng ảo so với mảng gốc. Khi xoay, ta chỉ cần tăng start_idx.
  • Truy vấn: Với mỗi truy vấn '?', ta duyệt qua $K$ phần tử liên tiếp trong mảng lớn từ chỉ số start_idx và tìm giá trị lớn nhất.
  • Nhược điểm: Độ phức tạp thời gian phụ thuộc vào $K$. Nếu $K$ lớn và số truy vấn nhiều, chương trình sẽ chạy chậm.
Cách Segment Tree (Cây Phân đoạn)
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <climits>

using namespace std;

const int MAXN = 200005;
int A[MAXN * 2];
int tree[4 * MAXN * 2];

// Xây dựng Segment Tree
void build(int node, int l, int r) {
    if (l == r) {
        tree[node] = A[l];
        return;
    }
    int mid = (l + r) / 2;
    build(2 * node, l, mid);
    build(2 * node + 1, mid + 1, r);
    tree[node] = max(tree[2 * node], tree[2 * node + 1]);
}

// Truy vấn Segment Tree
int query(int node, int l, int r, int u, int v) {
    if (v < l || r < u) return INT_MIN;
    if (u <= l && r <= v) return tree[node];
    int mid = (l + r) / 2;
    return max(query(2 * node, l, mid, u, v), query(2 * node + 1, mid + 1, r, u, v));
}

int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);

    int N, K, Q;
    if (!(cin >> N >> K >> Q)) return 0;

    // Mảng mở rộng độ dài 2N
    for (int i = 0; i < N; ++i) {
        cin >> A[i];
        A[i + N] = A[i];
    }

    // Xây dựng cây phân đoạn cho mảng mở rộng
    build(1, 0, 2 * N - 1);

    string S;
    cin >> S;

    int start_idx = 0;

    for (char c : S) {
        if (c == '!') {
            start_idx = (start_idx + 1) % N;
        } else {
            // Truy vấn khoảng [start_idx, start_idx + K - 1]
            int res = query(1, 0, 2 * N - 1, start_idx, start_idx + K - 1);
            cout << res << "\n";
        }
    }

    return 0;
}
  • Time Complexity: O(N + Q * log N)
  • Space Complexity: O(N)

Sử dụng Segment Tree để tối ưu hóa việc tìm max.

  • Mảng mở rộng: Tương tự cách 1, ta dùng mảng $A$ độ dài $2N$.
  • Xây dựng cây: Xây dựng Segment Tree trên mảng $2N$ này. Thời gian xây dựng là $O(N)$.
  • Xử lý truy vấn:
    • Với thao tác '!', chỉ cập nhật start_idx (thời gian $O(1)$).
    • Với truy vấn '?', ta truy vấn Segment Tree để tìm max trong khoảng từ start_idx đến start_idx + K - 1 (thời gian $O(log N)$).
  • Ưu điểm: Rất nhanh, phù hợp với dữ liệu lớn.
Cách Deque (Sliding Window Maximum)
#include <iostream>
#include <vector>
#include <deque>
#include <string>
#include <algorithm>

using namespace std;

int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);

    int N, K, Q;
    if (!(cin >> N >> K >> Q)) return 0;

    // Mảng mở rộng 2N
    vector<int> A(2 * N);
    for (int i = 0; i < N; ++i) {
        cin >> A[i];
        A[i + N] = A[i];
    }

    // Mảng lưu max cho mỗi đoạn độ dài K
    vector<int> MaxVal(N);
    deque<int> dq;

    // Khởi tạo Deque cho đoạn đầu tiên [0, K-1]
    for (int i = 0; i < K; ++i) {
        while (!dq.empty() && A[dq.back()] <= A[i]) dq.pop_back();
        dq.push_back(i);
    }
    MaxVal[0] = A[dq.front()];

    // Sliding Window cho các đoạn còn lại
    // Đoạn bắt đầu tại i tương ứng với khoảng [i, i+K-1]
    // Ta cần tính MaxVal cho i từ 1 đến N-1
    for (int i = 1; i < N; ++i) {
        // Loại bỏ chỉ số cũ nếu nó nằm ngoài cửa sổ mới
        if (!dq.empty() && dq.front() < i) dq.pop_front();

        // Thêm phần tử mới vào (tại vị trí i + K - 1)
        while (!dq.empty() && A[dq.back()] <= A[i + K - 1]) dq.pop_back();
        dq.push_back(i + K - 1);

        MaxVal[i] = A[dq.front()];
    }

    string S;
    cin >> S;

    int start_idx = 0;
    for (char c : S) {
        if (c == '!') {
            start_idx = (start_idx + 1) % N;
        } else {
            // In ra giá trị max tương ứng với start_idx hiện tại
            cout << MaxVal[start_idx] << "\n";
        }
    }

    return 0;
}
  • Time Complexity: O(N + Q)
  • Space Complexity: O(N)

Đây là cách tối ưu nhất về mặt thời gian chạy.

  • Tiền xử lý: Thay vì truy vấn trực tiếp, ta tính toán trước giá trị lớn nhất cho tất cả $N$ vị trí có thể là "đầu mảng" trong tương lai.
    • Xử lý mảng $A$ mở rộng $2N$.
    • Dùng Deque để tìm Max cho các đoạn con độ dài $K$. Vì chỉ có $N$ vị trí đầu mảng khác nhau (từ $0$ đến $N-1$), ta chỉ cần tính $N$ giá trị max.
    • MaxVal[i] sẽ lưu max của đoạn bắt đầu tại $i$.
  • Xử lý truy vấn:
    • Thao tác '!' chỉ thay đổi start_idx.
    • Thao tác '?' chỉ cần in ra MaxVal[start_idx] (truy cập mảng $O(1)$).
  • Độ phức tạp: Quá trình tiền xử lý mất $O(N)$, xử lý $Q$ truy vấn mất $O(Q)$. Tổng thể $O(N + Q)$.

Phân tích độ phức tạp

Cách tiếp cận Time Space Tên
1 O(Q * K) O(N) Mô phỏng với mảng sao chép (Simulation with Extended Array)
2 O(N + Q * log N) O(N) Segment Tree (Cây Phân đoạn)
3 O(N + Q) O(N) Deque (Sliding Window Maximum)

Bài học kinh nghiệm

  • Biến đổi bài toán từ thao tác 'xoay mảng' sang 'thay đổi chỉ số bắt đầu' (start index) giúp tiết kiệm thời gian đáng kể.
  • Sử dụng mảng mở rộng (độ dài $2N$) là kỹ thuật phổ biến để xử lý các bài toán về mảng vòng tròn.
  • Deque (Sliding Window Maximum) hoặc Segment Tree là các công cụ hiệu quả để xử lý bài toán tìm max trên đoạn con di chuyển.

Lỗi thường gặp

  • Quên cập nhật chỉ số bắt đầu modulo $N$ khi xoay mảng, dẫn đến chỉ số âm hoặc vượt quá giới hạn cho phép.
  • Lỗi truy cập mảng khi sử dụng Segment Tree hoặc Deque nếu không xử lý đúng biên của mảng mở rộng (ví dụ: chỉ số $N$ thay vì $0$ khi lặp lại).
  • Độ phức tạp thuật toán: Cách mô phỏng trực tiếp có thể gây Timeout nếu $K$ và $Q$ lớn.

Bình luận

Please read the guidelines before commenting.


Không có bình luận tại thời điểm này.