Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học

Chia Mảng (CSES Array Division)

Chia Mảng (CSES Array Division)
Nguồn: CSES – Array Division Độ khó: Khá Chặt nhị phân đáp án Tham lam

Đề bài

Bạn được cho một mảng chứa $n$ số nguyên dương. Nhiệm vụ của bạn là chia mảng thành $k$ đoạn con sao cho tổng lớn nhất trong các đoạn con nhỏ nhất có thể.

Input

  • Dòng đầu vào đầu tiên chứa hai số nguyên $n$ và $k$: kích thước của mảng và số lượng đoạn con trong cách chia.
  • Dòng tiếp theo chứa $n$ số nguyên $x_1, x_2, \ldots, x_n$: nội dung của mảng.

Output: in một số nguyên: tổng lớn nhất của một đoạn con trong cách chia tối ưu.

Constraints: $1 \le n \le 2 \cdot 10^5$, $1 \le k \le n$, $1 \le x_i \le 10^9$.

Note: một cách chia tối ưu của ví dụ là $[2, 4], [7], [3, 5]$, trong đó tổng của các đoạn con là $6, 7, 8$. Tổng lớn nhất là tổng cuối cùng $8$.

Ví dụ đề bài – input
5 3
2 4 7 3 5
Ví dụ đề bài – output
8
Test thêm 1 – input
5 2
1 5 2 2 3
Test thêm 1 – output
7
Test thêm 2 – input
5 3
1 5 2 2 3
Test thêm 2 – output
6

Hai test thêm là anh đưa ra để em hiểu rõ hơn: test 1 chia thành $[1, 5]$ và $[2, 2, 3]$, nhóm $[2, 2, 3]$ có tổng lớn nhất là 7; test 2 chia thành $[1, 5]$, $[2, 2]$ và $[3]$, nhóm $[1, 5]$ có tổng lớn nhất là 6.

Lời mở đầu

Bài này mình được một em học viên hỏi và mình đã hướng dẫn chi tiết cách thức giải quyết cho em ấy. Tin rằng sẽ có giá trị với nhiều bạn nên mình chia sẻ đề bài để các bạn thử sức làm, kèm toàn bộ phần hướng dẫn để bạn nào không giải quyết được thì có thể tham khảo nhen. Hi vọng mang đến được giá trị cho các bạn 😇. Chúc các bạn học tốt và gặt hái nhiều thành công.

Dành cho tất cả mọi ngườiNộp bài trên LQDOJ

Link này công khai, bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Chưa có tài khoản thì tạo tài khoản rồi đăng nhập nộp bài thử hen.

Nộp bài trên LQDOJ

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

Đọc kỹ đề bài, đặc biệt là xem phần ví dụ, thì chắc em đã nhận ra khái niệm "đoạn con" mà đề bài nói chính là "đoạn con liên tiếp" nhé.

Kiến thứcChặt nhị phân đáp án là gì?

Bài này dùng kỹ thuật tìm kiếm nhị phân trên đáp án, tiếng Anh là Binary search the answer, dân trong ngành còn gọi là chặt nhị phân đáp án.

Giả sử em đã biết miền đáp án nằm trong một đoạn từ Left đến Right. Thay vì cứ thử lần lượt từ Left đến Right sẽ rất lâu, em thử giả định nó nằm ở giữa, sau đó kiểm tra: nếu xác định đáp án nằm ở nửa bên phải thì tìm tiếp nửa bên phải, nằm ở nửa bên trái thì tìm tiếp nửa bên trái. Lặp lại quy trình cho đến khi tìm được hoặc không tìm được.

Cũng giống như em đọc sách: cần tìm một trang nào đó thì thay vì lật từ đầu đến cuối, em lật ở giữa, dựa vào số trang để biết trang cần tìm nằm ở nửa bên phải hay nửa bên trái, rồi lại tìm tiếp như vậy 📖.

