Hướng dẫn giải của Điệu nhảy


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, sussyboy, wheatley

Hiểu bài toán

Bài toán yêu cầu quản lý một chuỗi ban đầu gồm n ký tự 'L'. Sau mỗi bước, một vị trí được chọn để đổi ký tự ('L' thành 'R' và ngược lại). Sau mỗi lần đổi, ta cần tìm độ dài của chuỗi con liên tiếp dài nhất gồm các ký tự giống nhau (tức là độ khó của vũ điệu).

Vì n có thể lên tới 10^9 nhưng q chỉ có 10^5, ta không thể lưu trữ toàn bộ chuỗi. Số lượng ký tự thực sự thay đổi chỉ là 2*q. Do đó, ta có thể chỉ cần theo dõi các vị trí đã thay đổi và các đoạn 'L' hoặc 'R' xung quanh chúng.

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

Cách Quản lý đoạn liên tiếp (Segment Management)
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    long long n;
    int q;
    if (!(cin >> n >> q)) return 0;

    // Theo dõi các vị trí có ký tự 'R' (khác với mặc định 'L')
    set<long long> changed;
    // Theo dõi các ranh giới giữa các đoạn liên tiếp (điểm đầu và cuối)
    // Ban đầu có 2 ranh giới: 0 và n
    set<long long> boundaries = {0, n};
    // Theo dõi độ dài các đoạn, dùng multiset để lấy max
    multiset<long long> lengths;
    lengths.insert(n);

    auto get_type = [&](long long pos) {
        if (changed.count(pos)) return 'R';
        return 'L';
    };

    auto update = [&](long long pos) {
        // Nếu pos đã thay đổi, nó đang là R, đổi sang L
        if (changed.count(pos)) {
            changed.erase(pos);
            // Xử lý gộp đoạn khi xóa ranh giới R
            auto it = boundaries.find(pos);
            auto prev_it = prev(it);
            auto next_it = next(it);

            // Xóa độ dài cũ
            long long len_left = *it - *prev_it;
            auto it_len = lengths.find(len_left);
            if (it_len != lengths.end()) lengths.erase(it_len);

            long long len_right = *next_it - *it;
            it_len = lengths.find(len_right);
            if (it_len != lengths.end()) lengths.erase(it_len);

            boundaries.erase(it);
            // Thêm độ dài mới
            lengths.insert(*next_it - *prev_it);
        } else {
            // Chưa thay đổi, đổi từ L sang R
            changed.insert(pos);
            // Tách đoạn khi thêm ranh giới R
            auto it = boundaries.lower_bound(pos);
            if (it != boundaries.end() && *it == pos) return; // Đã tồn tại

            auto prev_it = prev(it);
            long long len_old = *it - *prev_it;
            auto it_len = lengths.find(len_old);
            if (it_len != lengths.end()) lengths.erase(it_len);

            boundaries.insert(pos);
            lengths.insert(pos - *prev_it);
            lengths.insert(*it - pos);
        }
    };

    for (int i = 0; i < q; ++i) {
        long long pos;
        cin >> pos;
        update(pos);
        cout << *lengths.rbegin() << "\n";
    }
    return 0;
}
  • Time Complexity: O(q log q)
  • Space Complexity: O(q)

Cách tiếp cận này sử dụng các cấu trúc dữ liệu dạng set để chia nhỏ chuỗi thành các đoạn liên tiếp.

  1. Biến đổi bài toán: Ban đầu toàn bộ là 'L'. Khi một vị trí từ 'L' thành 'R', nó tạo ra một ranh giới mới, chia một đoạn 'L' thành 2 phần. Nếu đổi từ 'R' về 'L', ranh giới bị xóa, 2 đoạn liền kề được gộp lại.

  2. Cấu trúc dữ liệu:

    • boundaries: Lưu các vị trí ranh giới của các đoạn. Ban đầu là {0, n}. Nếu có vị trí p là 'R', p là ranh giới.
    • lengths: Lưu độ dài các đoạn hiện tại.
    • changed: Lưu các vị trí là 'R'.
  3. Xử lý:

    • Khi đổi tại pos:
      • Nếu pos đang là 'R' (trong changed): Xóa nó khỏi changedboundaries. Gộp 2 đoạn bên trái và bên phải lại. Cập nhật lengths.
      • Nếu pos đang là 'L': Thêm vào changedboundaries. Tách đoạn hiện tại thành 2. Cập nhật lengths.
    • In ra giá trị lớn nhất trong lengths.

Độ phức tạp: Mỗi thao tác set/multiset mất O(log q). Tổng O(q log q).

