Tạo Mảng B (TAOMANGB)
Đề bài
Cho dãy số nguyên $A[\,]$ gồm có $N$ phần tử. Nhiệm vụ của bạn là tìm dãy số $B[\,]$ có tổng phần tử nhỏ nhất thỏa mãn tính chất $A[i] / B[i] = A[i+1] / B[i+1]$ với mọi chỉ số $i$ $(0 \le i \le N-2)$.
Phép chia trong bài toán này là phép chia nguyên (tức là chỉ lấy phần nguyên của kết quả: ví dụ $5/3 = 1$).
Dữ liệu vào:
- Dòng đầu tiên là số lượng phần tử $N$ $(1 \le N \le 1000)$.
- Dòng tiếp theo gồm $N$ số nguyên $A[i]$ $(1 \le A[i] \le 2000)$.
Kết quả: In ra một số nguyên là tổng các phần tử của dãy số $B[\,]$ tìm được.
5
18 27 16 22 625Giải thích test: Dãy $B[\,]$ tìm được là 5, 7, 5, 6, 2.
Lời mở đầu
Bài này mình được một em học viên hỏi: "anh ơi em cũng có thấy bài này hay hay nhưng em code không ra anh ạ" 😅. Mình đã hướng dẫn chi tiết cho em ấy, và tin rằng sẽ có giá trị với nhiều bạn nên mình viết lại thành bài giải đầy đủ ở đây. Cách của mình có thể chưa phải tối ưu nhất, nhưng về mặt cài đặt thì rất dễ, và với phạm vi dữ liệu của đề thì time limit 1 giây như tiêu chuẩn là yên tâm AC nhé 😇.
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 OJMình giải thích ví dụ mẫu để bạn nào chưa hiểu đề sẽ hiểu rõ. Với $A = [18, 27, 16, 22, 6]$, ta cần chọn mỗi $B[i]$ (số nguyên dương) sao cho tất cả các thương nguyên $A[i] / B[i]$ đều bằng nhau, và tổng các $B[i]$ nhỏ nhất. Đề chọn $B = [5, 7, 5, 6, 2]$:
$18/5 = 3,\quad 27/7 = 3,\quad 16/5 = 3,\quad 22/6 = 3,\quad 6/2 = 3$
Tất cả thương đều bằng $3$, tổng $5 + 7 + 5 + 6 + 2 = 25$. Bạn thử chọn cho mọi thương bằng $0$ chẳng hạn: phải lấy $B = [19, 28, 17, 23, 7]$ (mỗi số lớn hơn $A[i]$) thì tổng tới $94$ – lớn hơn nhiều. Vậy câu hỏi thật sự là: nên chọn "thương chung" $z$ bằng bao nhiêu, và với mỗi $z$ thì mỗi $B[i]$ nhỏ nhất là bao nhiêu?
Lộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Bảng đánh dấu 2 chiều – O(V² + V·N)
Đầu tiên các bạn để ý: mỗi phần tử $A[i]$ chỉ nằm trong phạm vi từ $1$ đến $2000$. Khi phạm vi giá trị nhỏ như thế này thì 90% bài có thể giải quyết bằng mảng đánh dấu – đây là dấu hiệu nhận biết rất quen thuộc đó nha.
Quan sát: x / y chỉ ra được vài giá trị
Với một số $x$ bất kỳ, nếu $x / y = z$ (chia nguyên, $y \ge 1$) thì $z$ chỉ nằm trong đoạn từ $0$ đến $x$ thôi – không thể lớn hơn $x$, cũng không thể nhỏ hơn $0$. Ví dụ $x = 6$:
Từ $y = 7$ trở đi thì $6 / y$ luôn bằng $0$ (vì $y \gt x$). Như vậy $z$ chỉ có các kết quả $0, 1, 2, 3, 6$. Các bạn cũng nhận ra có những giá trị trong đoạn $[0, 6]$ mà không có $y$ nào cho ra: chẳng có cách nào để $6$ chia cho một số mà được $4$ hay $5$ cả.
Vậy giờ ta tạo bảng thống kê: với mỗi giá trị $x$, với mỗi kết quả $z$ từ $0$ đến $x$, lưu lại $y$ nhỏ nhất sao cho $x / y = z$ (lưu $0$ nếu không có $y$ nào). Với $x = 6$:
| z | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| y nhỏ nhất | 7 | 4 | 3 | 2 | 0 | 0 | 1 |
| giải thích | 6/7, 6/8… đều ra 0, nhỏ nhất là 7 | 6/4, 6/5, 6/6 ra 1 → lấy 4 | chỉ có 6/3 | chỉ có 6/2 | không có | không có | chỉ có 6/1 |
Duyệt thương chung z
Khi có bảng thống kê rồi thì xét qua mảng $a$ đề cho:
- Tìm $Min$ của mảng $a$. Thương chung $z$ không thể vượt quá $Min$ (vì $A[i] / B[i] \le A[i]$), nên chỉ cần xét $z$ từ $0$ đến $Min$.
- Với từng $z$, xét lần lượt từng $a[j]$, lấy $y$ nhỏ nhất của nó là $b[a[j]][z]$. Chỉ cần một $a[j]$ có $b[a[j]][z] = 0$ (không tồn tại $y$) là bỏ qua trường hợp $z$ đó. Còn nếu tất cả $a[j]$ đều có $y$ thì cộng các $y$ lại.
- Đáp án là tổng nhỏ nhất trong các tổng tìm được.
Đây là bảng thống kê mình làm với ví dụ mẫu ($Min = 6$ nên chỉ cần cột $0..6$):
| a \ z | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| 18 | 19 | 10 | 7 | 5 | 4 | 0 | 3 |
| 27 | 28 | 14 | 10 | 7 | 6 | 5 | 4 |
| 16 | 17 | 9 | 6 | 5 | 4 | 3 | 0 |
| 22 | 23 | 12 | 8 | 6 | 5 | 4 | 0 |
| 6 | 7 | 4 | 3 | 2 | 0 | 0 | 1 |
| Tổng | 94 | 49 | 34 | 25 | ✗ | ✗ | ✗ |
Ví dụ với số $18$: giá trị nhỏ nhất mà $18$ chia cho nó được $0$ là $19$; được $1$ là $10$ ($18/10 = 1$); được $2$ là $7$ ($18/7 = 2$)… còn tại cột $5$ thì là $0$ – không có số nào để $18$ chia ra $5$ cả. Các cột $z = 4, 5, 6$ đều dính số $0$ nên loại:
- $z = 4$: không có $y$ nào để $6 / y = 4$.
- $z = 5$: không có $y$ nào để $18 / y = 5$ và $6 / y = 5$.
- $z = 6$: không có $y$ nào để $16 / y = 6$ và $22 / y = 6$.
Còn lại $z = 0, 1, 2, 3$ cho tổng $94, 49, 34, 25$ ⇒ đáp án nhỏ nhất là 25, ứng với $B = [5, 7, 5, 6, 2]$, tức là $18/5 = 27/7 = 16/5 = 22/6 = 6/2 = 3$ 🎉.
Minh hoạ duyệt từng cột z
Em nhập dãy $a$ (các số nhỏ thôi cho dễ nhìn) rồi bấm Tiếp để xem bảng $b[a[j]][z]$ và từng cột $z$ được xét như thế nào:
Bảng của mình là mảng 2 chiều: hàng $x$ có $x + 1$ ô (cột $0..x$). Quan trọng là build ra được cái bảng đó, ra cái bảng rồi thì xét dễ ẹt 😎.
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 solve(int a[], int n){
vector<vector<int>> b(2001);
for(int i = 1; i <= 2000; ++i){
b[i].resize(i + 1, 0);
b[i][0] = i + 1;
for(int j = 1; j <= i; ++j){
int x = i / j;
if(b[i][x] == 0){
b[i][x] = j;
}
}
}
int Min = a[0];
for(int i = 1; i < n; ++i){
Min = min(Min, a[i]);
}
int resMin = INT_MAX;
int idxMin = -1;
for(int i = 0; i <= Min; ++i){
bool check = true;
int sum = 0;
for(int j = 0; j < n; ++j){
sum += b[a[j]][i];
if(b[a[j]][i] == 0){
check = false;
break;
}
}
if(check == true && sum < resMin){
resMin = sum;
idxMin = i;
}
}
return resMin;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
int n;
cin >> n;
int a[n];
for(int i = 0; i < n; ++i){
cin >> a[i];
}
cout << solve(a, n);
return 0;
}
import sys
def solve(a, n):
b = [[0] * (i + 2) for i in range(2001)]
for i in range(1, 2001):
b[i][0] = i + 1
for j in range(1, i + 1):
x = i // j
if b[i][x] == 0:
b[i][x] = j
Min = a[0]
for i in range(1, n):
Min = min(Min, a[i])
resMin = float('inf')
idxMin = -1
for i in range(0, Min + 1):
check = True
sum_ = 0
for j in range(n):
if b[a[j]][i] == 0:
check = False
break
sum_ += b[a[j]][i]
if check and sum_ < resMin:
resMin = sum_
idxMin = i
return resMin
input_data = list(map(int, sys.stdin.read().split()))
n = input_data[0]
a = input_data[1:n+1]
print(solve(a, n))
sum += b[a[j]][i] trước rồi mới kiểm tra ô đó có bằng $0$ không. Không sao cả, vì nếu ô bằng $0$ thì check = false và tổng đó bị bỏ qua luôn. Biến idxMin lưu lại $z$ tốt nhất (không cần cho đáp án, để bạn nào muốn in luôn dãy $B$ thì dùng). Bản gốc của mình còn một đoạn in bảng 2 chiều chi tiết (đang comment lại) – chính là bảng thống kê ở trên; ở đây mình lược đi cho gọn.Các bạn bấm từng bước để xem code chạy thế nào. Để dễ theo dõi, mình cho chạy chi tiết vòng dựng bảng ở những hàng $i$ có trong mảng $a$; các hàng khác làm y hệt nên mình gom thành một bước.
Đánh giá độ phức tạp của cách làm này (gọi $V = 2000$ là giá trị lớn nhất của $A[i]$):
- Không gian: bảng $b$ có hàng $i$ dài $i + 1$ ô, tổng cộng khoảng $V^2 / 2 \approx 2 \cdot 10^6$ số
int(cỡ 8 MB) – thoải mái. - Thời gian: dựng bảng mất $1 + 2 + \dots + V \approx 2 \cdot 10^6$ phép chia. Duyệt $z$ từ $0$ đến $Min \le V$, mỗi $z$ đi qua tối đa $N$ phần tử: $O(V \cdot N) \le 2 \cdot 10^6$. Tổng cộng chỉ cỡ vài triệu phép tính.
- 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