Ý tưởngÁp dụng vào bài này

Với test 1: dãy 1 5 2 2 3 và $K = 2$.

  • Nếu chỉ chia $K = 1$ đoạn thì tổng lớn nhất là cả dãy: $1 + 5 + 2 + 2 + 3 = 13$. Đây là đáp án lớn nhất có thể.
  • Nếu chia $K = N$ đoạn (mỗi phần tử một đoạn) thì tổng lớn nhất là phần tử lớn nhất $= 5$. Đây là đáp án nhỏ nhất có thể.

Vậy xác định $L = 5$, $R = 13$, đáp án nằm trong đoạn này.

Kiểm tra một giá trị X có làm đáp án được không?
Gọi $X$ là "tổng lớn nhất cho phép" của mỗi đoạn. Ta duyệt dãy $O(N)$, tham lam nhét càng nhiều phần tử vào đoạn hiện tại càng tốt, khi nào thêm phần tử tiếp theo mà tổng vượt $X$ thì mới cắt sang đoạn mới. Đếm xem cần bao nhiêu đoạn:
• Cần ≤ K đoạn → $X$ thoả mãn (ít hơn $K$ cũng được, vì muốn đủ đúng $K$ đoạn thì cắt thêm vài chỗ, tổng các đoạn chỉ nhỏ đi).
• Cần > K đoạn → $X$ quá nhỏ, không được.

Thử $M = (L + R) / 2 = 18 / 2 = 9$. Với dãy 1 5 2 2 3 chia được $[1, 5, 2]$ và $[2, 3]$: đoạn đầu dừng ở 1 5 2, vì lấy thêm 2 nữa thì tổng thành 10 > 9. Được 2 đoạn ≤ K nên 9 thoả mãn. Nhưng ta cần đáp án nhỏ nhất nên chưa vội lấy 9, tìm tiếp sang trái: $R = M - 1 = 8$.

$L = 5$, $R = 8$ → $M = 6$: chia ra $[1, 5], [2, 2], [3]$, cần tới 3 đoạn > K = 2, không được. Phải tìm lên: $L = M + 1 = 7$.

$L = 7$, $R = 8$ → $M = 7$: chia ra $[1, 5], [2, 2, 3]$, được 2 đoạn, lấy được. Thử nhỏ hơn: $R = 6$, lúc này $L > R$ nên dừng. Đáp án là 7.

Tổng quát chi phí là $O(N \cdot \log(\text{sum} - \max a_i))$.

Chạy tayChạy tay với dãy 2 7 4 5 10 9 8, K = 3

Sau đó em ấy nhờ mình chạy tay quy trình với test 2 7 4 5 10 9 8, $K = 3$. Ta có $sum = 45$, $Max = 10$, nên $left = 10$, $right = 45$:

[L, R]midChia tham lam (tổng mỗi đoạn ≤ mid)Số đoạnSo với K = 3
[10, 45]27[2, 7, 4, 5] | [10, 9, 8]2✔ nhận, R = 26
[10, 26]18[2, 7, 4, 5] | [10] | [9, 8]3✔ nhận, R = 17
[10, 17]13[2, 7, 4] | [5] | [10] | [9] | [8]5✘ loại, L = 14
[14, 17]15[2, 7, 4] | [5, 10] | [9] | [8]4✘ loại, L = 16
[16, 17]16[2, 7, 4] | [5, 10] | [9] | [8]4✘ loại, L = 17
[17, 17]17[2, 7, 4] | [5, 10] | [9, 8]3✔ nhận, R = 16
Đáp án (in ra L)17

Khi $left > right$ thì dừng, in ra $left$ là kết quả, đáp án là 17: chia được $[2, 7, 4]$, $[5, 10]$, $[9, 8]$, tổng lần lượt 13, 15, 17.

Minh hoạTự chạy từng bước