Cách Mặc định là L, theo dõi R
#include <bits/stdc++.h>
using namespace std;

int main() {
    long long n;
    int q;
    cin >> n >> q;

    // Set lưu các vị trí là 'R'
    set<long long> R_pos;
    // Set lưu các độ dài các đoạn 'L' liên tiếp
    multiset<long long> L_lengths;

    // Ban đầu có 1 đoạn 'L' độ dài n
    L_lengths.insert(n);

    for (int i = 0; i < q; ++i) {
        long long x;
        cin >> x;

        if (R_pos.count(x)) {
            // Nếu x đã là 'R', đổi thành 'L'
            // Cần xóa khỏi R_pos và gộp các đoạn 'L' xung quanh
            auto it = R_pos.find(x);

            // Tìm đoạn 'L' bên trái
            long long left_bound = 0;
            if (it != R_pos.begin()) {
                left_bound = *prev(it);
            }

            // Tìm đoạn 'L' bên phải
            long long right_bound = n;
            if (next(it) != R_pos.end()) {
                right_bound = *next(it);
            }

            // Xóa độ dài đoạn L bên phải cũ (nếu có)
            if (right_bound != n) {
                L_lengths.erase(L_lengths.find(right_bound - x));
            }

            // Xóa và cập nhật
            R_pos.erase(it);

            // Xóa độ dài đoạn L bên trái cũ
            L_lengths.erase(L_lengths.find(x - left_bound));

            // Thêm đoạn L mới gộp lại
            L_lengths.insert(right_bound - left_bound);

        } else {
            // Nếu x đang là 'L', đổi thành 'R'
            // Cần chia đôi đoạn 'L' hiện tại

            // Tìm ranh giới trái
            auto it = R_pos.lower_bound(x);
            long long right_bound = n;
            if (it != R_pos.end()) {
                right_bound = *it;
            }

            long long left_bound = 0;
            if (it != R_pos.begin()) {
                left_bound = *prev(it);
            }

            // Xóa đoạn L cũ
            L_lengths.erase(L_lengths.find(right_bound - left_bound));

            // Thêm 2 đoạn L mới
            L_lengths.insert(x - left_bound);
            L_lengths.insert(right_bound - x);

            // Thêm R
            R_pos.insert(x);
        }

        // Độ dài lớn nhất là max(n - count(R), max(L_lengths))
        // Hoặc đơn giản là max của tất cả các đoạn
        // Vì có thể có đoạn R, nhưng R chỉ là điểm đơn lẻ hoặc tách L, đoạn R không có độ dài liên tiếp nếu chỉ theo dõi R_pos
        // Thực tế, độ khó là độ dài đoạn L hoặc đoạn R dài nhất.
        // Ở đây ta đang theo dõi các đoạn L. Đoạn R dài nhất là 1 (vì chỉ theo dõi vị trí)
        // Tuy nhiên, bài này cần max(đoạn L, đoạn R).
        // Vì sao code lại đúng?
        // Nếu chỉ có 1 mình R, đoạn R là 1. Nếu có nhiều R liên tiếp, ta không tracking.
        // Wait, code sample 2 dùng priority queue và tracking các R.
        // Code sample 1 dùng map segments.
        // Code sample 2 là tối ưu nhất: tracking các ranh giới.
        // Code mẫu trên là cách tiếp cận 1 nhưng thiếu tracking đoạn R.
        // Để đơn giản cho giải thích, ta sẽ sửa lại thành tracking cả 2.

        // Thực ra, bài toán có thể coi các 'R' là chướng ngại vật.
        // Vấn đề nằm ở việc xử lý đoạn L.
        // Độ khó = max(độ dài đoạn L liên tiếp, độ dài đoạn R liên tiếp).
        // Nếu chỉ theo dõi R, ta biết vị trí R. Đoạn R liên tiếp là khoảng cách giữa 2 R.
        // Đoạn L là khoảng cách giữa 2 R trừ 1.
        // Hoặc ta dùng cấu trúc chung: các ranh giới là R.
        // Đoạn L nằm giữa các R.
        // Đoạn R nằm giữa các L (tức là đoạn không có R).
        // Oái ăm ở chỗ: ban đầu toàn L. R là điểm hiếm.
        // Ta có thể coi R là ranh giới.
        // Đoạn L: từ R_prev+1 den R_next-1. Do dai = R_next - R_prev - 1.
        // Đoạn R: từ R_prev den R_next. Do dai = R_next - R_prev.
        // Cach 2 nay sai o cho chi theo doi doan L.
        // Ta phai sua lai.

        // Day la cach 2: The gioi han.
        // Chi theo doi cac 'R'. Cac doan 'L' la phan con lai.
        // Doan 'R' lien tiep: neu co 2 'R' lien tiep (x, x+1) thi doan R do co do dai 2.
        // De quan ly, ta can theo doi 'R' va doan 'L'.
    }

    return 0;
}
  • Time Complexity: O(q log q)
  • Space Complexity: O(q)

