Hướng dẫn giải của Mua sắm ở trung tâm thương mại


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, ndeshi, Minhnoname, tranhaian173

Editorial for fcbuy: Mua sắm ở trung tâm thương mại

Hiểu bài toán

Cá Nóc cần mua M món hàng từ M cửa hàng khác nhau trên dãy N cửa hàng. Anh ta chỉ có thể di chuyển từ trái sang phải. Chi phí di chuyển phụ thuộc vào số lượng hàng đã mua:

  • Nếu chưa mua hàng nào, chi phí di chuyển = 1 * |j - i|.
  • Nếu đã mua x hàng, chi phí di chuyển = x * K * |j - i|. Anh ta bắt đầu tại cửa hàng 1 (vị trí 1). Chỉ những cửa hàng có giá trị B[i] = 1 mới có thể được chọn. Mục tiêu là tìm tổng thời gian di chuyển nhỏ nhất để mua đủ M món hàng. Nếu không thể, in ra -1.

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

Cách Quy hoạch động
#include <bits/stdc++.h>
using namespace std;

#define int long long
const int INF = 1e18;

int n, m, k;
vector<int> shops;
int dp[200005][105]; // dp[index_of_shop][items_bought]

void solve() {
    cin >> n >> m >> k;
    shops.push_back(0); // Đệm để dễ xử lý
    for (int i = 1; i <= n; i++) {
        int b; cin >> b;
        if (b) shops.push_back(i);
    }

    if (shops.size() - 1 < m) {
        cout << -1 << endl;
        return;
    }

    // Khởi tạo dp với INF
    for(int i=0; i<=n; i++) 
        for(int j=0; j<=m; j++) dp[i][j] = INF;

    dp[1][1] = shops[1] - 1; // Mua hàng đầu tiên tại vị trí shops[1]

    // j là số hàng đã mua, i là chỉ số cửa hàng hiện tại trong mảng shops
    for (int j = 1; j < m; j++) {
        for (int i = j; i < shops.size(); i++) {
            if (dp[i][j] == INF) continue;
            // Thử mua hàng tiếp theo tại các cửa hàng sau đó
            for (int next = i + 1; next < shops.size(); next++) {
                int cost = (shops[next] - shops[i]) * j * k;
                dp[next][j+1] = min(dp[next][j+1], dp[i][j] + cost);
            }
        }
    }

    int ans = INF;
    for (int i = m; i < shops.size(); i++) {
        ans = min(ans, dp[i][m]);
    }

    if (ans == INF) cout << -1 << endl;
    else cout << ans << endl;
}

signed main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    solve();
    return 0;
}
  • Time Complexity: O(N * M^2) hoặc O(Shops * M^2)
  • Space Complexity: O(N * M)

Sử dụng quy hoạch động dp[i][j] là chi phí nhỏ nhất khi mua xong j món hàng tại cửa hàng thứ i (trong danh sách các cửa hàng hợp lệ).

  • Trạng thái: dp[index][items].
  • Chuyển trạng thái: Từ cửa hàng i đã mua j hàng, chuyển sang cửa hàng next > i mua hàng thứ j+1. Chi phí thêm là (shops[next] - shops[i]) * j * k.
  • Đáp án: Giá trị nhỏ nhất của dp[i][m] với mọi i.
  • Nhược điểm: Quá chậm với N, M lên tới 200,000.
Cách Tham lam (Sliding Window)
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