Em nhập dãy và $K$ (tối đa 14 phần tử) rồi bấm Tiếp. Mỗi màu là một đoạn trong cách chia tham lam với giá trị mid đang thử:

Ví dụ nhanh:

CodeSource code

Source code giải quyết theo tư tưởng anh đã trình bày ở trên và cũng đã nộp thành công trên web, em xem để được hiểu rõ hơn:

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

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n, m;
    cin >> n >> m;
    vector<long long> a(n + 1);
    long long sum = 0;
    long long Max = LLONG_MIN;
    for(int i = 1; i <= n; ++i){
        cin >> a[i];
        sum += a[i];
        Max = max(Max, a[i]);
    }
    if(m == 1){
        cout << sum;
        return 0;
    }
    if(m == n){
        cout << Max;
        return 0;
    }
    long long left = Max, right = sum;
    while(left <= right){
        long long mid = (left + right) / 2;
        int k = 1;
        long long s = 0;
        for(int i = 1; i <= n; ++i){
            if(s + a[i] <= mid){
                s += a[i];
            }
            else{
                s = a[i];
                k++;
            }
        }
        if(k <= m){
            right = mid - 1;
        }
        else{
            left = mid + 1;
        }
    }
    cout << left;
    return 0;
}
import sys
input = sys.stdin.readline

n, m = map(int, input().split())
a = [0] + list(map(int, input().split()))
total_sum = sum(a[1:])
Max = max(a[1:])

if m == 1:
    print(total_sum)
elif m == n:
    print(Max)
else:
    left = Max
    right = total_sum
    while left <= right:
        mid = (left + right) // 2
        k = 1
        s = 0
        for i in range(1, n + 1):
            if s + a[i] <= mid:
                s += a[i]
            else:
                s = a[i]
                k += 1
        if k <= m:
            right = mid - 1
        else:
            left = mid + 1
    print(left)
C++▶ Chạy tay từng dòng codeChặt nhị phân đáp án trong [Max, sum], mỗi lần kiểm tra mid bằng tham lam O(N).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 ios::sync_with_stdio(0);
6 cin.tie(0);
7 int n, m;
8 cin >> n >> m;
9 vector<long long> a(n + 1);
10 long long sum = 0;
11 long long Max = LLONG_MIN;
12 for(int i = 1; i <= n; ++i){
13 cin >> a[i];
14 sum += a[i];
15 Max = max(Max, a[i]);
16 }
17 if(m == 1){
18 cout << sum;
19 return 0;
20 }
21 if(m == n){
22 cout << Max;
23 return 0;
24 }
25 long long left = Max, right = sum;
26 while(left <= right){
27 long long mid = (left + right) / 2;
28 int k = 1;
29 long long s = 0;
30 for(int i = 1; i <= n; ++i){
31 if(s + a[i] <= mid){
32 s += a[i];
33 }
34 else{
35 s = a[i];
36 k++;
37 }
38 }
39 if(k <= m){
40 right = mid - 1;
41 }
42 else{
43 left = mid + 1;
44 }
45 }
46 cout << left;
47 return 0;
48}

Hai trường hợp đặc biệt được xử lý trước cho nhanh: $K = 1$ thì đáp án là tổng cả dãy, $K = N$ thì đáp án là phần tử lớn nhất. Chú ý dùng long long vì tổng có thể tới $2 \cdot 10^5 \times 10^9 = 2 \cdot 10^{14}$, vượt kiểu int.

Không gian$O(N)$
Thời gian$O(N \log(\text{sum}))$

Mình đã kiểm tra cả 2 bản code với 400 bộ test ngẫu nhiên so với cách vét cạn mọi cách chia, đều cho cùng kết quả. Với $n = 2 \cdot 10^5$, bản C++ chạy khoảng 0.02 giây; bản Python khoảng 1.3 giây trên máy mình, có thể sát giới hạn thời gian nên các bạn nộp bằng C++ cho chắc nhé.

Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇

