Hướng dẫn cho Bài 5 (TS10 Đắk Lắk 2025)


Chỉ sử dụng khi thực sự cần thiết như một cách tôn trọng tác giả và người viết hướng dẫn này.

Chép code từ bài hướng dẫn để nộp bài là hành vi có thể dẫn đến khóa tài khoản.

Tóm tắt đề bài

Cho \(N\) học sinh với kỹ năng ban đầu \(a_1, a_2, \dots, a_N\)\(M\) bài tập với độ khó \(b_1, b_2, \dots, b_M\). Một học sinh có kỹ năng \(S\) có thể làm bài tập độ khó \(x\) nếu \(S \geq x\). Sau khi làm xong, kỹ năng của học sinh tăng thêm \(x\) đơn vị. Mỗi bài tập chỉ được làm tối đa một lần. Với mỗi học sinh, hãy tìm kỹ năng tối đa có thể đạt được sau khi làm các bài tập theo thứ tự tối ưu.

Phân tích

  • Nhận xét quan trọng: Để đạt được kỹ năng cao nhất, học sinh nên làm các bài tập theo thứ tự độ khó từ thấp đến cao. Việc làm một bài tập dễ sẽ giúp kỹ năng tăng lên, từ đó có thể đủ điều kiện để làm các bài tập khó hơn.
  • Chiến thuật: Với mỗi học sinh, ta luôn chọn làm tất cả các bài tập có độ khó nhỏ hơn hoặc bằng kỹ năng hiện tại. Sau khi làm xong một nhóm bài tập, kỹ năng tăng lên, ta lại kiểm tra xem có thể làm thêm bài tập nào mới hay không. Quá trình này lặp lại cho đến khi không thể làm thêm bài tập nào nữa.
  • Ràng buộc: \(N, M \leq 5 \cdot 10^5\), kỹ năng và độ khó lên đến \(10^9\). Tổng kỹ năng có thể vượt quá giới hạn của kiểu số nguyên 32-bit, nên cần dùng kiểu long long trong C++ hoặc số nguyên lớn trong Python.

Cách làm đơn giản (Brute Force)

Ý tưởng

Với mỗi học sinh, ta sắp xếp danh sách bài tập tăng dần. Duyệt qua danh sách bài tập, nếu kỹ năng hiện tại lớn hơn hoặc bằng độ khó bài tập thì làm bài đó và tăng kỹ năng.

Độ phức tạp

  • Thời gian: \(O(N \cdot M)\)
  • Đánh giá: Với \(N, M = 5 \cdot 10^5\), độ phức tạp \(O(N \cdot M)\) sẽ lên tới \(2.5 \cdot 10^{11}\) phép tính, không thể kịp thời gian. Cách này chỉ phù hợp cho \(N, M \leq 10^3\).

Code Brute Force

C++
C++
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
    vector<long long> a(n), b(m);
    for (int i = 0; i < n; i++) cin >> a[i];
    for (int i = 0; i < m; i++) cin >> b[i];

    sort(b.begin(), b.end());

    for (int i = 0; i < n; i++) {
        long long current_skill = a[i];
        for (int j = 0; j < m; j++) {
            if (current_skill >= b[j]) {
                current_skill += b[j];
            } else {
                // Vì b đã sắp xếp, nếu không làm được b[j] thì cũng không làm được các bài sau
                break;
            }
        }
        cout << current_skill << (i == n - 1 ? "" : " ");
    }
    return 0;
}
Python
Python
import sys

def solve():
    n, m = map(int, sys.stdin.readline().split())
    a = list(map(int, sys.stdin.readline().split()))
    b = list(map(int, sys.stdin.readline().split()))

    b.sort()

    results = []
    for skill in a:
        current_skill = skill
        for difficulty in b:
            if current_skill >= difficulty:
                current_skill += difficulty
            else:
                break
        results.append(current_skill)

    print(*(results))

solve()

Hướng giải quyết (Tối ưu)

Thuật toán