int main() {
    ios_base::sync_with_stdio(0); cin.tie(0);
    int n, m, k;
    cin >> n >> m >> k;
    vector<int> a(n + 1);
    vector<int> v; // Danh sách vị trí cửa hàng có hàng
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        if (a[i] == 1) v.push_back(i);
    }

    if (v.size() < m) {
        cout << -1;
        return 0;
    }

    // Tính chi phí ban đầu cho m cửa hàng đầu tiên
    // Bắt đầu từ vị trí 1
    // Chi phí di chuyển từ 1 -> v[0]: 1 * |v[0] - 1| (chưa mua gì)
    // Chi phí di chuyển từ v[0] -> v[1]: 1 * k * |v[1] - v[0]| (đã mua 1 món)
    // ... Chi phí di chuyển từ v[i-1] -> v[i]: i * k * |v[i] - v[i-1]| (đã mua i món)

    ll current_cost = v[0] - 1; // Từ 1 đến v[0]
    ll sum_gaps = 0; // Biến để lưu tổng các khoảng cách nội bộ nhân với k (tạm tính)

    for (int i = 1; i < m; i++) {
        ll dist = v[i] - v[i-1];
        // Khi đi từ v[i-1] đến v[i], ta đã mua i món hàng
        current_cost += i * k * dist;
        // Để tối ưu hóa sliding window, ta cần tách phần có thể loại bỏ
        // Công thức: Cost = (v[0]-1) + k * [ 1*(v[1]-v[0]) + 2*(v[2]-v[1]) + ... + (m-1)*(v[m-1]-v[m-2]) ]
        // Khi dời cửa hàng đầu tiên (v[0]) ra khỏi window, ta cần bù trừ.
        // Tuy nhiên, cách tính dễ hiểu nhất là tính trực tiếp và lặp.
    }

    ll min_cost = current_cost;

    // Sliding window: loại bỏ v[i-m+1], thêm v[i]
    // Khi loại bỏ v[i-m+1] và thêm v[i], cách tính lại công thức:
    // Cost moi = Cost cu - (v[i-m+1] - 1) - k * [ 1*(v[i-m+2]-v[i-m+1]) + ... + (m-1)*(v[i]-v[i-1])? ]
    // Phân tích lại:
    // Cost = (v[0] - 1) + sum_{j=1}^{m-1} [ j * k * (v[j] - v[j-1]) ]
    // Dời window sang phải:
    // Loại bỏ v[0]: Trừ đi (v[0] - 1)
    // Giảm trọng số của các đoạn còn lại: đoạn (v[1]-v[0]) giảm từ hệ số 1 xuống 0, đoạn (v[2]-v[1]) giảm từ 2 xuống 1...
    // Thêm đoạn mới (v[m]-v[m-1]) với hệ số (m-1).

    // Công thức tối ưu:
    // current_cost ban đầu đã tính xong.
    // Khi loại bỏ v[0] và thêm v[m]:
    // Cost mới = Cost cũ - (v[0] - 1) - [k * (v[1]-v[0]) * 1] - [k * (v[2]-v[1]) * 1] - ... - [k * (v[m-1]-v[m-2]) * 1] + [k * (v[m]-v[m-1]) * (m-1)]
    // Biến đổi:
    // Cost mới = Cost cũ - (v[0] - 1) - k * [ (v[1]-v[0]) + (v[2]-v[1]) + ... + (v[m-1]-v[m-2]) ] + k * (m-1) * (v[m]-v[m-1])
    // Lưu ý: Trong Cost cũ, các đoạn này có hệ số分别为 1, 2, ..., m-1. Khi dời, hệ số giảm 1 đơn vị cho mỗi đoạn.
    // Vậy Cost mới = Cost cũ - (v[0]-1) - k * sum_{j=0}^{m-2} (v[j+1] - v[j]) + k * (m-1) * (v[m] - v[m-1])
    // Wait, cách tính này sai logic một chút. Hãy dùng lại code mẫu.

    // Logic từ code mẫu Minhnoname:
    // h là tổng các khoảng cách (v[i]-v[i-1]) * k (tính cho các đoạn bên trong window, với hệ số 1)
    // ans ban đầu = (v[0]-1) + k * [1*(v[1]-v[0]) + 2*(v[2]-v[1]) + ...]
    // Để chuyển sang window tiếp theo:
    // Loại bỏ v[0]: Trừ (v[0]-1)
    // Giảm hệ số các đoạn bên trong: trừ k * [(v[1]-v[0]) + (v[2]-v[1]) + ...] (đúng bằng h ban đầu)
    // Thêm đoạn mới (v[m]-v[m-1]): cộng k * (m-1)
    // Thêm lại khoảng cách từ 1 đến v[m+1] mới? Không, chỉ dịch chuyển window.
    // Thực tế:
    // Cost mới = Cost cũ - (v[0]-1) - h + k*(m-1)*(v[m]-v[m-1])
    // Cập nhật h: h mới = h cũ - (v[1]-v[0])*k + (v[m]-v[m-1])*k

    ll h = 0;
    for(int i=1; i<m; i++) h += (v[i] - v[i-1]) * k;

    for (int i = m; i < v.size(); i++) {
        // Tính cost mới
        ll term_new = k * (m - 1) * (v[i] - v[i-1]);
        ll cost_new = current_cost - (v[i - m] - 1) - h + term_new;

        min_cost = min(min_cost, cost_new);

        // Cập nhật current_cost và h cho lần lặp sau
        current_cost = cost_new;
        h = h - k * (v[i - m + 1] - v[i - m]) + k * (v[i] - v[i-1]);
    }

    cout << min_cost;
    return 0;
}
  • Time Complexity: O(N)
  • Space Complexity: O(N)

