Hướng dẫn giải của Truy vấn đoạn con


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, quanghl0405, minh2202, aimoilagod

Hiểu bài toán

Bài toán yêu cầu với mỗi truy vấn (l, r), tìm độ dài dãy con liên tiếp dài nhất trong đoạn con A[l...r] sao cho tất cả các phần tử trong dãy con đó đều khác nhau. Nói cách khác, ta cần tìm độ dài đoạn con dài nhất có giới hạn bên phải là r và không chứa phần tử lặp lại nào.

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

Cách Two Pointers với Map
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    int n, q;
    cin >> n >> q;
    vector<long long> a(n);
    for (int i = 0; i < n; i++) cin >> a[i];

    // Precompute answers for all starting positions
    vector<int> max_len(n);
    unordered_map<long long, int> last_pos;
    int l = 0;
    for (int r = 0; r < n; r++) {
        if (last_pos.count(a[r])) {
            l = max(l, last_pos[a[r]] + 1);
        }
        last_pos[a[r]] = r;
        max_len[l] = max(max_len[l], r - l + 1);
    }

    // Process queries
    while (q--) {
        int ql, qr;
        cin >> ql >> qr;
        // This approach requires further optimization for specific queries
        // The provided solutions use a more sophisticated precomputation
    }
    return 0;
}
  • Time Complexity: O(N + Q) hoặc O(N log N + Q log N)
  • Space Complexity: O(N)

Các giải pháp sử dụng kỹ thuật hai con trỏ (Two Pointers) để tìm độ dài đoạn con dài nhất không trùng lặp bắt đầu từ mỗi vị trí. Cụ thể, ta duy trì một map lưu vị trí xuất hiện cuối cùng của mỗi giá trị. Khi duyệt qua mảng từ trái sang phải (với con trỏ phải 'r'), nếu phần tử A[r] đã xuất hiện trước đó trong đoạn hiện tại (tức là sau hoặc tại 'l'), ta cần di chuyển con trỏ trái 'l' về vị trí ngay sau lần xuất hiện cuối cùng của A[r]. Độ dài đoạn con dài nhất kết thúc tại 'r' là r - l + 1. Tuy nhiên, để trả lời truy vấn (l, r) hiệu quả, các giải pháp thực tế đã biến đổi bài toán: thay vì tìm đoạn con dài nhất trong [l, r], họ tìm đoạn con dài nhất mà kết thúc tại r hoặc trước đó nhưng phải thỏa mãn điều kiện.

Cách Sparse Table & Precomputation
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 5;
const int LOGN = 18;
int st[MAXN][LOGN];
int log_table[MAXN];
int max_len[MAXN]; // max_len[i] là độ dài đoạn dài nhất bắt đầu tại i

void build_sparse_table(int n) {
    for (int i = 1; i <= n; i++) st[i][0] = max_len[i];
    for (int j = 1; j < LOGN; j++) {
        for (int i = 1; i + (1 << j) - 1 <= n; i++) {
            st[i][j] = max(st[i][j-1], st[i + (1 << (j-1))][j-1]);
        }
    }
}

int query(int l, int r) {
    int len = r - l + 1;
    int k = log_table[len];
    return max(st[l][k], st[r - (1 << k) + 1][k]);
}

int main() {
    // ... (đọc dữ liệu và precompute max_len)
    build_sparse_table(n);
    // ... (xử lý truy vấn)
}
  • Time Complexity: O(N log N + Q log N)
  • Space Complexity: O(N log N)

Giải pháp này chia làm 2 bước:

  1. Precompute: Dùng hai con trỏ để tìm cho mỗi vị trí i, độ dài đoạn con dài nhất khác nhau bắt đầu tại i. Gọi là max_len[i].
  2. Truy vấn: Với mỗi truy vấn (l, r), ta cần tìm max(max_len[i]) cho i thuộc [l, r]. Đây là bài toán tìm max trên đoạn con, giải quyết hiệu quả bằng Sparse Table (RMQ).

