Hướng dẫn giải của Xếp bánh Chưng


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, haidang3004, phanvanchung, lephuochauhungvuong

Hiểu bài toán

Bài toán yêu cầu tìm chi phí tối thiểu để di chuyển m chiếc bánh (tổng số từ các lớp) về một đoạn liên tiếp các tọa độ nguyên. Chi phí di chuyển một chiếc bánh từ x đến x' là |x - x'|. Nói cách khác, ta cần chọn một vị trí bắt đầu (hoặc vị trí trung tâm) để đặt m chiếc bánh liên tiếp (ví dụ từ t đến t+m-1) sao cho tổng khoảng cách di chuyển là nhỏ nhất.

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

Cách Mô phỏng trực tiếp (Trừ biến)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

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

    int n;
    if (!(cin >> n)) return 0;
    vector<ll> pos;
    ll total = 0;
    for (int i = 0; i < n; ++i) {
        int a;
        ll x;
        cin >> a >> x;
        total += a;
        // Thêm a vị trí x vào danh sách
        for(int j=0; j<a; ++j) pos.push_back(x);
    }
    sort(pos.begin(), pos.end());
    ll m = pos.size();
    // Biến đổi q[i] = pos[i] - i
    // Nếu chúng ta chọn bắt đầu từ t, chi phí là sum |pos[i] - (t + i)| = sum |(pos[i] - i) - t|
    // Vấn đề trở thành tìm t sao cho sum |q[i] - t| là nhỏ nhất (Bài toán trung vị)
    vector<ll> q(m);
    for(int i=0; i<m; ++i) q[i] = pos[i] - i;
    sort(q.begin(), q.end());
    ll median = q[m/2];
    ll ans = 0;
    for(int i=0; i<m; ++i) ans += abs(q[i] - median);
    cout << ans;
    return 0;
}
  • Time Complexity: O(m log m)
  • Space Complexity: O(m)

Cách này biến đổi bài toán về dạng chuẩn: Tìm t使得 tổng khoảng cách từ các điểm q[i] = x[i] - i về t là nhỏ nhất. Sau khi sắp xếp mảng q, ta lấy giá trị trung vị (median). Nếu m chẵn, bất kỳ giá trị nào giữa 2 phần tử ở giữa đều cho chi phí tối thiểu. Độ phức tạp phụ thuộc vào m (tổng số bánh), tối đa ~10^6 nên chạy được.

Cách Mô phỏng trực tiếp (Trừ biến - tối ưu bộ nhớ)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

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

    int n;
    cin >> n;
    ll m = 0;
    // Đọc dữ liệu và tạo mảng q trực tiếp không cần mảng pos trung gian
    // Tuy nhiên do cần sort nên ta vẫn cần mảng lưu q
    vector<ll> q;
    ll current_idx = 0;
    vector<pair<ll, int>> input(n);
    for(int i=0; i<n; ++i) {
        cin >> input[i].second >> input[i].first; // a, x
        m += input[i].second;
    }
    sort(input.begin(), input.end()); // Sắp xếp theo tọa độ x
    q.reserve(m);
    for(auto& p : input) {
        ll x = p.first;
        int a = p.second;
        for(int k=0; k<a; ++k) {
            q.push_back(x - current_idx);
            current_idx++;
        }
    }
    int mid = m / 2;
    nth_element(q.begin(), q.begin() + mid, q.end());
    ll median = q[mid];
    // Nếu m chẵn, median thứ 2 (m/2) cũng tối ưu, nhưng để chuẩn thì nên xét kĩ hơn
    // Tuy nhiên bài này chấp nhận lấy 1 trong 2 median
    ll ans = 0;
    for(ll val : q) ans += abs(val - median);
    cout << ans;
    return 0;
}
  • Time Complexity: O(m log m)
  • Space Complexity: O(m)

Giống cách 1 nhưng tối ưu hơn ở bước nhập dữ liệu: Thay vì lưu trữ mảng pos gồm m phần tử lặp lại, ta sort input theo x rồi sinh ra mảng q (x[i] - i) trực tiếp. Sử dụng nth_element để tìm median nhanh hơn sort toàn bộ mảng q.