Chặt Cây Xây Nhà

Chặt Cây Xây Nhà
Nguồn: Codeforces Group Độ khó: Trung bình Tìm kiếm nhị phân Chặt nhị phân kết quả

Đề bài

Bob là 1 thợ mộc tài hoa. Hiện tại anh đang có kế hoạch xây dựng một căn nhà toàn bộ bằng gỗ. Biệt thự được xây bởi các khúc gỗ khác nhau và tổng độ dài của các thanh gỗ là $L$.

Để lấy các khúc gỗ đó, anh ấy cần vào rừng và chặt cây. Khu rừng gần chỗ anh có $N$ cây với độ cao khác nhau. Bob sẽ dùng một cái máy cưa đặc biệt, nó có thể cắt một lượt qua tất cả các cây.

Đầu tiên, anh ấy sẽ chọn một độ cao cố định là $H$, sau đó dùng máy cưa đặc biệt để cắt một đường theo độ cao $H$ này trên các cây có độ cao lớn hơn $H$.

Ví dụ: các cây có độ cao lần lượt là 10, 16, 17, 15. Chọn $H = 15$. Do đó, cây thứ 2, 3 sẽ được cắt và tổng độ dài các khúc gỗ thu được là $1 + 2 = 3$.

Cho độ cao của các cây trong rừng và giá trị $L$ (tổng độ dài của các khúc gỗ cần). Hãy giúp Bob tìm giá trị lớn nhất của $H$ thoả mãn rằng Bob chỉ cần dùng máy cắt đúng một lượt để thu được số các khúc gỗ cần thiết.

Chú ý: tổng độ dài các khúc gỗ thoả mãn có thể lớn hơn $L$ (hay nói cách khác $L \le$ tổng độ dài).

Input: dòng đầu tiên chứa một số nguyên mô tả số lượng cây $N$ $(1 \le N \le 10^6)$ và $L$ là tổng độ dài của các khúc gỗ cần dùng $(1 \le L \le 2 \cdot 10^9)$. Dòng tiếp theo là độ cao tương ứng của các cây $h[i]$ $(h[i] < 10^9)$. Tổng độ cao của các cây lớn hơn hoặc bằng $L$.

Output: dòng duy nhất gồm 1 số nguyên duy nhất: giá trị độ cao lớn nhất mà tại đó Bob cắt đúng một đường để thu được các khúc gỗ thoả mãn.

Giới hạn

  • 50% số test đầu tiên có $h[i] \le 10^3$, $N \le 10^3$.
  • 50% số test còn lại không có giới hạn gì thêm.
Ví dụ 1 – input
3 6
1 2 3
Ví dụ 1 – output
0
Ví dụ 2 – input
4 10
10 15 12 13
Ví dụ 2 – output
10
Ví dụ 3 – input
4 1
5 5 5 5
Ví dụ 3 – output
4

Lời mở đầu

Bài này mình được một bạn học viên hỏi nên sẵn tiện mình viết bài giải chi tiết để nhiều bạn được tham khảo luôn. Hy vọng mang đến được giá trị cho các bạn 😇.

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.

Nộp bài trên CLUE OJ
Dành cho tất cả mọi ngườiNộp bài trên Codeforces

Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Chưa có tài khoản thì tạo tài khoản rồi đăng nhập nộp bài thử hen.

Nộp bài trên Codeforces

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

Hiểu đềTóm tắt đề bài

Trước tiên mình sẽ tóm tắt lại đề bài một cách đơn giản nhất để bạn nào chưa hiểu như sau: cho $N$ cây gỗ, $L$ là tổng độ dài các khúc gỗ cần dùng, mảng $h$ gồm $N$ phần tử với $h[i]$ là độ cao của cây thứ $i$. Ta có chiếc máy cưa đặc biệt: khi chọn 1 độ cao cố định $H$, nó lướt qua tất cả các cây, và những cây có độ cao $> H$ sẽ cho $h[i] - H$ khúc gỗ.