Để tối ưu, ta cần tránh việc duyệt qua từng bài tập cho mỗi học sinh.

  1. Sắp xếp và Tiền xử lý:
    • Sắp xếp mảng độ khó \(b\) tăng dần.
    • Xây dựng mảng cộng dồn prefixSum của mảng \(b\) đã sắp xếp để tính nhanh tổng độ khó của một đoạn bài tập.
  2. Tìm kiếm nhị phân:
    • Với mỗi học sinh có kỹ năng ban đầu \(S\):
      • Tìm số lượng bài tập \(k\) mà học sinh có thể làm được ngay lập tức (các bài có \(b_j \leq S\)) bằng upper_bound.
      • Cập nhật kỹ năng mới: \(S = S + \text{tổng độ khó của } k \text{ bài tập đầu tiên}\).
      • Tiếp tục tìm kiếm nhị phân từ vị trí \(k\) trở đi để xem với kỹ năng mới, học sinh có làm thêm được bài tập nào không.
      • Quá trình lặp lại cho đến khi số lượng bài tập làm được không tăng thêm nữa.

Tại sao cách này nhanh?

Mỗi lần kỹ năng tăng lên và ta tìm thêm bài tập, số lượng bài tập làm được tăng lên ít nhất 1. Tuy nhiên, thực tế kỹ năng tăng rất nhanh (thường là nhảy vọt qua nhiều bài tập), nên số lần lặp while cho mỗi học sinh là rất ít (tối đa \(M\) lần nhưng trung bình rất nhỏ).

Độ phức tạp

  • Thời gian: \(O(M \log M + N \cdot K \log M)\), với \(K\) là số lần lặp tìm thêm bài tập (trong trường hợp xấu nhất \(K\) có thể lớn, nhưng với dữ liệu thực tế \(K\) thường nhỏ).
  • Bộ nhớ: \(O(N + M)\) để lưu mảng và mảng cộng dồn.

Code tham khảo

C++
C++
#include <bits/stdc++.h>
using namespace std;

int main() {
    // Tăng tốc độ nhập xuất
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

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

    vector<long long> a(n);
    for (int i = 0; i < n; i++) cin >> a[i];

    vector<long long> b(m);
    for (int i = 0; i < m; i++) cin >> b[i];

    // Sắp xếp độ khó tăng dần
    sort(b.begin(), b.end());

    // Xây dựng mảng cộng dồn để tính tổng độ khó nhanh
    vector<long long> prefixSum(m + 1, 0);
    for (int i = 0; i < m; i++) {
        prefixSum[i + 1] = prefixSum[i] + b[i];
    }

    for (int i = 0; i < n; i++) {
        long long current_skill = a[i];
        int doneCount = 0;

        while (true) {
            // Tìm số lượng bài tập có độ khó <= current_skill
            int canDoCount = upper_bound(b.begin(), b.end(), current_skill) - b.begin();

            // Nếu không làm thêm được bài tập nào mới thì dừng
            if (canDoCount == doneCount) break;

            // Cộng tổng độ khó của các bài tập mới làm được
            current_skill += (prefixSum[canDoCount] - prefixSum[doneCount]);
            doneCount = canDoCount;

            // Nếu đã làm hết tất cả bài tập thì dừng
            if (doneCount == m) break;
        }

        cout << current_skill << (i == n - 1 ? "" : " ");
    }
    cout << endl;

    return 0;
}
Python
Python
import sys
from bisect import bisect_right

def solve():
    # Sử dụng sys.stdin.read để đọc dữ liệu nhanh hơn
    input_data = sys.stdin.read().split()
    if not input_data:
        return

    n = int(input_data[0])
    m = int(input_data[1])

    a = list(map(int, input_data[2:2+n]))
    b = list(map(int, input_data[2+n:2+n+m]))

    # Sắp xếp độ khó bài tập
    b.sort()

    # Mảng cộng dồn
    prefix_sum = [0] * (m + 1)
    for i in range(m):
        prefix_sum[i+1] = prefix_sum[i] + b[i]

    results = []
    for skill in a:
        current_skill = skill
        done_count = 0

        while True:
            # Tìm vị trí bài tập cuối cùng có độ khó <= current_skill
            can_do_count = bisect_right(b, current_skill)

            if can_do_count == done_count:
                break

            # Tăng kỹ năng bằng tổng các bài tập mới làm được
            current_skill += (prefix_sum[can_do_count] - prefix_sum[done_count])
            done_count = can_do_count

            if done_count == m:
                break

        results.append(str(current_skill))

    sys.stdout.write(" ".join(results) + "\n")

if __name__ == "__main__":
    solve()

Bình luận

Mới nhất
Tải bình luận...

Không có bình luận nào.