Hướng dẫn giải của 2.Đếm sỏ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, thaocoi, khoinguyennttdz, Chi_Cuong

Hiểu bài toán

Bài toán yêu cầu tính số cách để đặt k viên sỏi vào m ô (có đánh số từ 1 đến m) sao cho:

  1. Mỗi ô có thể có 0, 1, hoặc nhiều viên sỏi.
  2. Nếu ô i có sỏi thì các ô i+1, i+2, ..., i+n-1 (n là một số nguyên dương cho trước) phải trống.

Đề bài cung cấp 3 tham số: n (khoảng cách an toàn), k (số lượng sỏi), m (số lượng ô).

Ví dụ: n=3, k=2, m=5. Nếu đặt sỏi vào ô 1, các ô 2 và 3 phải trống, chỉ được phép đặt viên thứ 2 vào ô 4 hoặc 5.

Bài toán này có thể được hiểu là tìm số cách phân phối k viên sỏi vào m ô sao cho giữa hai viên sỏi bất kỳ (theo thứ tự vị trí) phải có ít nhất n-1 ô trống.

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

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

long long n, k, m;
long long dp[1000007]; // dp[i]: số cách đặt sỏi vào các ô từ 1 đến i

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    freopen("bai2.inp", "r", stdin);
    freopen("bai2.out", "w", stdout);

    cin >> n >> k >> m;

    // Base case: không đặt viên sỏi nào
    // Tuy nhiên, ta cần xử lý cho k viên sỏi

    // Ý tư: dp[i][j] là số cách đặt j viên sỏi vào i ô
    vector<vector<long long>> dp(m + 1, vector<long long>(k + 1, 0));

    // Không đặt viên sỏi nào
    for (int i = 0; i <= m; i++) dp[i][0] = 1;

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= k; j++) {
            // Nếu ô i không có sỏi
            dp[i][j] = dp[i-1][j];
            // Nếu ô i có sỏi (đặt viên thứ j vào ô i)
            // thì các ô i-1, i-2, ..., i-n+1 phải trống
            // Ta phải lấy từ ô i-n
            if (i > n) dp[i][j] += dp[i-n][j-1];
            else if (i == n || i == 1) dp[i][j] += 1; // Đặt viên sỏi đầu tiên
            else dp[i][j] += dp[0][j-1];
        }
    }

    cout << dp[m][k];
    return 0;
}
  • Time Complexity: O(m * k)
  • Space Complexity: O(m * k)

Sử dụng quy hoạch động 2 chiều. dp[i][j] là số cách đặt j viên sỏi vào i ô.

Công thức truy hồi:

  • Nếu ô i không có sỏi: dp[i][j] = dp[i-1][j]
  • Nếu ô i có sỏi: dp[i][j] = dp[i-n][j-1] (vì cần n-1 ô trống trước đó)

Độ phức tạp: O(m*k), có thể tối ưu không gian xuống O(k) nếu dùng mảng 1 chiều.

Tuy nhiên, các solution được cung cấp cho thấy cách tiếp cận khác, tập trung vào biến đổi tổng quát của dãy số.

Cách Quy hoạch động tối ưu (theo dõi tổng)
#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e6 + 5;
int dp[N]; // dp[j]: số cách đặt sỏi vào j ô
int n, k, m;

void solve() {
    freopen("bai2.inp", "r", stdin);
    freopen("bai2.out", "w", stdout);
    cin >> n >> k >> m;

    // Base case: không có sỏi
    // Tuy nhiên, ta cần tính toán theo số lượng sỏi
    // Solution 1 và 2 cho thấy cách tính lũy kế

    // Giả sử ta dùng mảng 1D:
    // dp[i] = số cách đặt sỏi vào i ô với 1 viên sỏi
    // Công thức tổng quát: số cách đặt j viên sỏi

    // Dựa trên Solution 1:
    // Dp[i][j]: số cách đặt j viên sỏi vào i ô
    // Dp[i][1] = Dp[i-1][1] + 1 (nếu i >= 1)
    // Dp[i][2] = Dp[i-1][2] + Dp[i-n][1]

    // Code tối ưu:
    vector<int> dp(m + 1, 0);
    dp[0] = 1; // 1 cách để không đặt gì

    // Tính cho 1 viên sỏi
    for (int i = 1; i <= m; i++) {
        if (i >= n) dp[i] = dp[i-n] + dp[i-1];
        else dp[i] = dp[i-1] + 1;
    }

    // Tính cho k viên sỏi
    for (int t = 2; t <= k; t++) {
        vector<int> new_dp(m + 1, 0);
        for (int i = 1; i <= m; i++) {
            new_dp[i] = new_dp[i-1] + dp[i-n];
        }
        dp = new_dp;
    }

    cout << dp[m];
}

