Hướng dẫn giải của Tập chạ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, Minhnoname, khoikhoi123, hieuochimchim

Editorial for cowrun: Tập chạy

Hiểu bài toán

Bessie có N phút để chạy trên đường chạy. Cô có thể chọn chạy hoặc nghỉ mỗi phút. Ban đầu thể lực (energy) là M.

  • Nếu chạy: được D_k mét, thể lực giảm 1 (không được xuống dưới 0).
  • Nếu nghỉ: thể lực tăng 1 (tối đa M).
  • Quy tắc đặc biệt: Khi Bessie bắt đầu nghỉ, cô chỉ có thể tiếp tục chạy khi thể lực trở lại M (tức là phải nghỉ đến khi full máu mới được chạy lại).
  • Kết thúc: Sau N phút, thể lực phải bằng M. Mục tiêu: Tìm quãng đường dài nhất có thể chạy được.

Ví dụ: N=5, M=2, D=[5,3,4,2,10].

  • Phút 1: Chạy (Energy 2->1), đc 5m.
  • Phút 2: Nghỉ (Energy 1->2), về M.
  • Phút 3: Chạy (Energy 2->1), đc 4m.
  • Phút 4,5: Nghỉ (Energy 1->2) Tổng: 5+4=9.

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

Cách Quy hoạch động trạng thái (DP State)
#include <bits/stdc++.h>
using namespace std;

int n, m;
int a[10005];
// f[i][j][lock]: quãng đường dài nhất tại phút i,
// với thể lực j, và trạng thái 'lock' (0: có thể chạy, 1: đang nghỉ/must full)
int f[10005][505][2];

int main() {
    ios_base::sync_with_stdio(0); cin.tie(0);
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> a[i];

    // Khởi tạo
    memset(f, -1, sizeof(f));
    f[0][m][0] = 0;

    for(int i = 0; i < n; i++) {
        for(int j = 0; j <= m; j++) {
            for(int lock = 0; lock < 2; lock++) {
                if (f[i][j][lock] == -1) continue;
                int current_val = f[i][j][lock];

                // 1. Chọn NGHỈ
                // Luôn được phép nghỉ
                int next_energy = min(m, j + 1);
                int next_lock = lock;
                // Nếu nghỉ mà full năng lượng hoặc đang full rồi thì hết khóa
                if (next_energy == m) next_lock = 0;

                f[i+1][next_energy][next_lock] = max(f[i+1][next_energy][next_lock], current_val);

                // 2. Chọn CHẠY
                // Chỉ được chạy nếu energy > 0 và KHÔNG bị khóa (lock == 0)
                if (j > 0 && lock == 0) {
                    int next_e_run = j - 1;
                    // Lock vẫn giữ là 0 (vẫn có thể chạy tiếp)
                    f[i+1][next_e_run][0] = max(f[i+1][next_e_run][0], current_val + a[i+1]);
                }
            }
        }
    }

    // Kết quả là giá trị tốt nhất tại phút N, thể lực M, lock = 0
    cout << f[n][m][0] << endl;
    return 0;
}
  • Time Complexity: O(N * M)
  • Space Complexity: O(N * M)

Cách tiếp cận trực tiếp nhất là mô phỏng quy tắc bằng DP.

  • Trạng thái f[i][j][lock]: Lưu quãng đường dài nhất sau i phút, với j thể lực, và lock là c cờ báo đang bị khóa (1) hay không (0).
  • Transition:
    1. Nghỉ: Luôn được phép. Tăng thể lực, reset lock về 0 nếu đạt M.
    2. Chạy: Chỉ được nếu j > 0lock == 0. Trừ thể lực, cộng quãng đường. Lock vẫn là 0.
  • Độ phức tạp: O(N * M), khá nhỏ (10^4 * 500 = 5*10^6) và chấp nhận được.
Cách Tối ưu hóa DP (Dùng mảng 2 chiều)
#include <stdio.h>
#include <string.h>

#define MAXN 10005
#define MAXM 505
#define NEG_INF -1000000000

int N, M;
int D[MAXN];
int dp[2][MAXM][2]; // dp[time%2][energy][lock]

static inline int max(int a, int b) { return a > b ? a : b; }

int main() {
    if (scanf("%d %d", &N, &M) != 2) return 0;
    for (int i = 1; i <= N; i++) scanf("%d", &D[i]);

    // Khởi tạo
    for (int e = 0; e <= M; e++) dp[0][e][0] = dp[0][e][1] = NEG_INF;
    dp[0][M][0] = 0;

    for (int t = 0; t < N; t++) {
        int cur = t & 1, nxt = cur ^ 1;
        for (int e = 0; e <= M; e++) dp[nxt][e][0] = dp[nxt][e][1] = NEG_INF;

        for (int e = 0; e <= M; e++) {
            for (int lock = 0; lock <= 1; lock++) {
                int val = dp[cur][e][lock];
                if (val == NEG_INF) continue;

                // 1. Nghỉ
                int e1 = e + 1; if (e1 > M) e1 = M;
                int lock1 = (e1 == M) ? 0 : 1; // Nếu full năng lượng thì hết khóa
                // Nếu đang lock=1, nghỉ vẫn là lock=1 (trừ khi full M)
                // Nếu đang lock=0, nghỉ vẫn lock=0 (trừ khi full M)
                // Thực chất lock1 chỉ phụ thuộc năng lượng mới
                dp[nxt][e1][lock1] = max(dp[nxt][e1][lock1], val);

                // 2. Chạy
                if (e > 0 && lock == 0) {
                    int e2 = e - 1;
                    // lock vẫn là 0
                    dp[nxt][e2][0] = max(dp[nxt][e2][0], val + D[t+1]);
                }
            }
        }
    }

    printf("%d\n", dp[N&1][M][0]);
    return 0;
}
  • Time Complexity: O(N * M)
  • Space Complexity: O(M)

