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

Hoán Đổi

Hoán Đổi
Nguồn: Codeforces Group Độ khó: Dễ Tham lam Mảng

Đề bài

Bạn được cho hai mảng số nguyên dương $a$ và $b$, mỗi mảng có độ dài $n$. Bạn có thể thực hiện phép toán sau đây bao nhiêu lần tuỳ ý (có thể là 0 lần):

  • Chọn một chỉ số $i$ $(1 \le i \le n)$ và hoán đổi giá trị $a[i]$ với $b[i]$.

Mục tiêu: tìm giá trị nhỏ nhất có thể của biểu thức $\max(a[1], a[2], \ldots, a[n]) \times \max(b[1], b[2], \ldots, b[n])$ sau khi đã thực hiện các phép hoán đổi.

Input: dòng đầu tiên chứa một số nguyên $t$ $(1 \le t \le 10^6)$ – số lượng bộ test. Với mỗi bộ test:

  • Dòng thứ nhất chứa một số nguyên $n$ $(1 \le n \le 10^6)$ – độ dài của hai mảng.
  • Dòng thứ hai chứa $n$ số nguyên $a[1], a[2], \ldots, a[n]$ $(1 \le a[i] \le 10^4)$.
  • Dòng thứ ba chứa $n$ số nguyên $b[1], b[2], \ldots, b[n]$ $(1 \le b[i] \le 10^4)$.

Dữ liệu đảm bảo tổng giá trị $n$ trên toàn bộ test không vượt quá $10^6$.

Output: với mỗi bộ test, in ra một số nguyên duy nhất: giá trị nhỏ nhất có thể của tích hai giá trị lớn nhất như đã mô tả.

Subtask: 50% số test đầu tiên có $1 \le t, n \le 10^2$; 50% số test còn lại không có giới hạn gì thêm.

Ví dụ – input
3
6
1 2 6 5 1 2
3 4 3 2 2 5
3
3 3 3
3 3 3
2
1 2
2 1
Ví dụ – output
18
9
2

Giải thích ví dụ:

  • Test 1: có thể hoán đổi tại các vị trí $i = 2$ và $i = 6$. Khi đó mảng $a$ trở thành $[1, 4, 6, 5, 1, 5]$, giá trị lớn nhất là 6; mảng $b$ trở thành $[3, 2, 3, 2, 2, 2]$, giá trị lớn nhất là 3. Kết quả $= 6 \times 3 = 18$.
  • Test 2: các mảng đã giống nhau, không thể thay đổi. Kết quả $= 3 \times 3 = 9$.
  • Test 3: có thể hoán đổi tại vị trí $i = 1$. Khi đó $a = [2, 2]$, max $= 2$; $b = [1, 1]$, max $= 1$. Kết quả $= 2 \times 1 = 2$.

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 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 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:

Ý tưởngTham lam: dồn hết số lớn về một mảng

Tóm tắt: cho 2 mảng $a, b$ cùng số lượng phần tử, ta có thể đổi chỗ cặp phần tử cùng vị trí $a[i]$ và $b[i]$ cho nhau, bao nhiêu cặp cũng được. Tìm tích 2 phần tử lớn nhất của 2 mảng sao cho tích này nhỏ nhất có thể.

Đọc đề thấy "đáp án phải nhỏ nhất" là thấy mùi tham lam ngay 😄. Nhìn ví dụ 1: ban đầu max của 2 mảng là 6 và 5, tích 30. Sau vài phép đổi chỗ thì max của mảng $b$ chỉ còn 3, tích 18.

Mấu chốt
Phần tử lớn nhất của cả 2 mảng (ví dụ là 6) dù nằm ở mảng nào thì khi tính tích cũng vẫn gặp nó. Các phần tử lớn còn lại nếu nằm chung mảng với nó thì bị "che" đi, không ảnh hưởng tích nữa. Vậy ta dồn hết số lớn hơn của mỗi cặp về cùng một mảng ($a$), mảng còn lại ($b$) chỉ chứa số nhỏ hơn của mỗi cặp. Khi đó max của $b$ là nhỏ nhất có thể.
Ban đầu: 6 × 5 = 30
a126512
b343225
Dồn số lớn về a: 6 × 3 = 18
a346525
b123212

Vì sao không thể tốt hơn? Xét cặp có số nhỏ hơn lớn nhất (gọi giá trị đó là $m$): hai số của cặp này nằm ở hai mảng khác nhau và đều $\ge m$, nên cả hai mảng đều có max $\ge m$. Một mảng lại chắc chắn chứa số lớn nhất $M$. Vậy tích luôn $\ge M \times m$, và cách dồn ở trên đạt đúng $M \times m$.