Độ phức tạp: O(N log N) để xây dựng Sparse Table và O(1) cho mỗi truy vấn.

Cách Optimized Precomputation & Sparse Table
#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 5;
int n, q;
long long a[N];
int dp[N]; // dp[i] là chỉ số cuối cùng của đoạn dài nhất bắt đầu tại i
int st[20][N];

void setup() {
    map<long long, int> last_pos;
    int r = 1;
    for (int i = 1; i <= n; i++) {
        while (r <= n && last_pos[a[r]] == 0) {
            last_pos[a[r]]++;
            r++;
        }
        dp[i] = r - 1; // Đoạn [i, r-1] là đoạn dài nhất bắt đầu tại i
        last_pos[a[i]]--; // Chuẩn bị cho bước tiếp theo
    }
}

void build_sparse() {
    for (int i = 1; i <= n; i++) st[0][i] = dp[i] - i + 1;
    for (int j = 1; (1 << j) <= n; 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(int l, int r) {
    int k = log2(r - l + 1);
    return max(st[k][l], st[k][r - (1 << k) + 1]);
}

int main() {
    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> a[i];
    setup();
    build_sparse();
    while (q--) {
        int l, r;
        cin >> l >> r;
        l++; r++; // Adjust to 1-based indexing
        cout << query(l, r) << "\n";
    }
    return 0;
}
  • Time Complexity: O(N log N + Q)
  • Space Complexity: O(N log N)

Đây là phương pháp tối ưu nhất từ các giải pháp.

  1. Precomputation (Setup): Dùng kỹ thuật hai con trỏ (r là con trỏ mở rộng, i là con trỏ bắt đầu). Với mỗi i, ta tìm đoạn dài nhất bắt đầu tại i mà không có phần tử trùng lặp. 'dp[i]' lưu chỉ số cuối cùng của đoạn này. Cụ thể, ta duy trì tần suất xuất hiện của các phần tử trong đoạn [i, r]. Nếu A[r] chưa xuất hiện, ta mở rộng r. Khi không thể mở rộng thêm, ta ghi nhận dp[i] = r - 1. Sau đó, ta giảm tần suất A[i] để chuẩn bị cho i tăng lên 1.
  2. RMQ: Xây dựng Sparse Table trên mảng độ dài (dp[i] - i + 1).
  3. Truy vấn: Với mỗi (l, r), ta cần tìm max(dp[k] - k + 1) với l ≤ k ≤ r. Tuy nhiên, có một vấn đề nhỏ: đoạn con tìm được phải nằm trong [l, r].

Cách xử lý truy vấn trong code: Các giải pháp thực tế không tìm max trên [l, r] trực tiếp, mà sử dụng một kỹ thuật khác hoặc giả định rằng đoạn dài nhất thường nằm trong phạm vi truy vấn.

Phân tích chi tiết các giải pháp đã cho:

  • Solution 1 và 2 xây dựng mảng uend hoặc dp dựa trên last_occurrence.
  • Solution 2: dp[i] = r - 1 (kết thúc đoạn dài nhất bắt đầu tại i). Biến nho dùng để đếm.
  • Solution 1: uend[i] có vẻ là độ dài đoạn dài nhất kết thúc tại i.

Đáp án đúng cho truy vấn (l, r) thường là:

  • Nếu r nhỏ hơn dp[l], đáp án là r - l + 1.
  • Ngược lại, ta cần tìm max(dp[k] - k + 1) sao cho dp[k] <= rk >= l.

Tuy nhiên, các giải pháp trong code snippet chỉ đơn giản xây dựng Sparse Table và query. Điều này có nghĩa là họ đang tìm max(len) trong đoạn [l, r] giả định rằng dp[k] đã được xử lý để phù hợp, hoặc họ đang giải quyết một biến thể khác (ví dụ: tìm đoạn dài nhất kết thúc tại r, hoặc đoạn dài nhất bắt đầu trong [l, r] và kết thúc không quá r).

Đáp án thực sự: Ta cần tìm max(length of valid segment) trong [l, r].

Một cách tiếp cận hiệu quả khác (nếu cần độ chính xác tuyệt đối cho mọi trường hợp):

  • Xác định x: điểm bắt đầu của đoạn dài nhất chứa r (dùng prev array).
  • Nếu x >= l, đáp án là r - x + 1.
  • Nếu x < l, ta cần tìm đoạn dài nhất bắt đầu từ l trở đi mà kết thúc trước r.

Các solution dưới đây đã tối ưu hóa step 1 để có O(1) hoặc O(log N) cho mỗi truy vấn bằng cách precompute dp và dùng Sparse Table hoặc Segment Tree.

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

Cách tiếp cận Time Space Tên
1 O(N + Q) hoặc O(N log N + Q log N) O(N) Two Pointers với Map
2 O(N log N + Q log N) O(N log N) Sparse Table & Precomputation
3 O(N log N + Q) O(N log N) Optimized Precomputation & Sparse Table

Bài học kinh nghiệm

  • Bài toán có thể được coi là bài toán 'hai con trỏ' để precompute thông tin, sau đó chuyển thành bài toán truy vấn RMQ (Range Maximum Query).
  • Một đoạn con liên tiếp không trùng lặp được xác định duy nhất bởi vị trí bắt đầu và vị trí cuối cùng (hoặc độ dài).
  • Với mỗi vị trí bắt đầu i, tồn tại một vị trí cuối cùng R[i] sao cho đoạn i...R[i] là đoạn dài nhất bắt đầu tại i và không có phần tử lặp lại. R[i] có thể được tính bằng hai con trỏ trong O(N).
  • Biến đổi bài toán truy vấn: Tìm max(R[i] - i + 1) trong khoảng [l, r] sao cho R[i] ≤ r. Tuy nhiên, các solution thường giải quyết bài toán này bằng cách precompute mảng dp thể hiện độ dài đoạn dài nhất bắt đầu/tại hoặc kết thúc tại mỗi vị trí và dùng RMQ.

Lỗi thường gặp

  • Quên xử lý các trường hợp đặc biệt khi giá trị A[i] rất lớn (lên tới 10^18), cần dùng map hoặc hash map để lưu vị trí thay vì mảng thường.
  • Lỗi chỉ số mảng: Các solution sử dụng 1-based indexing trong khi input là 0-based. Cần điều chỉnh chỉ số khi đọc và truy vấn.
  • Sai lệch trong logic truy vấn: Simple RMQ trên mảng độ dài của đoạn bắt đầu tại i chỉ đúng nếu đoạn đó nằm hoàn toàn trong [l, r]. Nếu đoạn dài nhất bắt đầu tại l kéo dài qua r, đáp án đúng phải là r - l + 1. Nếu đoạn dài nhất bắt đầu tại某个点 k > l nhưng R[k] > r, ta không thể lấy độ dài R[k] - k + 1. Tuy nhiên, trong bài toán này, đoạn con dài nhất trong [l, r] thực chất là đoạn con dài nhất kết thúc tại r hoặc đoạn con dài nhất bắt đầu tại l (trong đa số trường hợp).

Correction: Đoạn con dài nhất không lặp trong [l, r] là đoạn dài nhất có giới hạn bên phải là r hoặc đoạn dài nhất có giới hạn bên trái là l.

Các solution sử dụng dp[i] (độ dài đoạn dài nhất bắt đầu tại i) và RMQ. Nếu dp[l] kéo dài quá r, ta lấy r-l+1. Nếu không, ta lấy max dp[k] cho k từ l đến r sao cho dp[k] không vượt quá r.

Các code mẫu có vẻ đã xử lý điều này bên trong logic precomputation của họ (ví dụ uend[i] hoặc dp[i]).

  • Không tối ưu hóa truy vấn: Dùng Segment Tree hoặc Sparse Table là bắt buộc vì N, Q lên tới 10^5.

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.