Yêu cầu
Tìm giá trị $H$ lớn nhất có thể nhưng vẫn thu được lượng gỗ $\ge L$ (thu thừa gỗ cũng không sao). Nói cách khác: cố gắng thu được lượng gỗ ít nhất có thể sao cho lượng gỗ đó vẫn $\ge L$.

Ví dụ: $N = 4$, $L = 10$, $h = \{10, 15, 12, 13\}$. Thì $H$ lớn nhất ta tìm được sẽ bằng 10: cây thứ nhất có $h[1] \le H$ nên không thu được gỗ, cây thứ 2 thu được $15 - 10 = 5$, cây thứ 3 thu được $12 - 10 = 2$, cây thứ 4 thu được $13 - 10 = 3$. Tổng cộng $5 + 2 + 3 = 10$ khúc gỗ, thoả mãn $\ge L$.

Em tự tay kéo thanh trượt $H$ dưới đây để thấy máy cưa "lướt" qua các cây như thế nào nhé. Phần màu cam phía trên đường cưa chính là gỗ thu được:

Tối đa 12 cây, mỗi cây cao từ 1 đến 60 để hình vẽ dễ nhìn.
H

50%Cách vét cạn: thử H từ trên xuống

Giờ cách làm đơn giản nhất của bài này là vét cạn thôi. Ta chắc chắn rằng $H$ sẽ nằm trong khoảng từ 0 đến độ cao cây gỗ cao nhất trừ 1 (ta gọi là hMax - 1 nha). Nhiều bạn sẽ nghĩ tới việc cho $H$ chạy từ 0 lên hMax - 1, nhưng bởi vì ta đang cần tìm $H$ lớn nhất, tại sao ta không thử chạy ngược lại, từ hMax - 1 về 0? Khi đó, lần đầu tiên ta gặp một giá trị thoả mãn số gỗ $\ge L$ là ta kết luận luôn đó là $H$ lớn nhất rồi.

Ví dụ $N = 4$, $L = 10$, $h = \{10, 15, 12, 13\}$, $hMax = 15$, ta cho $H$ chạy từ 14 về 0:

HSố gỗ thu đượcSo với L = 10
141 = 1✘ nhỏ hơn L
132 = 2✘ nhỏ hơn L
123 + 1 = 4✘ nhỏ hơn L
114 + 1 + 2 = 7✘ nhỏ hơn L
105 + 2 + 3 = 10✔ ≥ L, chọn!

Em nhập $L$ và độ cao các cây rồi bấm Tiếp để xem cách vét cạn hạ dần đường cưa $H$ từ cây cao nhất xuống, tới lần đầu tiên thu đủ gỗ thì dừng:

Ví dụ nhanh:

Mặc dù về độ phức tạp thời gian thì chạy từ trên xuống hay từ dưới lên là như nhau, nhưng ta chọn chạy từ trên xuống vì chỉ cần gặp trường hợp đầu tiên thoả mãn thì kết luận được ngay.

Đây là source code cho bạn nào muốn tham khảo cách làm trên của mình nha:

Xem code vét cạn (50% điểm)
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    ll N, L;
    cin >> N >> L;
    vector<ll> h(N);
    ll hMax = 0;
    for(int i = 0; i < N; ++i) {
        cin >> h[i];
        hMax = max(hMax, h[i]);
    }
    for(int H = hMax; H >= 0; --H) {
        ll sum = 0;
        for(int i = 0; i < N; ++i) {
            if(h[i] > H) {
                 sum += h[i] - H;
            }
        }
        if(sum >= L) {
            cout << H;
            break;
        }
    }
    return 0;
}
N, L = map(int, input().split())
h = list(map(int, input().split()))
hMax = max(h)
for H in range(hMax, -1, -1):
    s = 0
    for x in h:
        if x > H:
            s += x - H
    if s >= L:
        print(H)
        break