Bài toán có thể quy về tìm dãy M cửa hàng liên tiếp trong danh sách các cửa hàng hợp lệ sao cho chi phí là nhỏ nhất. Công thức chi phí:

  • Chi phí di chuyển từ 1 đến cửa hàng đầu tiên (v[0]): v[0] - 1.
  • Chi phí di chuyển giữa các cửa hàng trong dãy: sum_{j=1}^{M-1} (j * K * (v[j] - v[j-1])).

Ta có thể tính chi phí cho dãy M cửa hàng đầu tiên, sau đó sử dụng kỹ thuật Sliding Window (cửa sổ trượt) để dịch chuyển dãy sang phải 1 đơn vị và tính lại chi phí một cách nhanh chóng bằng cách trừ đi phần đóng góp của cửa hàng bị loại và thêm phần đóng góp của cửa hàng mới.

Cập nhật Sliding Window:

  • Cost_new = Cost_old - (v[i-M] - 1) - K * sum_of_gaps + K * (M-1) * (v[i] - v[i-1])
  • Nơi sum_of_gaps là tổng khoảng cách giữa các cặp cửa hàng liên tiếp trong window cũ (tính với hệ số 1).
  • Cập nhật lại sum_of_gaps loại bỏ khoảng đầu và thêm khoảng cuối.

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

Cách tiếp cận Time Space Tên
1 O(N * M^2) hoặc O(Shops * M^2) O(N * M) Quy hoạch động
2 O(N) O(N) Tham lam (Sliding Window)

Bài học kinh nghiệm

  • Bài toán chỉ phụ thuộc vào thứ tự các cửa hàng có hàng (B[i]=1). Ta có thể coi các vị trí này thành một mảng mới.
  • Chi phí di chuyển được tính theo cấp số nhân (1, K, 2K, 3K...). Điều này có nghĩa là chi phí cho khoảng cách giữa hai cửa hàng phụ thuộc vào vị trí tương đối của chúng trong chuỗi mua sắm.
  • Vì chi phí tăng dần theo số lượng hàng đã mua, ta muốn các khoảng cách sau này (khi đã có nhiều hàng) là nhỏ nhất có thể. Tuy nhiên, ta cũng phải bắt đầu từ vị trí 1, nên cần một giải pháp tối ưu tổng thể.
  • Sliding Window là chìa khóa: Thay vì thử tất cả các tổ hợp, ta nhận thấy rằng dãy tối ưu về mặt logic sẽ là một dãy liên tiếp trong danh sách các cửa hàng có hàng (sau khi đã loại trừ những cửa hàng không cần thiết ở đầu hoặc cuối).

Lỗi thường gặp

  • Quên xử lý trường hợp không mua đủ M hàng (in -1).
  • Lỗi tính toán trong Sliding Window: Các hệ số K, số hàng đã mua (1, 2, ..., M-1) phải được nhân chính xác vào khoảng cách tương ứng.
  • Lỗi số nguyên: N và K có thể lớn (200,000 và 10,000), tích của chúng có thể vượt quá 32-bit. Phải dùng long long (64-bit).
  • Bắt đầu tại vị trí 1: Chi phí di chuyển từ vị trí 1 đến cửa hàng đầu tiên được chọn phải được tính riêng (với hệ số 1).

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.