Future Number 1 (FUTURENUM)
Đề bài
FUTURE NUMBER 1
Điểm: 300. Thời gian: 1.0s. Bộ nhớ: 64M. Input: bàn phím. Output: màn hình.
Định nghĩa: Số "tương lai" là số có các ước (không kể $1$ và chính nó) là các số nguyên tố.
Ví dụ: $10$ có $2$ ước thực sự là $2$ và $5$ là các số nguyên tố nên $10$ là số "tương lai".
Yêu cầu: Cho dãy số nguyên $a_1, a_2, \ldots, a_n$ ($1 \le n \le 10^4$ với mọi $i$ sao cho $1 \le a_i \le 10^6$). Hãy cho biết trong dãy trên có bao nhiêu số tương lai.
Input:
- Dòng thứ nhất gồm số nguyên $n$.
- Dòng thứ hai gồm các số $a_1, a_2, \ldots, a_n$.
Output:
- Số lượng số tương lai thỏa đề.
9
9 7 10 6 17 4 19 21 135Lờ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 viết lại thành bài giải đầy đủ để các bạn tham khảo nhen. Hy vọng mang đến được giá trị cho các bạn 😇.
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 OJHiểu đề trước nè: không tính $1$ và chính nó, nếu từng ước số còn lại đều là số nguyên tố thì số đó là số "tương lai". Ví dụ số $10$ có 4 ước là $1, 2, 5, 10$; bỏ $1$ và $10$ thì chỉ còn $2$ và $5$, cả hai đều là số nguyên tố ⇒ $10$ thỏa mãn. Việc của mình là đếm xem trong dãy $n$ số có bao nhiêu số như vậy.
Với test của đề, mình xét lần lượt từng số:
| Số | Ước thực sự (bỏ 1 và chính nó) | Tương lai? |
|---|---|---|
| 9 | 3 | ✅ |
| 7 | (không có) | ❌ |
| 10 | 2, 5 | ✅ |
| 6 | 2, 3 | ✅ |
| 17 | (không có) | ❌ |
| 4 | 2 | ✅ |
| 19 | (không có) | ❌ |
| 21 | 3, 7 | ✅ |
| 13 | (không có) | ❌ |
Có 5 số tương lai là $9, 10, 6, 4, 21$ ⇒ đáp án 5. Để ý là các số nguyên tố ($7, 17, 19, 13$) không được tính vì chúng chẳng có ước thực sự nào cả. Số $1$ cũng vậy.
Lộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Liệt kê ước + hàm kiểm tra nguyên tố – O(n · √X · √X)
Muốn biết $x$ có phải số tương lai không thì mình cần phân tích xem $x$ (không tính $1$ và chính nó) có những ước số cụ thể là số nào. Đây chính là bài toán quen thuộc: liệt kê các ước số của một số.
Cách liệt kê ước nhanh là chỉ cho $i$ chạy từ $2$ đến $\sqrt{x}$: mỗi khi $x$ chia hết cho $i$ thì ta có luôn một cặp ước là $i$ và $\dfrac{x}{i}$. Cặp này đi chung với nhau nên chỉ cần duyệt tới $\sqrt{x}$ là gặp đủ hết các ước rồi.
Một nhận xét nhỏ: với số nguyên dương $x$ bất kỳ, ước số lớn nhất của nó chính là $x$, còn các ước khác đều nhỏ hơn $x$ (vì đã là ước thì $x$ phải chia hết cho nó). Bỏ chính nó ra thì ước lớn nhất chỉ còn tối đa là $\dfrac{x}{2}$. Với $a_i \le 10^6$ thì mọi ước cần kiểm tra đều nằm trong đoạn $[2, 5 \cdot 10^5]$.
Ở cách này, mỗi ước mình kiểm tra nguyên tố bằng một hàm riêng isPrime(v) thử chia từ $2$ đến $\sqrt{v}$.
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;
bool isPrime(int x){
if(x < 2) return false;
for(int i = 2; i * i <= x; ++i){
if(x % i == 0) return false;
}
return true;
}
int main(){
int n;
cin >> n;
int res = 0;
while(n--){
int x;
cin >> x;
if(x > 1 && !isPrime(x)){
bool flag = true;
for(int i = 2; i * i <= x; ++i){
if(x % i == 0){
if(!isPrime(i) || !isPrime(x / i)){
flag = false;
break;
}
}
}
if(flag) res++;
}
}
cout << res;
return 0;
}
import sys
def is_prime(x):
if x < 2:
return False
i = 2
while i * i <= x:
if x % i == 0:
return False
i += 1
return True
def main():
data = sys.stdin.read().split()
n = int(data[0])
res = 0
for k in range(1, n + 1):
x = int(data[k])
if x > 1 and not is_prime(x):
flag = True
i = 2
while i * i <= x:
if x % i == 0:
if not is_prime(i) or not is_prime(x // i):
flag = False
break
i += 1
if flag:
res += 1
print(res)
main()
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(1)$, đọc số nào xử lý số đó.
- Thời gian: mỗi số tốn $O(\sqrt{X})$ để duyệt $i$, cộng thêm mỗi lần gặp ước thì gọi hàm kiểm tra nguyên tố tốn thêm $O(\sqrt{X})$. Thô nhất là $O(n \cdot \sqrt{X} \cdot \sqrt{X})$, với $X = 10^6$ là giá trị lớn nhất.
Cách 2Sàng Eratosthenes + liệt kê ước – O(X log log X + n√X)
Một kinh nghiệm mình muốn các bạn ghi nhớ: nếu trong ngữ cảnh chỉ cần kiểm tra một hoặc vài số có là số nguyên tố hay không thì ta có thể tự viết một hàm kiểm tra riêng cho từng số. Nhưng với những bài toán cần kiểm tra cả một danh sách nhiều số, và biết rằng các số đó nằm trong một phạm vi nhỏ (kiểu từ $1$ đến tối đa $10^7$), thì tốt nhất đừng gọi hàm kiểm tra cho từng số nữa, mà hãy dùng sàng nguyên tố Eratosthenes.
Sàng một lần từ $1$ đến $N = 10^6$, sau đó với một số bất kỳ ta biết được ngay lập tức trong $O(1)$ nó có là số nguyên tố hay không, chỉ việc tra mảng isPrime. Bạn nào chưa biết sàng thì xem bài viết này nhé, đọc từ đầu đến hết phần "Sàng nguyên tố Eratosthenes": Lý thuyết số – VNOI Wiki.
- Sàng Eratosthenes:
isPrime[i]cho mọi $i \le 10^6$. - Với mỗi số $x$: bỏ qua nếu $x = 1$ hoặc $x$ là số nguyên tố (không có ước thực sự).
- Duyệt $i$ từ $2$ khi $i \cdot i \le x$: gặp $x \bmod i = 0$ thì tra
isPrime[i]vàisPrime[x / i], một trong hai sai thìflag = falsevà thoát vòng lặp. - Cuối cùng
flagvẫn đúng thì tăngres.
Minh hoạ xét các cặp ước
Các bạn nhập dãy số rồi bấm Tiếp để xem với mỗi số, $i$ chạy từ $2$ đến $\sqrt{x}$ như thế nào, gặp cặp ước $(i, x/i)$ thì tra bảng sàng ra sao (xanh = cặp ước đều nguyên tố, đỏ = có ước không nguyên tố ⇒ dừng sớm):
Source code để các bạn tham khảo nếu làm theo hướng này (đây là code mình đã nộp AC):
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
bool isPrime[N + 1];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
for(int i = 0; i <= N; ++i){
isPrime[i] = true;
}
isPrime[0] = false;
isPrime[1] = false;
for(int i = 2; i * i <= N; ++i){
if(isPrime[i] == true){
for(int j = i * i; j <= N; j += i)
isPrime[j] = false;
}
}
int n;
cin >> n;
int res = 0;
while(n--){
int x;
cin >> x;
if(!isPrime[x] && x > 1){
bool flag = true;
for(int i = 2; i * i <= x; ++i){
if(x % i == 0){
if(!isPrime[i] || !isPrime[x / i]){
flag = false;
break;
}
}
}
if(flag){
res++;
}
}
}
cout << res;
return 0;
}
import sys
def main():
N = 10 ** 6
is_prime = bytearray([1]) * (N + 1)
is_prime[0] = 0
is_prime[1] = 0
i = 2
while i * i <= N:
if is_prime[i]:
# gạch bỏ các bội i*i, i*i + i, ... (gán cả đoạn một lần cho nhanh)
is_prime[i * i::i] = bytes(len(range(i * i, N + 1, i)))
i += 1
# Python chậm hơn C++ nhiều nên chỉ thử chia cho các số nguyên tố <= 1000
# (ước nhỏ nhất > 1 của x luôn là số nguyên tố, kết quả không đổi)
small = [p for p in range(2, 1001) if is_prime[p]]
data = sys.stdin.buffer.read().split()
n = int(data[0])
res = 0
for k in range(1, n + 1):
x = int(data[k])
if not is_prime[x] and x > 1:
flag = True
for i in small:
if i * i > x:
break
if x % i == 0:
if not is_prime[x // i]:
flag = False
break
if flag:
res += 1
print(res)
main()
Các bạn bấm chạy tay code C++ để xem từng dòng. Phần sàng chạy tới $10^6$ nên mình chỉ cho xem chi tiết vài giá trị $i$ đầu, phần còn lại gom thành một bước:
Đánh giá độ phức tạp của cách làm này:
- Không gian: mảng
isPrimecó $10^6 + 1$ phần tử kiểubool, khoảng 1 MB. - Thời gian: bước sàng Eratosthenes từ $1$ đến $X$ có độ phức tạp $O(X \log \log X)$; với $X = 10^6$ thì cỡ hơn 4 triệu bước. Bước xét từng số: mỗi số tốn tối đa $\sqrt{X} = 10^3$ bước (nếu gặp ước không nguyên tố thì còn kết thúc sớm), mỗi lần tra sàng chỉ tốn $O(1)$. Có tối đa $n = 10^4$ số ⇒ $10^4 \cdot 10^3 = 10^7$ bước.
- Tổng cộng khoảng $4$ triệu $+ 10$ triệu $= 14$ triệu, nhỏ hơn nhiều so với ngưỡng 1 giây (khoảng 30 – 50 triệu, tối đa lắm là 100 triệu phép tính) nên các bạn yên tâm nha.
- 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