C++▶ Chạy tay từng dòng codeVét cạn: thử H từ hMax xuống, gặp H đầu tiên thu đủ gỗ thì in ra.
Dữ liệu mẫu:
1#include<bits/stdc++.h>
2#define endl '\n'
3using namespace std;
4typedef long long ll;
5
6int main() {
7 ios_base::sync_with_stdio(0);
8 cin.tie(0);
9 ll N, L;
10 cin >> N >> L;
11 vector<ll> h(N);
12 ll hMax = 0;
13 for(int i = 0; i < N; ++i) {
14 cin >> h[i];
15 hMax = max(hMax, h[i]);
16 }
17 for(int H = hMax; H >= 0; --H) {
18 ll sum = 0;
19 for(int i = 0; i < N; ++i) {
20 if(h[i] > H) {
21 sum += h[i] - H;
22 }
23 }
24 if(sum >= L) {
25 cout << H;
26 break;
27 }
28 }
29 return 0;
30}

Giải thích một số chỗ trong code

  1. ios::sync_with_stdio(0); cin.tie(0); 2 dòng đầu tiên của hàm main sẽ giúp đẩy nhanh tốc độ nhập xuất dữ liệu nếu các bạn đang code theo ngôn ngữ C++ (để hiểu vì sao thì các bạn cứ gõ mấy từ này lên Google là sẽ ra rất nhiều bài viết giải thích nhé), nên kinh nghiệm trong các bài mà cần nhập xuất nhiều, ví dụ như bài này dữ liệu đầu vào có thể lên đến $10^6$ số, thì ta hãy nên để 2 dòng này vào để chạy nhanh hơn. Tất nhiên nếu các bạn code theo cú pháp của C thì chẳng cần mấy dòng đó làm gì, tuy nhiên thường khi code các bài lập trình thi đấu thế này ta dùng C++ vì cú pháp ngắn gọn cũng như nhiều hỗ trợ của nó, vậy nên bạn nào code C++ thì nhớ thêm 2 dòng này vào đầu hàm main nhé.
  2. vector<ll> h(N): ta khai báo vector $h$ có $N$ phần tử để tiện việc đọc, chỉ việc cin >> h[i].
Không gian$O(N)$
Thời gian$O(hMax \cdot N)$

Ta dễ dàng nhận thấy cách trên không thể nào AC được. Trong trường hợp xấu nhất $hMax = 10^9$, $N = 10^6$, và muốn chặt hết toàn bộ các cây ta cần chặt tận gốc, $H = 0$ chẳng hạn, thì sẽ chạy tận $10^9 \times 10^6 = 10^{15}$ phép tính. Thời gian cần lên tới $10^{15} / 10^8 = 10^7$ giây, 10 triệu giây là tầm hơn 100 ngày đó 😱.

Tuy nhiên hãy vẫn nên cài đặt nó để sau này lấy làm công cụ kiểm tra tính chính xác cho cách làm tối ưu. Bạn nào chưa biết cách lấy nó làm công cụ kiểm tra thì xem bài viết Hạn chế lỗi Wrong Answer của mình nha.

Ghi chú viết tắt: AC (Accepted – được chấp thuận), TLE (Time Limit Exceeded – quá thời gian), WA (Wrong Answer – sai kết quả).

100%Cách tối ưu: chặt nhị phân kết quả

Để AC bài này ta dùng giải thuật tìm kiếm nhị phân trên kết quả (còn gọi là chặt nhị phân kết quả, hay gọi tắt là chặt nhị phân). Để áp dụng được, ta cần thoả mãn 3 điều sau:

1Tập kết quả là gì?Quá rõ ràng rồi: $0 \le H \le hMax - 1$.
2Biết H thì kiểm tra được không?Có: chạy 1 vòng for qua mảng $h$, đếm số gỗ thu được rồi so với $L$.
3Biết 1 giá trị H đúng hoặc sai thì cả một phía cũng đúng hoặc sai theoĐiều này nghe hơi khó hiểu, xem ví dụ ngay dưới nhé.