Cách Tối ưu hóa (Không cần mảng)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

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

    int n;
    cin >> n;
    vector<pair<ll, int>> v(n);
    ll total_banh = 0;
    for(int i=0; i<n; ++i) {
        cin >> v[i].second >> v[i].first; // a, x
        total_banh += v[i].second;
    }
    sort(v.begin(), v.end());

    ll m = total_banh;
    ll median_rank = m / 2 + (m % 2); // Rank 1-based
    ll current_rank = 0;
    ll median_val = 0;

    // Tìm giá trị trung vị của q[i] = x - i
    // q[i] được sinh ra theo thứ tự tăng dần của x và i
    // Ta chỉ cần duyệt qua các đoạn
    for(auto& p : v) {
        ll x = p.first;
        int cnt = p.second;
        if (current_rank + cnt < median_rank) {
            current_rank += cnt;
        } else {
            // Median nằm trong đoạn này
            ll offset = median_rank - current_rank - 1; // 0-based offset
            // Giá trị tại rank k trong đoạn này là: x - (current_rank + k)
            median_val = x - (current_rank + offset);
            break;
        }
    }

    // Tính tổng chi phí bằng cách duyệt lại
    ll ans = 0;
    ll current_idx = 0;
    for(auto& p : v) {
        ll x = p.first;
        int cnt = p.second;
        for(int k=0; k<cnt; ++k) {
            ll q_val = x - current_idx;
            ans += abs(q_val - median_val);
            current_idx++;
        }
    }
    cout << ans;
    return 0;
}
  • Time Complexity: O(n log n + m)
  • Space Complexity: O(n)

Phương pháp này dựa trên quan sát: Sau khi sort input theo x, ta có thể sinh ra các giá trị q[i] = x - i một cách tuần tự. Do x tăng dần và i tăng dần, q[i] cũng tăng dần. Do đó, ta có thể tìm giá trị trung vị q[mid] mà không cần sắp xếp mảng q lớn (chỉ cần sort input n). Ta dùng biến đếm current_rank để xác định median thuộc về dòng input nào. Cuối cùng duyệt lại để tính tổng khoảng cách. Độ phức tạp giảm từ O(m log m) xuống O(n log n + m).

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

Cách tiếp cận Time Space Tên
1 O(m log m) O(m) Mô phỏng trực tiếp (Trừ biến)
2 O(m log m) O(m) Mô phỏng trực tiếp (Trừ biến - tối ưu bộ nhớ)
3 O(n log n + m) O(n) Tối ưu hóa (Không cần mảng)

Bài học kinh nghiệm

  • Bài toán di chuyển về đoạn liên tiếp có thể biến đổi về bài toán tìm trung vị: Chọn bắt đầu từ t, chi phí = sum |(xi - i) - t|. Do đó t tối ưu là giá trị trung vị của mảng q[i] = xi - i.
  • Sau khi sort input theo tọa độ x, các giá trị q[i] = x - i được sinh ra theo thứ tự không giảm. Điều này cho phép tìm median và tính tổng chi phí chỉ bằng cách duyệt qua danh sách input thay vì lưu trữ toàn bộ mảng m phần tử.

Lỗi thường gặp

  • Lỗi tràn số nguyên: Chi phí và các tọa độ có thể lên tới 10^14, bắt buộc dùng long long.
  • Sai lệch khi tính median nếu m chẵn: Nếu chỉ lấy phần tử ở giữa mảng (m/2), ta có thể không lấy đúng giá trị tối ưu nếu xét kĩ (giá trị tối ưu nằm giữa 2 phần tử giữa), nhưng trong bài này phép tính tổng abs thường chấp nhận giá trị trung vị bất kỳ nằm trong khoảng tối ưu.
  • Xử lý sai thứ tự khi tính q[i]: q[i]x trừ đi chỉ số i (thứ tự xuất hiện sau khi sort), không phải chỉ số của lớp.

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.