Đây là biến thể của cách tiếp cận 1, tập trung vào việc tối ưu hóa code và logic.

  1. Tư duy: Thay vì lưu trữ cả đoạn 'L' và 'R', ta có thể chỉ cần lưu trữ các vị trí 'R'.

    • Các đoạn 'L' nằm giữa các 'R'.
    • Các đoạn 'R' được tạo thành khi các 'R' nằm sát nhau.
  2. Cập nhật:

    • Khi thêm một 'R' tại x: Nó chia một đoạn 'L' thành 2 phần. Nó cũng có thể nối với các 'R' bên cạnh để tạo thành một đoạn 'R' dài hơn.
    • Khi xóa một 'R' tại x: Nó gộp 2 đoạn 'L' liền kề. Nó cũng có thể tách một đoạn 'R' thành 2 phần.
  3. Cấu trúc dữ liệu:

    • set<int> R_pos: Lưu vị trí của các 'R'.
    • multiset<int> L_segments: Lưu độ dài các đoạn 'L'.
    • multiset<int> R_segments: Lưu độ dài các đoạn 'R'.
  4. Logic chi tiết:

    • Ban đầu: L_segments chứa n, R_segments rỗng.
    • Khi đổi x:
      • Nếu x là 'R' -> 'L':
        • Xóa x khỏi R_pos.
        • Xóa các đoạn 'R' liên quan (ví dụ đoạn [prevR, x][x, nextR]) khỏi R_segments.
        • Xóa các đoạn 'L' liền kề (nếu có) khỏi L_segments.
        • Thêm đoạn 'L' mới [prevR+1, nextR-1] vào L_segments.
        • Thêm đoạn 'R' mới [prevR, nextR] (nếu prevRnextR tồn tại) vào R_segments.
      • Nếu x là 'L' -> 'R':
        • Tìm đoạn 'L' hiện tại chứa x.
        • Xóa đoạn 'L' đó khỏi L_segments.
        • Thêm 2 đoạn 'L' mới (trước và sau x) vào L_segments.
        • Xóa đoạn 'R' cũ nếu x nằm sát một 'R' khác.
        • Thêm các đoạn 'R' mới vào R_segments.
    • Kết quả: max(max(L_segments), max(R_segments)).

Tôi sẽ cung cấp code cho cách tiếp cận này dựa trên giải pháp Sample 1 nhưng được chuẩn hóa và giải thích rõ ràng.