main() {
    solve();
    return 0;
}
  • Time Complexity: O(k * m)
  • Space Complexity: O(m)

Cách tiếp cận này tối ưu hóa việc lưu trữ bằng cách chỉ dùng mảng 1 chiều.

Quan sát thấy:

  1. Với 1 viên sỏi: Số cách đặt vào i ô bằng số cách đặt vào i-1 ô + 1 (nếu ô i có sỏi).
  2. Với j viên sỏi: Khi thêm một viên sỏi vào vị trí i, ta cần biết tổng số cách của j-1 viên sỏi ở các ô trước đó.

Các solution thực tế sử dụng biến Sum để lưu tổng tiền tố, giúp tính toán nhanh hơn:

  • Sum = tổng số cách của j-1 viên sỏi từ đầu đến hiện tại
  • Khi xét ô i, số cách mới = Sum + dp[j][i-n] (nếu có)

Đây là cách tiếp cận hiệu quả về cả thời gian và bộ nhớ.

Cách Công thức toán học (Tổng hợp)
#include <bits/stdc++.h>
#define int long long
using namespace std;

main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    freopen("bai2.inp", "r", stdin);
    freopen("bai2.out", "w", stdout);

    int n, k, m;
    cin >> n >> k >> m;

    // Solution 2 & 3 cho thấy cách tính lũy kế
    // dp[i] = số cách đặt sỏi vào i ô
    // Bắt đầu với 1 viên sỏi

    vector<int> dp(m + 1, 0);
    dp[1] = 1;
    if (m >= 2) dp[2] = 1;

    // Tính lũy kế cho 1 viên sỏi
    int sum = dp[1] + dp[2];
    for (int i = 3; i <= m; i++) {
        dp[i] = sum;
        sum += dp[i];
    }

    // Với k viên sỏi, ta lặp lại quá trình
    // Tăng dp[1], dp[2] thêm 1 mỗi lần thêm sỏi
    // Tính tổng mới cho các ô tiếp theo

    int current_sum;
    for (int t = 2; t <= k; t++) {
        dp[1]++;
        dp[2]++;
        current_sum = dp[1] + dp[2];
        for (int i = 3; i <= m; i++) {
            dp[i] = current_sum;
            current_sum += dp[i];
        }
    }

    cout << dp[m];
    return 0;
}
  • Time Complexity: O(k * m)
  • Space Complexity: O(m)

Đây là cách tiếp cận được dùng trong các solution accepted.

Nhận xét:

  • dp[i] là số cách đặt sỏi vào i ô.
  • Với 1 viên sỏi: dp[i] = dp[i-1] + dp[i-2] + ... + dp[i-n] (với điều kiện base).
  • Các solution sử dụng biến Sum để tính nhanh:
    • dp[i] = Sum + dp[i-n] (nếu i > n)
    • Sum = Sum + dp[i]

Cách này bỏ qua việc phân biệt số lượng sỏi trực tiếp, mà tính toán dựa trên sự lan truyền của các cách đặt.

Ý tưởng chính:

  • Đặt 1 viên sỏi vào ô 1 hoặc 2 (base).
  • Với mỗi viên sỏi tiếp theo, các cách đặt sẽ "tăng" lên.
  • dp[i] có thể được tính bằng tổng của các dp trước đó.

Đây là biến thể của bài toán "Đặt các quân cờ sao cho không trùng nhau".

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

Cách tiếp cận Time Space Tên
1 O(m * k) O(m * k) Quy hoạch động cơ bản
2 O(k * m) O(m) Quy hoạch động tối ưu (theo dõi tổng)
3 O(k * m) O(m) Công thức toán học (Tổng hợp)

Bài học kinh nghiệm

  • Bài toán có thể biến đổi thành bài toán quy hoạch động với công thức tổng quát: dp[i] = sum(dp[i-n]) hoặc tương đương.
  • Sử dụng biến Sum để lưu tổng tiền tố giúp giảm độ phức tạp từ O(n) xuống O(1) cho mỗi bước tính toán.
  • Với k viên sỏi, ta có thể tính lũy kế: bắt đầu từ 1 viên, sau đó với mỗi viên thêm vào, cập nhật mảng dp từ đầu.
  • Cấu trúc dữ liệu mảng 1 chiều là đủ, không cần mảng 2 chiều.

Lỗi thường gặp

  • Quên xử lý các trường hợp đặc biệt khi i < n (ví dụ: i=1, i=2).
  • Sử dụng int (32-bit) thay vì long long (64-bit) dẫn đến tràn số vì kết quả có thể rất lớn.
  • Lỗi truy cập mảng ngoài biên nếu không kiểm tra điều kiện i >= n trước khi truy cập dp[i-n].
  • Điều chỉnh sai công thức lũy kế Sum, dẫn đến tính toán sai số cách.

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.