Ví dụ $N = 4$, $L = 13$, $h = \{10, 15, 12, 13\}$. Bảng dưới là số gỗ thu được với từng giá trị $H$:

Hgỗ
050
146
242
338
434
530
626
722
818
914
1010
117
124
132
141
✔ gỗ ≥ 13 (thoả mãn)✘ gỗ < 13 (không thoả mãn)đáp án: ô ✔ cuối cùng
  1. Thử $H = 7$: số gỗ $= 22 \ge L$, thoả mãn. Mọi giá trị $H \le 7$ chắc chắn cũng thoả mãn, vì H càng xuống thì gỗ thu được càng nhiều. Còn $H > 7$ thì chưa biết, có thể đáp án là 7, cũng có thể đáp án nằm ở phía $H > 7$.
  2. Thử $H = 11$: số gỗ $= 7 < L$, không thoả mãn. Mọi giá trị $H \ge 11$ chắc chắn cũng không thoả mãn, vì H càng cao thì gỗ thu được càng giảm.
  3. Vậy $H$ lớn nhất có khả năng nằm trong $8 \le H \le 10$, nếu không thì sẽ là 7. Thử $H = 9$: số gỗ $= 14$, thoả mãn. Đáp án không phải 7 nữa, mà có thể là 9 hoặc 10.
  4. Thử $H = 10$: số gỗ $= 10$, không thoả mãn. Vậy $H$ lớn nhất bằng 9.
Điểm mấu chốt
Dãy kết quả luôn có dạng ✔✔✔…✔✘✘…✘: một đoạn thoả mãn rồi tới một đoạn không thoả mãn. Mỗi lần thử ở giữa, ta loại được một nửa số giá trị còn lại. Đáp án là ô ✔ cuối cùng.

Các bước thực hiện

  1. Ba biến left, right, res: đoạn $[left, right]$ là khoảng mà $H$ lớn nhất có khả năng nằm trong đó, còn res lưu kết quả tốt nhất đã biết. Lúc đầu res = 0 (chắc chắn thoả mãn vì tổng độ cao các cây $\ge L$), left = 1, right = hMax - 1.
  2. Hàm check(H): trả về true nếu với độ cao $H$ ta thu được lượng gỗ $\ge L$.
  3. Biến mid = (left + right) / 2: nếu check(mid) sai thì mọi $H \ge mid$ đều sai, cập nhật right = mid - 1. Nếu đúng thì mọi $H \le mid$ đều đúng, cập nhật res = mid và left = mid + 1. Lặp lại đến khi left > right thì dừng và in ra res.

Bạn nhìn lại ví dụ ở trên đi, mình chọn các giá trị thử dựa đúng theo mid = (left + right) / 2 đó:

[left, right]midgỗ khi H = midcheck(mid)Cập nhậtres
[1, 14]722trueres = 7, left = 87
[8, 14]117falseright = 107
[8, 10]914trueres = 9, left = 109
[10, 10]1010falseright = 99

Giờ em tự tay chạy từng bước nhé. Có thể sửa $L$ và độ cao các cây để thử với dữ liệu của mình:

Tối đa 12 cây, mỗi cây cao từ 1 đến 60 để hình vẽ dễ nhìn.
■ chắc chắn thoả mãn■ chắc chắn không thoả mãn■ đoạn [left, right] còn đang xét□ mid: giá trị đang thử

Đây là source code cho bạn nào muốn tham khảo cách làm trên của mình:

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;

ll N, L;
vector<ll> h;