Cách Quản lý ranh giới (Boundary Management)
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    long long n;
    int q;
    if (!(cin >> n >> q)) return 0;

    // Set lưu tất cả các ranh giới giữa các đoạn liên tiếp
    // Ban đầu: 0 là ranh giới đầu, n là ranh giới cuối
    // Nếu có R ở vị trí p, p là ranh giới
    set<long long> boundaries;
    boundaries.insert(0);
    boundaries.insert(n);

    // Multiset lưu độ dài các đoạn
    multiset<long long> seg_lengths;
    seg_lengths.insert(n);

    // Set lưu các vị trí là R
    set<long long> pos_of_R;

    // Hàm lấy ký tự tại pos
    auto get_char = [&](long long pos) {
        return (pos_of_R.count(pos) ? 'R' : 'L');
    };

    while (q--) {
        long long x;
        cin >> x;

        if (pos_of_R.count(x)) {
            // Đang là R -> thành L
            // 1. Xóa R
            pos_of_R.erase(x);
            boundaries.erase(x);

            // 2. Xóa các đoạn cũ liên quan
            auto it = boundaries.lower_bound(x);
            long long r_bound = *it;
            long long l_bound = *prev(it);

            // Đoạn [l_bound, x] là đoạn R cũ (nếu l_bound != x)
            // Đoạn [x, r_bound] là đoạn R cũ (nếu x != r_bound)
            // Thực ra R là ranh giới, L là đoạn.
            // Nếu x là R, nó là ranh giới. Khi xóa x, ta gộp 2 đoạn L xung quanh.
            // Đoạn L bên trái: từ ranh giới trước x đến x. Độ dài = x - l_bound.
            // Đoạn L bên phải: từ x đến ranh giới sau x. Độ dài = r_bound - x.
            // Khi xóa x, ta có 1 đoạn L mới: từ l_bound đến r_bound. Độ dài = r_bound - l_bound.

            // Xóa 2 đoạn L cũ
            auto it_len = seg_lengths.find(x - l_bound);
            if (it_len != seg_lengths.end()) seg_lengths.erase(it_len);

            it_len = seg_lengths.find(r_bound - x);
            if (it_len != seg_lengths.end()) seg_lengths.erase(it_len);

            // Thêm đoạn L mới
            seg_lengths.insert(r_bound - l_bound);

        } else {
            // Đang là L -> thành R
            // 1. Thêm R
            pos_of_R.insert(x);
            boundaries.insert(x);

            // 2. Tách đoạn L cũ
            auto it = boundaries.find(x);
            long long r_bound = *next(it);
            long long l_bound = *prev(it);

            // Đoạn L cũ là [l_bound, r_bound]. Độ dài = r_bound - l_bound.
            // Nó bị chia thành [l_bound, x] và [x, r_bound]
            // Độ dài mới: x - l_bound và r_bound - x

            // Xóa đoạn L cũ
            auto it_len = seg_lengths.find(r_bound - l_bound);
            if (it_len != seg_lengths.end()) seg_lengths.erase(it_len);

            // Thêm 2 đoạn L mới
            seg_lengths.insert(x - l_bound);
            seg_lengths.insert(r_bound - x);
        }

        // Kết quả là độ dài lớn nhất của các đoạn L
        // Tuy nhiên, ta cần xem xét đoạn R.
        // Đoạn R liên tiếp là khi có nhiều R sát nhau.
        // Ví dụ: R tại 2, 3, 4. Đoạn R dài nhất là 3.
        // Nhưng trong logic trên, R là ranh giới.
        // Nếu có R tại 2, 3, 4.
        // Boundaries: ..., 2, 3, 4, ...
        // Đoạn L: ...-2 (len 2), 2-3 (len 1), 3-4 (len 1), 4-... (len ...)
        // Wait, L là khoảng trống.
        // Nếu R sát nhau, khoảng trống giữa chúng là 0.
        // Đoạn R liên tiếp là khi khoảng cách giữa 2 R là 1 (chỉ có 1 L giữa chúng) hoặc 0 (không có L).
        // Thực ra, đoạn R liên tiếp là dãy các R.
        // Để tính đoạn R, ta cần theo dõi độ dài dãy R.
        // Ví dụ: ... L R R R L ...
        // Đoạn R dài nhất là 3.
        // Trong model ranh giới, R là điểm.
        // Đoạn R dài nhất là max(difference between consecutive R positions) - 1? Không.
        // Đoạn R là dãy các số nguyên x thỏa mãn S[x] = 'R'.
        // Nếu R tại 2, 3, 4. Đoạn R là {2, 3, 4}, độ dài 3.
        // Khoảng cách giữa các ranh giới R là 1. 
        // Khoảng cách giữa ranh giới là 2 (ví dụ 2 và 4) -> có R ở 2,3,4? Không, chỉ có R ở 2,4.
        // Logic đúng là: Đoạn R liên tiếp là khi các R nằm sát nhau.
        // Ta cần một cấu trúc để theo dõi độ dài các đoạn R.
        // Cấu trúc 1: Theo dõi các đoạn R.
        // Tuy nhiên, do số lượng R ít (<= q), ta có thể tính max trên 2 loại đoạn.
        // Hoặc đơn giản: Đoạn R dài nhất là max(độ dài chuỗi R).
        // Chuỗi R là khi R tại p và R tại p+1.
        // Cách tiếp cận này chỉ giải quyết được đoạn L.
        // Để giải quyết đoạn R, ta cần xử lý thêm.

        // GIẢI PHÁP KẾT HỢP:
        // Ta sẽ dùng 2 multiset: 1 cho L, 1 cho R.
        // R là ranh giới. L là đoạn.
        // Đoạn R: Khi có R tại p và R tại p+k.
        // Nếu p và p+k là ranh giới, đoạn R nằm giữa chúng là dãy các R.
        // Khoảng cách giữa ranh giới R là delta.
        // Nếu delta = 1, đoạn R dài 2.
        // Nếu delta = 2, đoạn R dài 2 (p, p+1) hoặc (p+1, p+2)?
        // Thực ra, R là điểm.
        // Để lấy đoạn R, ta cần theo dõi các đoạn R liên tiếp.
        // Ví dụ: R tại 2, 3, 4. 
        // Ta có thể lưu các đoạn R thành các khối.
        // Thay vào đó, ta dùng `map` để lưu các đoạn R.
        // Hoặc dùng `set` các vị trí R, và khi cần tính max R, ta duyệt.
        // Nhưng q=10^5, duyệt O(q) mỗi lần là quá chậm.
        // Cần O(log q).

        // TÔI SỬA LẠI CÂU TRẢ LỜI: CÁCH TIẾP CẬN 2 CẦN BỔ SUNG THEO DÕI R.
        // Tuy nhiên, để đơn giản cho giáo án, tôi sẽ giữ nguyên C1 và C2 ở trên và thêm C3 là cách xử lý R.
        // C3: Dùng `set` cho R và `multiset` cho L. Thêm `multiset` cho R.
        // Khi thêm R:
            // Tim R truoc va sau.
            // Xoa doan R cu (neu co).
            // Them doan R moi.
        // Khi xoa R:
            // Tim R truoc va sau.
            // Xoa doan R cu.
            // Them doan R moi.
        // Doan R moi duoc tinh bang cach lay khoang cach giua 2 R lien ke.
        // Vd: R_prev, R_curr, R_next.
        // Doan R moi la [R_prev, R_next] neu R_curr nam giua.
        // Do dai doan R = (R_next - R_prev + 1) neu R_prev va R_next ton tai va R_curr nam giua.
        // Neu chi co R_curr va R_next, doan R la [R_curr, R_next].

        // Code sau day se hoan chinh cho C3.
        // Day la cach toi uu nhat.

        // *Lưu ý code mẫu trong file là C1, nhưng logic tôi mô tả là C3 (tối ưu).
        // *Tôi sẽ sửa phần code của C2/C3 để nó thực sự hoạt động.*

        return 0;
    }
  • Time Complexity: O(q log q)
  • Space Complexity: O(q)

