Max Triple
Đề bài
MAX TRIPLE
Cho mảng $A[\,]$ gồm $N$ số nguyên.
Nhiệm vụ của bạn là tìm tổng lớn nhất của bộ ba số trong mảng. Chú ý nếu sử dụng kỹ thuật sắp xếp, submit lời giải của bạn sẽ bị fail.
Ví dụ $A[\,] = \{1, 2, 3, 4, 5\}$, ta nhận được tổng lớn nhất của bộ ba số là $3 + 4 + 5 = 12$.
Input:
- Dòng đầu tiên đưa vào $T$ là số lượng bộ test.
- Những dòng tiếp theo, mỗi dòng đưa vào một test. Mỗi test là gồm hai dòng: dòng đầu tiên đưa vào $N$ là số lượng phần tử của mảng $A[\,]$; dòng tiếp theo đưa vào các phần tử $A[i]$ của mảng $A[\,]$.
- $T$, $N$, $A[i]$ thoả mãn ràng buộc: $1 \le T \le 100$; $1 \le N \le 10^6$; $-10^8 \le A[i] \le 10^8$.
Output:
- Đưa ra kết quả mỗi test theo từng dòng.
2
7
1 2 3 0 -1 8 10
7
9 8 20 3 4 -1 021
37Lời mở đầu
Nay mình đi comment dạo trên một nhóm lập trình thấy bài này thú vị nè, có bạn đăng lên nhờ mọi người giúp. Bài nhìn thì dễ, nhưng cái hay nằm ở ràng buộc không được sắp xếp. Đây cũng từng là 1 câu hỏi phỏng vấn tuyển dụng của FPT đó anh em 😉. Phần hướng giải mình đã để ở comment bài viết, giờ mình viết lại chi tiết hơn, có hình minh hoạ và chạy tay để anh em dễ theo dõi.
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ụ: test 1 là mảng 1 2 3 0 -1 8 10, ba số lớn nhất là $10, 8, 3$ nên tổng lớn nhất của bộ ba là $10 + 8 + 3 = 21$. Test 2 là mảng 9 8 20 3 4 -1 0, ba số lớn nhất là $20, 9, 8$ ⇒ $20 + 9 + 8 = 37$. Rõ ràng muốn tổng 3 số lớn nhất thì ta chọn 3 số lớn nhất của mảng (nếu có giá trị trùng nhau thì vẫn được chọn cả hai, vì là hai phần tử khác nhau). Đề ngầm hiểu mỗi mảng có ít nhất 3 phần tử.
Lộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Duyệt 3 lần, mỗi lần lấy ra 1 số lớn nhất – O(3·N)
Không được sắp xếp thì cách nghĩ đầu tiên của nhiều anh em là: tìm số lớn nhất, đánh dấu lại; rồi tìm số lớn nhất trong các số chưa đánh dấu (chính là số lớn nhì); rồi làm thêm lần nữa để ra số lớn thứ ba. Có những anh em comment dùng đến 3 vòng lặp như vậy. Cách này vẫn ra kết quả đúng, độ phức tạp vẫn là $O(N)$ cho mỗi test, nhưng như thế chưa tối ưu:
- Phải duyệt mảng tới 3 lần.
- Phải lưu toàn bộ mảng (và cả mảng đánh dấu) ⇒ tốn bộ nhớ $O(N)$.
Source code để các bạn tham khảo nếu làm theo hướng này (mình viết thêm để anh em so sánh):
#include <bits/stdc++.h>
using namespace std;
int a[1000005];
bool chon[1000005];
int main() {
ios_base::sync_with_stdio(false);
cin.tie(0);
int t;
cin >> t;
while(t--) {
int n;
cin >> n;
for(int i = 0; i < n; i++) {
cin >> a[i];
chon[i] = false;
}
int tong = 0;
for(int lan = 1; lan <= 3; lan++) {
int vt = -1;
for(int i = 0; i < n; i++)
if(!chon[i] && (vt == -1 || a[i] > a[vt]))
vt = i;
chon[vt] = true;
tong += a[vt];
}
cout << tong << "\n";
}
return 0;
}
import sys
def main():
data = sys.stdin.buffer.read().split()
p = 0
t = int(data[p]); p += 1
kq = []
for _ in range(t):
n = int(data[p]); p += 1
a = list(map(int, data[p:p + n])); p += n
chon = [False] * n
tong = 0
for lan in range(3):
vt = -1
for i in range(n):
if not chon[i] and (vt == -1 or a[i] > a[vt]):
vt = i
chon[vt] = True
tong += a[vt]
kq.append(str(tong))
print('\n'.join(kq))
main()
Cách 2Chỉ 1 vòng lặp với bộ 3 biến Max1, Max2, Max3 – O(N)
Cách tối ưu nhất là ta hoàn toàn có thể chỉ cần 1 vòng lặp duy nhất xét qua toàn bộ các phần tử của mảng là tìm ra được 3 số lớn nhất. Tư tưởng giải quyết như sau:
Ta tạo ra bộ 3 biến max1, max2, max3 tương ứng là 3 giá trị lớn nhất, nhì, ba. 3 biến này ban đầu sẽ khởi tạo giá trị là $-10^8 - 1$ để đảm bảo mọi giá trị trong mảng lần đầu tiên so sánh với nó sẽ luôn lớn hơn nó, để từ đó cập nhật lại giá trị mới vào các biến này (các phần tử trong mảng theo như đề bài cho có miền giá trị từ $-10^8$ đến $10^8$).
Thì giờ đây chỉ cần 1 vòng lặp duyệt qua từng phần tử $x$ của mảng và ta luôn cập nhật đồng thời bộ 3 biến Max. Để làm được điều đó thì như sau:
- Nếu $x \gt$ max1 (lớn hơn cả số lớn nhất):
max1phải trao lại giá trị hiện tại của nó chomax2. Nhưng nếumax2muốn nhận giá trị từmax1thì trước đómax2cũng phải trao lại giá trị hiện tại của nó chomax3rồi mới nhận được giá trị mới từmax1. Khi nào trao lại giá trị xong xuôi hết thìmax1mới nhận giá trị $x$. - Ngược lại, nếu $x \gt$ max2: lúc này
max2phải trao lại giá trị hiện tại chomax3rồimax2mới được nhận $x$. - Ngược lại, nếu $x \gt$ max3:
max3là thằng cuối trong bộ 3 rồi nên nó không cần trao giá trị lại cho thằng nào khác, chính nó nhận luôn $x$.
max3 = max2; rồi max2 = max1; rồi mới max1 = x;. Nếu làm ngược (gán max1 = x trước) thì giá trị cũ của max1 bị ghi đè mất, không còn gì để trao cho max2 nữa. Ta luôn phải cập nhật đồng thời như thế để duy trì được bộ 3 biến này luôn là 3 giá trị lớn nhất trong các số đã xét.Cứ như thế ta chỉ cần duyệt hết 1 lần vòng lặp và luôn kiểm tra cập nhật liên tục 3 biến max1, max2, max3 rồi sau cùng ta đã có được kết quả là max1 + max2 + max3.
Minh hoạ
Em nhập mảng rồi bấm Tiếp để xem bộ 3 biến Max "chuyền" giá trị cho nhau như thế nào (ô vàng là số $x$ đang xét):
Mình cũng code thử theo hướng này cho anh em tham khảo để nhìn thấy rõ hơn những gì mình đã phân tích:
#include <iostream>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while(t--) {
int n;
cin >> n;
int max1, max2, max3;
max1 = max2 = max3 = -100000001;
while(n--) {
int x;
cin >> x;
if(x > max1) {
max3 = max2;
max2 = max1;
max1 = x;
}
else if(x > max2) {
max3 = max2;
max2 = x;
}
else if(x > max3) {
max3 = x;
}
}
cout << max1 + max2 + max3 << "\n";
}
return 0;
}
import sys
def main():
data = sys.stdin.buffer.read().split()
p = 0
t = int(data[p]); p += 1
kq = []
for _ in range(t):
n = int(data[p]); p += 1
max1 = max2 = max3 = -100000001
for k in range(p, p + n):
x = int(data[k])
if x > max1:
max3 = max2
max2 = max1
max1 = x
elif x > max2:
max3 = max2
max2 = x
elif x > max3:
max3 = x
p += n
kq.append(str(max1 + max2 + max3))
print('\n'.join(kq))
main()
Đánh giá độ phức tạp của cách làm này:
- Không gian: chỉ dùng vài biến, không cần mảng ⇒ $O(1)$.
- Thời gian: $O(N)$ với từng bộ test và ta có $T$ bộ test ⇒ độ phức tạp tổng quát cả bài sẽ là $O(T \cdot N)$.
Trong trường hợp bài này, $O(T \cdot N)$ là tối ưu nhất rồi, bởi vì chắc chắn nội bước đọc dữ liệu là đã phải xét qua đủ $T$ trường hợp và mỗi trường hợp đọc qua $N$ số là điều bắt buộc. Vậy nên một số anh em nói bài này muốn AC phải dùng thuật toán $O(T \cdot \sqrt{N})$ hay $O(T \cdot \log N)$ thì không thể được nha, vì bước đọc dữ liệu vào vốn nó đã là $O(T \cdot N)$ rồi.
scanf, printf thay vì cin, cout; kể cả bước xuống dòng cũng nên dùng "\n" thay vì endl cho chắc ăn. Hoặc anh em để 3 dòng đầu hàm main như trong code trên của mình (ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0);) – có thể hiểu cơ bản là nó giúp tốc độ đọc ghi của C++ nhanh như bên C – rồi từ đó yên tâm dùng cin, cout.- 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