Minh hoạChạy từng cặp

Em nhập 2 mảng cùng độ dài (tối đa 12 phần tử) rồi bấm Tiếp:

Ví dụ nhanh:

Cách 1Đổi chỗ rồi sắp xếp – O(N log N)

Có rất nhiều cách cài đặt. Ví dụ: chạy một lượt, nếu a[i] < b[i] thì đổi chỗ tụi nó để mảng $a$ chứa số lớn, $b$ chứa số nhỏ ở từng vị trí. Xong thì tìm phần tử lớn nhất của $a$ và $b$ rồi nhân lại. Để tìm phần tử lớn nhất có thể dùng luôn hàm sort, 2 thằng cuối cùng của mỗi mảng chính là 2 thằng lớn nhất.

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

const int MAX = 1e6;
int a[MAX + 5], b[MAX + 5];

int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int t;
    cin >> t;
    while(t--){
        int n;
        cin >> n;
        for(int i = 0; i < n; ++i){
            cin >> a[i];
        }
        for(int i = 0; i < n; ++i){
            cin >> b[i];
            if(a[i] < b[i]){
                swap(a[i], b[i]);
            }
        }
        sort(a, a + n);
        sort(b, b + n);
        cout << a[n - 1] * b[n - 1] << endl;
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    t = int(data[0]); pos = 1
    out = []
    for _ in range(t):
        n = int(data[pos]); pos += 1
        a = list(map(int, data[pos:pos + n])); pos += n
        b = list(map(int, data[pos:pos + n])); pos += n
        for i in range(n):
            if a[i] < b[i]:
                a[i], b[i] = b[i], a[i]      # dồn số lớn hơn của mỗi cặp về mảng a
        a.sort()
        b.sort()
        out.append(a[-1] * b[-1])
    print('\n'.join(map(str, out)))

main()
C++▶ Chạy tay từng dòng codeCách 1: đổi chỗ để a chứa số lớn, rồi sort lấy phần tử cuối.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const int MAX = 1e6;
5int a[MAX + 5], b[MAX + 5];
6
7int main(){
8 ios_base::sync_with_stdio(0);
9 cin.tie(0);
10 int t;
11 cin >> t;
12 while(t--){
13 int n;
14 cin >> n;
15 for(int i = 0; i < n; ++i){
16 cin >> a[i];
17 }
18 for(int i = 0; i < n; ++i){
19 cin >> b[i];
20 if(a[i] < b[i]){
21 swap(a[i], b[i]);
22 }
23 }
24 sort(a, a + n);
25 sort(b, b + n);
26 cout << a[n - 1] * b[n - 1] << endl;
27 }
28 return 0;
29}
  1. Mỗi lần đọc xong b[i] là có đủ dữ liệu để so sánh với a[i] đã đọc trước đó, đổi chỗ luôn nếu cần, khỏi mất công làm thêm 1 vòng lặp.
  2. Hàm swap có sẵn trong C++, gọi ra dùng cho nhanh.
  3. Hàm sort có sẵn trong STL, độ phức tạp luôn đảm bảo $O(N \log N)$.

Độ phức tạp: $O(2N)$ đọc 2 mảng cộng $O(2N \log N)$ sắp xếp, tổng quát là $O(N \log N)$ mỗi bộ test. Với tổng $n$ tối đa $10^6$ thì vẫn qua, nhưng bản chất để tìm số lớn nhất ta đâu cần sắp xếp làm gì!

Cách 2Tìm max ngay khi đọc – O(N)

Chỉ cần duyệt một lượt là tìm được số lớn nhất, $O(N)$. Cũng không cần 2 vòng lặp vì 2 mảng có cùng $N$ phần tử; thậm chí tận dụng luôn vòng lặp đọc mảng $b$. Và cũng không cần đổi chỗ gì hết: tạo 2 biến maxA, maxB, với mỗi cặp thì maxA cập nhật bằng số lớn hơn của cặp, maxB cập nhật bằng số nhỏ hơn của cặp.

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

const int MAX = 1e6;
int a[MAX + 5], b[MAX + 5];

int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int t;
    cin >> t;
    while(t--){
        int n;
        cin >> n;
        int maxA = 0, maxB = 0;
        for(int i = 0; i < n; ++i){
            cin >> a[i];
        }
        for(int i = 0; i < n; ++i){
            cin >> b[i];
            maxA = max(maxA, max(a[i], b[i]));
            maxB = max(maxB, min(a[i], b[i]));
        }
        cout << maxA * maxB << endl;
    }
    return 0;
}
C++▶ Chạy tay từng dòng codeCách 2: cập nhật maxA, maxB ngay khi đọc, không cần sort.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const int MAX = 1e6;
5int a[MAX + 5], b[MAX + 5];
6
7int main(){
8 ios_base::sync_with_stdio(0);
9 cin.tie(0);
10 int t;
11 cin >> t;
12 while(t--){
13 int n;
14 cin >> n;
15 int maxA = 0, maxB = 0;
16 for(int i = 0; i < n; ++i){
17 cin >> a[i];
18 }
19 for(int i = 0; i < n; ++i){
20 cin >> b[i];
21 maxA = max(maxA, max(a[i], b[i]));
22 maxB = max(maxB, min(a[i], b[i]));
23 }
24 cout << maxA * maxB << endl;
25 }
26 return 0;
27}
  1. Khởi tạo maxA = maxB = 0: giá trị nhỏ nhất có thể để lần so sánh đầu tiên phần tử nào cũng lớn hơn và được cập nhật vào. Vì đề cho $a[i], b[i] \ge 1$ nên khởi tạo 0 là đảm bảo.
  2. Hàm max(a, b) trả về giá trị lớn hơn, min(a, b) trả về giá trị nhỏ hơn, dùng cho code gọn thay vì viết if.