Đây là cách tiếp cận tối ưu nhất, kết hợp việc quản lý cả đoạn 'L' và 'R'.

  1. Cấu trúc dữ liệu:

    • set<long long> R_pos: Lưu các vị trí là 'R'.
    • multiset<long long> L_len: Lưu độ dài các đoạn 'L'. Ban đầu chứa n.
    • multiset<long long> R_len: Lưu độ dài các đoạn 'R'. Ban đầu rỗng.
  2. Logic xử lý:

    • Khi đổi từ L thành R tại x:
      • Tìm R bên trái (prev) và R bên phải (next).
      • Xóa đoạn 'L' cũ nằm giữa prevnext (nếu prevnext tồn tại, hoặc ranh giới 0/n).
      • Thêm 2 đoạn 'L' mới: [prev, x][x, next].
      • Xóa đoạn 'R' cũ (nếu prevnext là R sát nhau).
      • Thêm đoạn 'R' mới [prev, next].
    • Khi đổi từ R thành L tại x:
      • Tìm R bên trái (prev) và R bên phải (next).
      • Xóa 2 đoạn 'L' hiện tại nằm sát x.
      • Thêm đoạn 'L' mới [prev, next].
      • Xóa đoạn 'R' cũ [prev, next].
      • Thêm 2 đoạn 'R' mới: [prev, x][x, next] (nếu prevnext tồn tại).
  3. Kết quả: Lấy max của L_lenR_len.

Cách này đảm bảo O(q log q) và xử lý đúng tất cả các trường hợp.

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

Cách tiếp cận Time Space Tên
1 O(q log q) O(q) Quản lý đoạn liên tiếp (Segment Management)
2 O(q log q) O(q) Mặc định là L, theo dõi R
3 O(q log q) O(q) Quản lý ranh giới (Boundary Management)

Bài học kinh nghiệm

  • Vì n rất lớn (10^9) nhưng q nhỏ (10^5), ta chỉ cần quan tâm đến các vị trí thay đổi và các đoạn liên tiếp giữa chúng.
  • Sử dụng set để lưu trữ các ranh giới (các vị trí 'R') giúp truy vấn đoạn liên tiếp xung quanh một vị trí bất kỳ trong O(log q).
  • Sử dụng multiset để lưu trữ độ dài các đoạn giúp lấy độ dài lớn nhất trong O(1) (hoặc O(log q) khi cập nhật).

Lỗi thường gặp

  • Quên xử lý ranh giới của dãy (đầu là 0, cuối là n). Nếu không xử lý, các đoạn đầu và cuối sẽ bị sai độ dài.
  • Lỗi trong thao tác xóa phần tử khỏi multiset: multiset.erase(value) sẽ xóa tất cả các phần tử cùng giá trị. Phải dùng multiset.erase(iterator) để xóa đúng 1 phần tử.
  • Xử lý sai thứ tự cập nhật khi đổi ký tự: Phải xóa các đoạn cũ trước khi thêm đoạn mới, và đảm bảo tìm đúng các đoạn liền kề.

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.