bool check(ll H) {
    ll sum = 0;
    for(int i = 0; i < N; ++i) {
        if(h[i] > H) {
            sum += h[i] - H;
        }
    }
    return sum >= L;
}

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cin >> N >> L;
    ll hMax = 0;
    for(int i = 0; i < N; ++i) {
        ll x;
        cin >> x;
        hMax = max(hMax, x);
        h.push_back(x);
    }
    ll res = 0, left = 1, right = hMax - 1;
    while(left <= right) {
        ll mid = (left + right) / 2;
        if(check(mid)) {
            res = mid;
            left = mid + 1;
        }
        else {
            right = mid - 1;
        }
    }
    cout << res;
    return 0;
}
import sys
from bisect import bisect_right

def main():
    data = sys.stdin.buffer.read().split()
    N, L = int(data[0]), int(data[1])
    h = sorted(map(int, data[2:2 + N]))
    # suffix[i] = tổng các h[j] với j >= i (mảng đã sắp xếp tăng dần)
    suffix = [0] * (N + 1)
    for i in range(N - 1, -1, -1):
        suffix[i] = suffix[i + 1] + h[i]

    def check(H):
        i = bisect_right(h, H)          # các cây từ vị trí i trở đi cao hơn H
        return suffix[i] - H * (N - i) >= L

    res, left, right = 0, 1, h[-1] - 1
    while left <= right:
        mid = (left + right) // 2
        if check(mid):
            res = mid
            left = mid + 1
        else:
            right = mid - 1
    print(res)

main()
C++▶ Chạy tay từng dòng codeChặt nhị phân kết quả: mỗi lần thử mid bằng check(mid), loại một nửa đoạn [left, right].
Dữ liệu mẫu:
1#include<bits/stdc++.h>
2#define endl '\n'
3using namespace std;
4typedef long long ll;
5
6ll N, L;
7vector<ll> h;
8
9bool check(ll H) {
10 ll sum = 0;
11 for(int i = 0; i < N; ++i) {
12 if(h[i] > H) {
13 sum += h[i] - H;
14 }
15 }
16 return sum >= L;
17}
18
19int main() {
20 ios_base::sync_with_stdio(0);
21 cin.tie(0);
22 cin >> N >> L;
23 ll hMax = 0;
24 for(int i = 0; i < N; ++i) {
25 ll x;
26 cin >> x;
27 hMax = max(hMax, x);
28 h.push_back(x);
29 }
30 ll res = 0, left = 1, right = hMax - 1;
31 while(left <= right) {
32 ll mid = (left + right) / 2;
33 if(check(mid)) {
34 res = mid;
35 left = mid + 1;
36 }
37 else {
38 right = mid - 1;
39 }
40 }
41 cout << res;
42 return 0;
43}

Giải thích một số chỗ trong code

return sum >= L;: nếu sum >= L thì hàm trả về true, ngược lại là false thôi nha.

Không gian$O(N)$
Thời gian$O(N \cdot \log_2 hMax)$

Hàm check mất $O(N)$ phép tính, còn vòng lặp while chỉ chạy $\log_2(hMax)$ lần. Trường hợp xấu nhất chỉ phải thực hiện $10^6 \times \log_2(10^9) \approx 3 \cdot 10^7$ phép tính, tầm 0.3 giây, nhanh hơn cách vét cạn rất nhiều rồi đúng không các bạn?

Ghi chú của mình: bản Python là mình viết thêm. Vì Python chạy chậm hơn C++ nhiều, bản này sắp xếp mảng độ cao trước và tính tổng hậu tố, để mỗi lần check(H) chỉ cần tìm nhị phân vị trí cây đầu tiên cao hơn $H$ (bisect) thay vì duyệt cả mảng. Ý tưởng chặt nhị phân trên $H$ vẫn giữ nguyên. Cả 4 bản code đã được kiểm tra với 400 bộ test ngẫu nhiên so với cách vét cạn; với $N = 10^6$, bản C++ chạy khoảng 0.15 giây, bản Python khoảng 1 giây.
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