Hướng dẫn giải của Dãy số nhân tính
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.
Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.
Bạn phải đăng nhập để mở lời giải này.
Đăng nhậpTác giả: , , ,
Editorial for nhantinh: Dãy số nhân tính
Hiểu bài toán
Cho một dãy số nguyên $a1, a2, ..., an$. Dãy được gọi là 'nhân tính hoàn toàn' nếu với mọi cặp chỉ số $i, j$ sao cho $1 \le i, j \le n$ và $i \cdot j \le n$ thì điều kiện $a{i \cdot j} = ai \times aj$ phải được thỏa mãn. Bài toán yêu cầu kiểm tra xem dãy số cho trước có phải là dãy nhân tính hoàn toàn hay không.
Ví dụ: Nếu $n=10$, ta cần kiểm tra các cặp $(i, j)$ như $(1, 1) \rightarrow a1 = a1 \cdot a1$, $(1, 2) \rightarrow a2 = a1 \cdot a2$, $(2, 2) \rightarrow a4 = a2 \cdot a2$, ..., $(3, 3) \rightarrow a9 = a3 \cdot a3$ (vì $3 \cdot 3 = 9 \le 10$), nhưng không cần kiểm tra $(3, 4)$ (vì $3 \cdot 4 = 12 > 10$).
Dữ liệu nhập: $n$ từ $1$ đến $10^5$, các giá trị $a_i$ từ $0$ đến $10^9$. Cần chú ý tràn số khi nhân (sử dụng long long).
Các cách tiếp cận
Cách Brute Force (Lặp kép)
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<long long> a(n + 1);
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) { // Duyệt j từ 1 đến n
ll k = i * j;
if(k > n) break; // Thoát khi chỉ số vượt quá n
if(a[k] != a[i] * a[j]) {
cout << "NO";
return 0;
}
}
}
cout << "YES";
return 0;
}
- Time Complexity: O(n^2) hoặc O(n log n)
- Space Complexity: O(n)
Đây là cách tiếp cận trực quan nhất: duyệt qua tất cả các cặp chỉ số $(i, j)$ có thể và kiểm tra điều kiện.
- Cách hoạt động: Hai vòng lặp lồng nhau, vòng lặp ngoài cho $i$ từ $1$ đến $n$, vòng lặp trong cho $j$ từ $1$ đến $n$. Nếu $i \times j > n$ thì dừng vòng lặp trong (break).
- Độ phức tạp thời gian:
- Trong trường hợp xấu nhất (ví dụ $ai = 1$ với mọi $i$), số lượng cặp $(i, j)$ cần kiểm tra xấp xỉ tổng các số $\lfloor n/i \rfloor$ với $i$ từ $1$ đến $n$. Tổng này xấp xỉ $n \ln n$. Do đó độ phức tạp là $O(n \log n)$.
- Tuy nhiên, nếu $ai$ tăng nhanh (ví dụ $ai = 2^i$), vòng lặp sẽ dừng rất sớm. Nếu $ai \ge 2$ với mọi $i \ge 2$, vòng lặp sẽ dừng ở $j=1$ hoặc $j=2$.
- Độ phức tạp không gian: $O(n)$ để mảng chứa dãy số.
Cách Brute Force (Tối ưu)
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<long long> a(n + 1);
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++) {
for(int j = 1; i * j <= n; j++) { // Kiểm tra ngay trong điều kiện vòng lặp
if(a[i * j] != a[i] * a[j]) {
cout << "NO";
return 0;
}
}
}
cout << "YES";
return 0;
}
- Time Complexity: O(n log n) (Worst case)
- Space Complexity: O(n)
Đây là phiên bản slightly optimized của brute force. Thay vì lặp $j$ từ $1$ đến $n$ rồi break, ta直接 đưa điều kiện $i \cdot j \le n$ vào phần kiểm tra của vòng lặp for.
- Cách hoạt động: Vòng lặp
jchạy chừng nào $i \times j \le n$. Điều này đảm bảo ta chỉ xét các cặp $(i, j)$ hợp lệ. - Tối ưu hóa: Tránh được lệnh
breakbên trong vòng lặp, code gọn hơn và logic rõ ràng hơn. Tuy nhiên về mặt độ phức tạp thuật toán thì vẫn giống cách 1. - Phân tích số lần lặp: Nếu $a1 \neq 1$, ta có thể break sớm. Ví dụ nếu $a1 = 0$, mọi giá trị $ak = 0$, vòng lặp vẫn chạy đủ $O(n \log n)$ lần. Nếu $a1 = 2$, $a2 = a1 \cdot a1 = 4$, $a4 = 16, \dots$ tăng nhanh, vòng lặp sẽ kết thúc sớm.
Cách Giả lập theo số mũ (Mathematical)
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<long long> a(n + 1);
for(int i = 1; i <= n; i++) cin >> a[i];
// Xử lý trường hợp đặc biệt a[1]
// Logic bài này yêu cầu a[1] = a[1]*a[1] => a[1] = 0 hoặc 1
// Tuy nhiên, nếu a[1]=0 thì a[k] phải = 0 với mọi k.
// Nếu a[1]=1 thì a[k] = a[1]*a[k] => a[k]=a[k] (luôn đúng).
// Điều kiện chính là a[k] = a[i]*a[j] khi k=i*j.
// Chỉ cần kiểm tra các cặp (i, j) sao cho i*j <= n
// Nhưng có thể tối ưu bằng cách chỉ cần kiểm tra các thừa số nguyên tố?
// Không, vì không có giả định về tính đơn điệu.
// Cách tiếp cận tối ưu nhất:
// Chỉ cần kiểm tra các cặp (i, j) mà i*j <= n.
// Tuy nhiên, ta có thể nhận thấy:
// Nếu dãy thỏa mãn, thì a[i] = a[1]^i (nếu coi a[1] là hệ số).
// Nhưng a[1] có thể bằng 0.
// Ta có thể kiểm tra bằng cách:
// Duyệt i từ 2 đến sqrt(n), j từ i đến n/i.
// Hoặc đơn giản là dùng Brute Force đã tối ưu ở trên vì n <= 10^5.
// O(n log n) hoàn toàn có thể chấp nhận được.
// Code này là phiên bản Brute Force tối ưu đã được đề cập ở trên.
// Để thêm một hướng tiếp cận 'khác', ta có thể dùng đệ quy hoặc map, nhưng không tối ưu bằng.
// Dưới đây là code Brute Force tối ưu (giống Solution 1) để đảm bảo tính chính xác và hiệu quả.
for (int i = 1; i <= n; ++i) {
if (a[i] == 0) continue; // Nếu a[i] = 0, chỉ cần kiểm tra a[i*j] == 0
for (int j = 1; i * j <= n; ++j) {
// Kiểm tra điều kiện nhân
// Nếu a[i] * a[j] tràn số long long thì sao?
// a[i], a[j <= 10^9. Product <= 10^18. Long long (2^63 ~ 9e18) đủ.
if (a[i * j] != a[i] * a[j]) {
cout << "NO";
return 0;
}
}
}
cout << "YES";
return 0;
}
- Time Complexity: O(n log n)
- Space Complexity: O(n)
Hướng tiếp cận này mặc dù mã nguồn tương tự Brute Force tối ưu, nhưng nhấn mạnh vào việc phân tích tính chất toán học của bài toán.
Phân tích tính chất:
- Nếu $a1 \neq 1$, ví dụ $a1 = 0$, thì điều kiện $a{1 \cdot j} = a1 \cdot aj$ trở thành $aj = 0 \cdot aj = 0$. Do đó, nếu $a1 = 0$, toàn bộ dãy phải là các số 0.
- Nếu $a_1 = 1$, điều kiện này không ép buộc gì thêm (trừ khi kết hợp với các điều kiện khác).
- Điều kiện $a{ij} = ai \times a_j$ giống như định nghĩa của một hàm nhân (multiplicative function) hoặc một dãy lũy thừa.
Tại sao Brute Force là đủ?:
- Với $N=10^5$, thuật toán $O(N \log N)$ thực thi khoảng $10^6$ đến $1.5 \times 10^6$ thao tác, hoàn toàn chạy được trong 1s.
- Các giải pháp khác (như dùng Hash Map hoặc tìm thừa số nguyên tố) có thể phức tạp hơn và dễ sai sót do phải xử lý số 0 hoặc tràn số.
Cải tiến nhỏ:
- Trong vòng lặp, nếu
a[i] == 0, ta chỉ cần kiểm traa[i*j] == 0thay vì nhân 0 vớia[j]. Tuy nhiên, phép nhân0 * a[j]rất nhanh.
- Trong vòng lặp, nếu
Lưu ý dữ liệu:
- Sử dụng
long longcho mảngavà biến tích là bắt buộc vì $10^9 \times 10^9 = 10^{18}$.
- Sử dụng
Tóm lại, giải pháp này là sự lựa chọn tối ưu nhất về độ cân bằng giữa độ phức tạp, độ khó code và độ an toàn.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(n^2) hoặc O(n log n) | O(n) | Brute Force (Lặp kép) |
| 2 | O(n log n) (Worst case) | O(n) | Brute Force (Tối ưu) |
| 3 | O(n log n) | O(n) | Giả lập theo số mũ (Mathematical) |
Bài học kinh nghiệm
- Độ phức tạp thuật toán: Vòng lặp lồng nhau với điều kiện dừng sớm (khi $i \times j > n$ hoặc khi tích quá lớn) cho độ phức tạp $O(N \log N)$ trong trường hợp xấu nhất (khi tất cả các phần tử đều là 1). Đây là độ phức tạp chấp nhận được cho $N=10^5$.
- Sử dụng kiểu dữ liệu: Vì giá trị $ai$ lên tới $10^9$, tích $ai \times a_j$ có thể lên tới $10^{18}$, vượt quá giới hạn của
int(khoảng $2 \times 10^9$). Do đó, bắt buộc phải dùnglong long(trong C++) để lưu trữ mảng và tính toán tích. - Trường hợp đặc biệt của $a1$: Nếu $a1 = 0$, thì với mọi $j$, $aj = a{1 \cdot j} = a1 \cdot aj = 0$. Do đó, nếu $a1 = 0$, toàn bộ dãy phải là 0. Nếu $a1 \neq 0$ (và $a_1 \neq 1$), bài toán vẫn có thể có đáp án 'NO' ngay lập tức.
Lỗi thường gặp
- Tràn số nguyên (Integer Overflow): Sử dụng
intthay cholong longđể tính tích $a[i] * a[j]$ hoặc lưu mảng sẽ dẫn đến kết quả sai (số âm hoặc giá trị nhỏ không đúng do tràn số). - Lặp quá nhiều (Time Limit Exceeded): Các giải pháp lặp hai vòng lặp tự do từ $1$ đến $n$ không kiểm tra điều kiện $i \times j \le n$ có thể bị TLE nếu $n$ lớn và dữ liệu đầu vào 'dễ' (ví dụ tất cả bằng 1). Tuy nhiên, giải pháp trong bài vẫn giữ an toàn bằng cách break hoặc kiểm tra điều kiện.
- Bỏ qua chỉ số 1: Một số người có thể bỏ qua kiểm tra $i=1$ hoặc $j=1$ vì nghĩ nó hiển nhiên, nhưng thực tế nó quy định giá trị của $aj$ (ví dụ $a2 = a1 \cdot a2$ implies $a1=1$ hoặc $a2=0$). Tuy nhiên, vòng lặp tự nhiên sẽ bao quát hết.
- Input/Output nhanh: Với $n=10^5$, việc đọc/ghi lượng lớn số liệu có thể chậm nếu không dùng
ios::sync_with_stdio(false); cin.tie(nullptr);.
Bình luận