Bản chất giống Approach 1 nhưng tối ưu bộ nhớ.

  • Thay vì dùng mảng 3 chiều to lớn, dùng滚动数组 (duyệt mảng 2 chiều luân phiên).
  • dp[cur][e][lock] tính cho phút hiện tại.
  • dp[nxt][...] tính cho phút tiếp theo.
  • Logic xử lý 'lock' hơi tinh tế: Khi nghỉ và đạt M năng lượng, lock tự động về 0. Nếu chưa đạt M, lock giữ nguyên (tức là 1 nếu đang nghỉ, 0 nếu đang chạy rồi nghỉ vội).
  • Space: O(M) thay vì O(N*M).
Cách Cách tiếp cận 'Gộp đoạn' (Segment / Block DP)
#include <bits/stdc++.h>
using namespace std;
#define int long long

signed main() {
  int n, m;
  cin >> n >> m;
  vector<int> a(n + 1, 0);
  vector<int> pre(n + 1, 0);
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
    pre[i] = pre[i - 1] + a[i];
  }
  // dp[i]: quãng đường dài nhất kết thúc ở phút i và thể lực đầy (M)
  vector<int> dp(n + 1, 0);

  for (int i = 0; i <= n; i++) {
    // Option 1: Tạm nghỉ ở đây (không chạy thêm)
    dp[min(n, i + 1)] = max(dp[min(n, i + 1)], dp[i]);

    // Option 2: Chạy một đoạn, rồi nghỉ
    // Chạy j phút (tốn j thể lực), nghỉ j phút (lấy lại j thể lực)
    // Tổng thời gian: 2*j. Tổng thể lực giữ nguyên.
    // Điều kiện: i + 2*j <= n, j <= m
    int maxj = min(m, (n - i) / 2);
    for (int j = 1; j <= maxj; j++) {
      // Chạy từ i+1 đến i+j
      // Thời gian kết thúc: i + 2*j
      // Thể lực về M
      dp[i + 2 * j] = max(dp[i + 2 * j], dp[i] + pre[i + j] - pre[i]);
    }
  }
  cout << dp[n] << endl;
  return 0;
}
  • Time Complexity: O(N * M)
  • Space Complexity: O(N)

Cách này dựa trên quan sát:

  • Chu kỳ hoạt động lý tưởng là: Chạy liên tục cho đến khi cạn thể lực, sau đó nghỉ liên tục cho đến khi đầy lại.
  • Do yêu cầu cuối cùng phải đầy thể lực, ta có thể coi các hoạt động là các khối (Block):
    1. Nghỉ không giới hạn (nếu đang full).
    2. Chạy j phút (tốn j năng lượng) + Nghỉ j phút (lấy lại j năng lượng). Tổng thời gian 2j.
  • DP[i] là trạng thái tại thời điểm i (đang full năng lượng).
  • Từ DP[i], ta có thể:
    • Chuyển sang DP[i+1] bằng cách nghỉ (không chạy gì).
    • Chuyển sang DP[i + 2*j] bằng cách chọn chạy j phút (tích lũy D[i+1...i+j]) rồi nghỉ j phút.
  • Đặt điều kiện j <= M để đảm bảo không bao giờ chạy quá năng lượng tối đa có thể tích lũy (vì nghỉ chỉ lấy lại tối đa M).
  • Độ phức tạp: Vòng lặp i chạy từ 0 đến n, vòng lặp j chạy tối đa M/2. Tổng O(N*M).

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

Cách tiếp cận Time Space Tên
1 O(N * M) O(N * M) Quy hoạch động trạng thái (DP State)
2 O(N * M) O(M) Tối ưu hóa DP (Dùng mảng 2 chiều)
3 O(N * M) O(N) Cách tiếp cận 'Gộp đoạn' (Segment / Block DP)

Bài học kinh nghiệm

  • Quy tắc 'bắt đầu nghỉ phải nghỉ đến khi full' có thể được xử lý bằng state 'lock' (đang bị khóa) hoặc bằng cách chia bài toán thành các khối 'Chạy - Nghỉ' (Block).
  • Bài toán yêu cầu thể lực cuối cùng là M, nên các khối hoạt động có thể được coi là độc lập về mặt năng lượng (vì sau mỗi khối chạy rồi nghỉ, năng lượng về lại M).
  • Với N <= 10000 và M <= 500, thuật toán O(N*M) là tối ưu và an toàn.

Lỗi thường gặp

  • Quên kiểm tra điều kiện năng lượng > 0 khi chạy.
  • Xử lý sai logic 'lock': Nếu đang lock (đang nghỉ) mà nghỉ tiếp thì vẫn lock, chỉ reset lock khi đạt M.
  • Quên ràng buộc 'không được chạy quá M' (dù năng lượng có thể tích lũy được là M, nhưng trong một khối chạy, ta không thể chạy quá M phút liên tục vì sẽ cạn năng lượng trước).

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.