Hộp Quà (HOPQUA)
Đề bài
Bài 2 (6 điểm): HỘP QUÀ
Có $N$ hộp quà được đánh số từ $1$ đến $N$, hộp quà thứ $i$ $(i = 1, 2, \ldots, N)$ có giá trị thẩm mỹ là $m_i$. Nếu xếp lần lượt các hộp quà theo số thứ tự $1, 2, \ldots, N$ thì giá trị thẩm mỹ của $N$ hộp quà là
$m_1 + m_2 + \ldots + m_N + \max(0, m_2 - m_1) + \max(0, m_3 - m_2) + \ldots + \max(0, m_N - m_{N-1})$.
Học sinh An được giao nhiệm vụ là tìm ra cách xếp $N$ hộp quà sao cho giá trị thẩm mỹ của $N$ hộp quà là lớn nhất.
Yêu cầu: Cho $N$ và $m_i$, $i = 1, 2, \ldots, N$. Xác định giá trị thẩm mỹ lớn nhất của $N$ hộp quà.
Dữ liệu: Vào từ file văn bản HOPQUA.INP gồm:
- Dòng đầu tiên chứa số nguyên $N$ $(1 \le N \le 10^5)$.
- Dòng thứ 2 chứa $N$ số nguyên $m_1, m_2, \ldots, m_N$ $(1 \le m_i \le 10000)$. Các số ghi cách nhau một dấu cách.
Kết quả: Đưa ra file văn bản HOPQUA.OUT giá trị thẩm mỹ lớn nhất của $N$ hộp quà.
Giới hạn thời gian: 1 giây. (Code trong bài đọc/ghi chuẩn – bàn phím/màn hình; khi đi thi đọc ghi file thì các bạn thêm freopen nhé.)
5
5 3 1 7 1039Lờ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 😇.
Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì link bài đây nhé:
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
Nộp bài trên CLUE OJGiải thích ví dụ cho bạn nào chưa hiểu đề: ta được phép xếp lại thứ tự 5 hộp quà $\{5, 3, 1, 7, 10\}$. Nếu xếp thành $1, 10, 3, 7, 5$ thì giá trị thẩm mỹ là
$(1 + 10 + 3 + 7 + 5) + \max(0, 9) + \max(0, -7) + \max(0, 4) + \max(0, -2) = 26 + 9 + 0 + 4 + 0 = 39$
và không có cách xếp nào cho kết quả lớn hơn, nên đáp án là 39. Nói nôm na: mỗi lần hộp đứng sau "cao hơn" hộp đứng trước thì được thưởng thêm đúng phần chênh lệch, còn thấp hơn thì không bị trừ gì cả (lấy $\max$ với $0$).
Lộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Vét cạn thử mọi cách xếp – O(N!·N)
Cách "làm theo đề" đơn giản nhất: thử hết tất cả các cách xếp (tất cả các hoán vị của dãy), với mỗi cách xếp tính giá trị thẩm mỹ đúng như công thức đề cho, rồi lấy lớn nhất. Trong C++ có sẵn hàm next_permutation để sinh lần lượt mọi hoán vị (nhớ sort tăng dần trước để nó đi qua đủ tất cả), Python thì có itertools.permutations.
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
int main(){
int n;
cin >> n;
vector<int> a(n);
for(int i = 0; i < n; ++i){
cin >> a[i];
}
sort(a.begin(), a.end());
long long best = 0;
do{
long long cur = 0;
for(int i = 0; i < n; ++i){
cur += a[i];
if(i > 0) cur += max(0, a[i] - a[i - 1]);
}
best = max(best, cur);
} while(next_permutation(a.begin(), a.end()));
cout << best;
return 0;
}
from itertools import permutations
n = int(input())
a = list(map(int, input().split()))
best = 0
for p in permutations(a):
cur = sum(p)
for i in range(1, n):
cur += max(0, p[i] - p[i - 1])
best = max(best, cur)
print(best)
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(N)$.
- Thời gian: có $N!$ cách xếp, mỗi cách tính mất $O(N)$ ⇒ $O(N! \cdot N)$. Chỉ cần $N = 12$ là đã gần $6 \cdot 10^9$ phép tính rồi, trong khi đề cho $N$ tới $10^5$ ⇒ TLE chắc luôn.
Cách 2Tham lam: ghép lớn nhất với bé nhất – O(N log N)
Bài này để giải quyết nó chúng ta sẽ dùng phương pháp tham lam (Greedy). Kể cả bạn nào không biết về tham lam thì thực ra đôi khi các bạn vẫn đang giải quyết vấn đề theo chính tư tưởng của nó mà không biết. Nói nhanh: tham lam nghĩa là đứng trước nhiều lựa chọn thì ta chọn cách tốt nhất ở hiện tại rồi đi theo nó; lại đứng trước nhiều lựa chọn nữa thì lại chọn cái tốt nhất trong số đó – cứ tốt nhất mà triển, vì thế mới gọi là tham lam. Nó không có công thức từng bước cụ thể như mấy thuật toán khác, chỉ là một tư tưởng, còn để hiện thực được thì có thể cần thêm những kỹ thuật bổ trợ khác (như bài này là sắp xếp + hai con trỏ).
Tại sao bài này nhìn ra mùi tham lam? Đọc đề thấy rõ luôn hehe: muốn sắp xếp theo quy luật gì đó để sau cùng đáp án là lớn nhất ⇒ thấy mùi tham lam ngay 😁.
Nhận xét 1: tổng các phần tử không đổi
Giá trị thẩm mỹ $= \underbrace{m_1 + m_2 + \ldots + m_N}_{\text{tổng các phần tử}} + \underbrace{\max(0, m_2 - m_1) + \ldots + \max(0, m_N - m_{N-1})}_{\text{tổng các "bước lên"}}$ (trong bài này quy ước vị trí tính từ 1 nhé).
Phần tổng các phần tử thì dù sắp xếp thế nào sau cùng vẫn cộng đủ các giá trị của tụi nó thôi. Vậy yếu tố quyết định chính là phần thứ hai: sắp xếp sao cho tổng các hiệu "thằng đứng sau trừ thằng đứng trước" là lớn nhất. Hiệu nào âm thì chỉ lấy $0$, coi như không được cộng gì; hiệu nào dương thì chính nó được cộng vô.
Nhận xét 2: lớn nhất trừ bé nhất, lớn nhì trừ bé nhì...
Đến đây tư tưởng tham lam thấy rất rõ ràng: làm sao để mỗi cặp đứng cạnh nhau (thằng sau trừ thằng trước) ra giá trị lớn nhất có thể; càng nhiều kết quả lớn cộng lại thì kết quả sau cùng càng lớn. Chênh lệch lớn nhất là cặp <lớn nhất> − <bé nhất>, sau đó là <lớn 2> − <bé 2>, rồi <lớn 3> − <bé 3>... Tức là xếp dãy theo quy luật:
bé nhất, lớn nhất, bé 2, lớn 2, bé 3, lớn 3, …
Với ví dụ mẫu, dãy sau khi xếp như vậy là $1, 10, 3, 7, 5$, và như đã tính ở trên ra đúng $39$ – khớp với kết quả đề cho, phần nào yên tâm về tư tưởng tham lam này hehe.
Còn các cặp "nối" như <bé 2> − <lớn nhất> hay <bé 3> − <lớn 2> thì kết quả hoặc là $0$ (khi tụi nó bằng nhau) hoặc là âm, mà đề lấy $\max$ với $0$ nên sau cùng cũng chỉ cộng $0$ – coi như không cần cộng luôn. Vậy chỉ cần cộng các hiệu lớn trừ bé là được.
Minh hoạ
Em nhập dãy giá trị các hộp quà rồi bấm Tiếp: dãy được sắp tăng dần, hai con trỏ $L$, $R$ chạy từ hai đầu vào giữa; mỗi bước ghép một cặp (bé, lớn) và cộng thêm $a[R] - a[L]$. Hàng dưới là cách xếp hộp quà tương ứng (bé nhất, lớn nhất, bé 2, lớn 2...).
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
int a[n + 1];
long long sum = 0;
for(int i = 1; i <= n; ++i){
cin >> a[i];
sum += a[i];
}
sort(a + 1, a + 1 + n);
int l = 1, r = n;
while(l < r){
sum += a[r] - a[l];
l++;
r--;
}
cout << sum;
return 0;
}
import sys
data = sys.stdin.buffer.read().split()
n = int(data[0])
a = [0] + sorted(map(int, data[1:n + 1]))
s = sum(a)
l, r = 1, n
while l < r:
s += a[r] - a[l]
l += 1
r -= 1
print(s)
Giải thích một số chỗ trong code:
- 3 dòng đầu hàm
main(ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);): hiểu cơ bản là giúpcin/coutcủa C++ chạy nhanh gần bằng nhập xuất của C. Bài này phải đọc tới $10^5$ số nên thêm 3 dòng này vào cho chắc ăn. Code C++ thì nhớ thêm 3 dòng này vào đầu hàm main nhé. - Biến
sumtận dụng ngay vòng lặp đọc dữ liệu để cộng từng phần tử vào luôn; khúc sau chỉ cần cộng thêm các hiệu $a[R] - a[L]$. - Hàm
sortcó sẵn trong thư viện của C++ luôn đảm bảo $O(N \log N)$. $N$ tới $10^5$ nên phải dùng nó (hoặc tự cài Quick Sort, Merge Sort, Heap Sort); còn mấy kiểu Interchange Sort, Selection Sort, Bubble Sort $O(N^2)$ thì TLE chắc luôn. Tự cài vừa mất công vừa dễ sai, lỡ xui Quick Sort rơi vào trường hợp xấu nhất cũng thành $O(N^2)$, nên cứ dùngsortcó sẵn cho nhanh gọn. sumkiểulong long: tổng các phần tử tối đa $10^5 \cdot 10^4 = 10^9$, cộng thêm các hiệu tối đa khoảng $5 \cdot 10^4 \cdot 10^4 = 5 \cdot 10^8$ ⇒ cỡ $1.5 \cdot 10^9$, đã khá sát giới hạn củaint(khoảng $2.1 \cdot 10^9$). Dự kiến kết quả lớn nên để chắc ăn mình đểlong long(số nguyên 8 byte) cho khỏi lo tràn số.- Mảng
acó $N + 1$ phần tử: bỏ qua chỉ số 0, chỉ dùng chỉ số $1..N$ cho khớp với đề, tránh bị loạn.
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(N)$ cho mảng $a$ ($N$ là số hộp quà).
- Thời gian: $O(N)$ đọc dữ liệu + $O(N \log N)$ sắp xếp + $O(N)$ hai con trỏ. Theo quy tắc cộng Big-O lấy thành phần lớn nhất ⇒ $O(N \log N)$. Trường hợp xấu nhất $N = 10^5$: khoảng $10^5 + 17 \cdot 10^5 + 10^5 \approx 2 \cdot 10^6$ lần lặp, ít hơn ngưỡng $(3 \div 5) \cdot 10^7$ từ 10 đến 20 lần nên yên tâm không bị TLE.
- Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
- Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
- Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
- Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
- Dễ nản giữa chừng khi không có ai theo sát.
- Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
- Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
- Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
- Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.
Nhận xét
Đăng nhận xét