Thực ra muốn tối ưu thêm về bộ nhớ thì chẳng cần mảng $b$: giá trị b[i] đọc vào xong là so sánh rồi bỏ, nên chỉ cần một biến $x$:

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

const int MAX = 1e6;
int a[MAX + 5];

int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int t;
    cin >> t;
    while(t--){
        int n;
        cin >> n;
        int maxA = 0, maxB = 0;
        for(int i = 0; i < n; ++i){
            cin >> a[i];
        }
        for(int i = 0; i < n; ++i){
            int x;
            cin >> x;
            maxA = max(maxA, max(a[i], x));
            maxB = max(maxB, min(a[i], x));
        }
        cout << maxA * maxB << endl;
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    t = int(data[0]); pos = 1
    out = []
    for _ in range(t):
        n = int(data[pos]); pos += 1
        a = data[pos:pos + n]; pos += n
        b = data[pos:pos + n]; pos += n
        maxA = maxB = 0
        for x, y in zip(map(int, a), map(int, b)):
            if x < y:
                x, y = y, x
            if x > maxA: maxA = x       # số lớn của cặp dồn về mảng a
            if y > maxB: maxB = y       # số nhỏ của cặp nằm ở mảng b
        out.append(maxA * maxB)
    print('\n'.join(map(str, out)))

main()
C++▶ Chạy tay từng dòng codeBản tối ưu bộ nhớ: chỉ dùng 1 biến x thay cho mảng b.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const int MAX = 1e6;
5int a[MAX + 5];
6
7int main(){
8 ios_base::sync_with_stdio(0);
9 cin.tie(0);
10 int t;
11 cin >> t;
12 while(t--){
13 int n;
14 cin >> n;
15 int maxA = 0, maxB = 0;
16 for(int i = 0; i < n; ++i){
17 cin >> a[i];
18 }
19 for(int i = 0; i < n; ++i){
20 int x;
21 cin >> x;
22 maxA = max(maxA, max(a[i], x));
23 maxB = max(maxB, min(a[i], x));
24 }
25 cout << maxA * maxB << endl;
26 }
27 return 0;
28}
Không gian$O(1)$ (bản cuối)
Thời gian$O(N)$ mỗi bộ test
Mẹo nhỏ khi có rất nhiều bộ test
Đề cho $t$ tới $10^6$ bộ test. endl vừa xuống dòng vừa đẩy (flush) dữ liệu ra ngay, gọi tới $10^6$ lần có thể làm chậm chương trình. Các bạn có thể thay endl bằng '\n' cho an toàn.
Ghi chú của mình: 2 bản Python là mình viết thêm theo cách 1 và cách 2 (đọc toàn bộ input một lần cho nhanh). Tất cả các bản đã được kiểm tra với 200 bộ test ngẫu nhiên so với cách vét cạn mọi cách hoán đổi; với $10^6$ bộ test, bản C++ cách 2 chạy khoảng 0.3 giây, bản Python khoảng 0.85 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 😇

Nhận xét