You are on page 1of 14

I.

Số nguyên tố hoàn hảo

Thì cách cơ bản nhất đó là vét cạn xét hết tất cả mọi trường hợp có thể có, nghĩa là xét
hết tất cả các số có N chữ số rồi với mỗi số trong đó ta đi kiểm tra xem nó có thoả là số
nguyên tố hoàn hảo không? Nếu số nào thoả thì tăng biến đếm lên. Sau cùng ta có biến
đếm chính là kết quả bài toán.
Dễ dàng thấy cách này sẽ không ổn, vì việc xét qua tất cả các số có N chữ số, chưa xét
đến độ phức tạp của việc đi kiểm tra từng số đó mà chỉ việc xét qua tất cả các số đó thì độ
phức tạp rất lớn. Cụ thể với số có N chữ số thì ta phải xét phạm vi từ 10^(N-1) đến
(10^N) - 1. Nếu N <= 8 thì còn có thể chạy được với giới hạn thời gian 1 giây. Nhưng
nếu từ N > 8 cụ thể khi N là 9, 10, 11, 12 thì miền giá trị các số phải xét rất lớn, lúc này
vượt qua ngưỡng giới hạn 1 giây rồi. Ví dụ nếu N = 9 đi thì ta phải xét từ 10^8 (100 triệu)
đến (10^9) - 1 (999 triệu 999 ngàn 999) lúc này số lượng các số phải xét đến là 10^9 -
10^8 = 900 triệu. Trong khi đó ngưỡng giới hạn 1 giây thì vòng lặp chỉ chạy tối đa ở
ngưỡng (3 đến 5)*10^7 tức là 30 đến 50 triệu hay cao lắm là 10^8 tức là 100 triệu thôi là
quá mức rồi. Ở đây đến 900 triệu số phải xét là vượt ngưỡng gấp cả 9 lần rồi. Mà mới N
= 9 đã vậy, với N = 10, 11, 12 thì còn khủng khiếp cỡ nào nữa. Nên cách làm này cùng
lắm nếu bạn nào không nhìn ra được cách làm tối ưu thì buộc làm cách này để vét được
điểm nào hay điểm đó thôi.
Source code cho cách làm vét cạn ở trên để các bạn tham khảo thấy rõ những gì mình
phân tích:
#include <bits/stdc++.h>
using namespace std;
bool isPrime(long long x){
for(long long i = 2; i * i <= x; ++i){
if(x % i == 0){
return false;
}
}
return x >= 2;
}
bool isPalind(long long x){
long long rever = 0;
long long temp = x;
while(temp){
rever = rever * 10 + temp % 10;
temp /= 10;
}
return rever == x;
}
bool isPerfectPrime(long long x){
if(!isPalind(x) || !isPrime(x)){
return false;
}
while(x){
if(!isPrime(x % 10)){
return false;
}
x /= 10;
}
return true;
}
int main(){
int n;
cin >> n;
long long start = (long long)pow(10.0, n - 1);
long long finish = (long long)pow(10.0, n) - 1;
long long cnt = 0;
for(long long i = start; i <= finish; ++i){
if(isPerfectPrime(i)){
cnt++;
}
}
cout << cnt;
return 0;
}
Đánh giá độ phức tạp của cách làm này:
+ Độ phức tạp không gian (Space Complexity): O(1).
+ Độ phức tạp thời gian (Time Complexity): O((10^N - 10^(N-1)) * X) với X là độ phức
tạp của bước đi kiểm tra số nguyên tố hoàn hảo, N là số lượng chữ số cần tìm.
Như đã phân tích ở trên thì cách này chắc chắn sẽ bị TLE. Tuy nhiên hãy vẫn nên cài đặt
nó để sau này ta lấy nó làm công cụ kiểm tra tính chính xác cho cách làm tối ưu. Bạn nào
có chưa biết cách lấy nó làm công cụ kiểm tra tính chính xác thì có thể xem qua bài viết
này của mình nha:
Giờ ta sẽ nói về cách làm tốt hơn cho bài này: Trong các yếu tố để là số nguyên tố hoàn
hảo thì ta thấy có 2 yếu tố: Chỉ bao gồm các chữ số là số nguyên tố và số đó là số đối
xứng. Chính 2 yếu tố này kết hợp lại sẽ giúp ta giảm thiểu được rất nhiều số phải xét
không cần thiết, số lượng các số còn phải xét lúc này sẽ không bao nhiêu cả. Lúc này ta
chỉ cần đi kiểm tra tính nguyên tố với những số đó thôi rồi số nào thoả thì tăng biến đếm
lên.
Tại sao lại nói nhờ 2 yếu tố nói trên thì số lượng các số thoả mãn cần xét sẽ rất ít? Vì với
các chữ số từ 0 đến 9, trong đó thì chỉ có 4 chữ số: 2, 3, 5, 7 là các chữ số nguyên tố. Rồi
thì từ 4 chữ số này ta dựng lên các số đối xứng có độ dài N chỉ bao gồm 4 chữ số này bên
trong. Như vậy số lượng các số sẽ có phạm vi nhỏ dễ dàng tính được ngay. Cụ thể như
sau:
Với N = 1 thì các số thoả mãn là:
2
3
5
7
=> Tổng cộng có 4 số
Với N = 2 thì các số thoả mãn là:
22
33
55
77
=> Tổng cộng có 4 số
Từ N = 3 trở lên thì ta có quy luật tính như sau:
Ví dụ với N = 3 thì ta sẽ hiểu nó gồm tất cả các trường hợp của N = 1 và mỗi trường hợp
đó được kẹp giữa bởi 2 đầu là lần lượt các chữ số 2, 3, 5, 7 để tạo thành được số đối
xứng. Cụ thể các số đối xứng có 3 chữ số mà chỉ gồm các chữ số nguyên tố sẽ là:
222
232
252
272
323
333
353
373
525
535
555
575
727
737
757
777
=> Tổng cộng có 16 số
Ví dụ với N = 4 thì ta sẽ hiểu nó gồm tất cả các trường hợp của N = 2 và mỗi trường hợp
đó được kẹp giữa bởi 2 đầu là lần lượt các chữ số 2, 3, 5, 7 để tạo thành được số đối
xứng. Cụ thể các số đối xứng có 4 chữ số mà chỉ gồm các chữ số nguyên tố sẽ là:
2222
2332
2552
2772
3223
3333
3553
3773
5225
5335
5555
5775
7227
7337
7557
7777
=> Tổng cộng có 16 số
Và cứ quy luật như thế, ví dụ nếu N = 5 thì ta sẽ hiểu nó gồm tất cả các trường hợp của N
= 3 và mỗi trường hợp đó được kẹp giữa bởi 2 đầu là lần lượt các chữ số 2, 3, 5, 7 để tạo
thành được số đối xứng. Ví dụ lúc này sẽ là:
22222
22322
22522
22722
32223
32323
.... (còn tiếp)
Vậy quy luật tổng quát là với N >= 3 (riêng trường hợp N = 1, 2 ta chủ động để sẵn đáp
án) sẽ gồm tất cả các trường hợp của N-2 với mỗi trường hợp sẽ lần lượt được kẹp giữa
bởi 2 đầu là lần lượt các chữ số 2, 3, 5, 7 để sau cùng tạo thành được số đối xứng có N
chữ số.
Như vậy nghĩa là từ đầu ta xét nếu N là số chẵn, giả sử N = 10 thì như thế ta phải tính ra
được trường hợp N = 2 từ đó tính ra trường hợp N = 4 từ đó tính ra trường hợp N = 6 từ
đó tính ra trường hợp N = 8 rồi thì lúc này mới tính ra được trường hợp N = 10. Tương tự
nếu N là số lẻ, giả sử N = 9 thì như thế ta phải tính ra được trường hợp N = 1 từ đó tính ra
trường hợp N = 3 từ đó tính ra trường hợp N = 5 từ đó tính ra trường hợp N = 7 rồi thì lúc
này mới tính ra được trường hợp N = 9.
Dễ dàng nhẩm ra được số lượng các số của mỗi trường hợp sẽ là bao nhiêu: Ban đầu với
N = 1, 2 thì mỗi trường hợp đều có 4 số. Với N = 3 thì ta hiểu nó sẽ gấp 4 lần của trường
hợp N = 1. Với N = 4 thì ta hiểu nó sẽ gấp 4 lần của trường hợp N = 2. Rồi N = 5 sẽ gấp
4 lần của trường hợp N = 3. N = 6 sẽ gấp 4 lần của trường hợp N = 4 và cứ thế ...
Công thức tổng quát là:
N = 1, 2 => có 4 số
Với N >= 3 thì sẽ có 4 * (số lượng số của trường hợp N - 2)
Vậy ta tính ra được:
N = 1 => 4
N = 2 => 4
N = 3 => 4 * 4 = 16
N = 4 => 4 * 4 = 16
N = 5 => 4 * 16 = 64
N = 6 => 4 * 16 = 64
N = 7 => 4 * 64 = 256
N = 8 => 4 * 64 = 256
N = 9 => 4 * 256 = 1024
N = 10 => 4 * 256 = 1024
N = 11 => 4 * 1024 = 4096
N = 12 => 4 * 1024 = 4096
Như thế ta thấy trong trường hợp N lớn nhất là 12 theo như đề bài cho thì cả thảy lúc này
chỉ có 4096 số cần xét, lúc này mỗi số sẽ đi kiểm tra có phải là số nguyên tố không? Nếu
thoả thì tăng biến đếm lên sau cùng ta có đáp án số lượng các số nguyên tố hoàn hảo. Rõ
ràng các bạn sẽ thấy số lượng các số phải xét lúc này sẽ rất ít so với cách làm ban đầu nếu
N = 12 thì số lượng các số phải xét sẽ là 10^12 - 10^11 = 9 * 10^11 tức là 900 tỷ số phải
xét. Các bạn đã thấy nó được rút ngắn lại kinh khủng chưa?
Rồi thì giờ vấn đề chỉ là làm sao để ta tạo ra được đủ các số có độ dài N chỉ bao gồm các
chữ số nguyên tố 2, 3, 5, 7 và là số đối xứng như đã phân tích ở trên? Lúc này đây nếu
bạn nào vững kiến thức về đệ quy là có thể xử lý nhanh gọn, nhưng nếu bạn nào không
biết hay không vững đệ quy thì ở đây mình sẽ chỉ cho các bạn cách xử lý chỗ này không
cần dùng đệ quy để cho các bạn dễ hình dung. Tuy nhiên nó đòi hỏi các bạn phải có kiến
thức về cấu trúc dữ liệu hàng đợi (Queue) vì ta sẽ vận dụng nó để khử đệ quy trong bài
này. Bạn nào chưa biết có thể Google search tìm hiểu thêm nhé, nó cũng rất đơn giản thôi
không có gì phức tạp cả.
Thì cách xử lý nó như mình đã miêu tả quy trình ở trên cho các bạn thấy đó. Đầu tiên xét
N là chẵn hay lẻ? Nếu là lẻ thì khởi tạo một hàng đợi (Queue) chứa 4 giá trị chuỗi “2”,
“3”, “5”, “7”. Nếu là chẵn thì khởi tạo một hàng đợi (Queue) chứa 4 giá trị chuỗi “22”,
“33”, “55”, “77”. Tại sao lại dùng kiểu chuỗi? Bởi dùng chuỗi sẽ rất tiện trong việc tạo ra
số mới bởi chuỗi có thể nhanh chóng ghép được bởi phép toán + được hỗ trợ sẵn. Ví dụ
ta có string x = “22” thì để tạo ra string y = “2222” chỉ đơn giản y = “2” + x + “2”. Rồi
thì xét biến i nếu N là lẻ thì i = 1, N là chẵn thì i = 2. Vòng lặp sẽ lặp cho đến khi nào i
khác N còn nếu i bằng N thì vòng lặp kết thúc, mỗi bước lặp i += 2 tức là tăng i thêm 2
đơn vị. Để mục đích nếu N = 5 thì ta sẽ tính đủ các trường hợp i = 1 từ đó lên i = 3 từ đó
lên i = 5. Vì nó cần các dữ kiện của các trường hợp N-2 trước đó để tạo ra kết quả của
trường hợp N hiện tại. Ta cứ sinh ra các số của trường hợp i = 1 rồi từ đó sinh ra các số
của trường hợp i = 3 rồi từ đó sinh ra các số của trường hợp i = 5 và lúc này đây là các số
ta cần xét (có độ dài N = 5).
Thì với mỗi trường hợp vòng lặp i thì ta sẽ làm quy trình lặp lấy lần lượt từng giá trị từ
hàng đợi ra và ghép nó để tạo thành 4 giá trị mới rồi đẩy 4 giá trị mới đó vào lại hàng đợi
để có thể sẽ lấy nó tiếp tục sinh ra tiếp ở trường hợp lặp tiếp theo. Ví dụ ta có string s là
kết quả lấy ra từ hàng đợi hiện tại thì ta sẽ đưa thêm 4 giá trị mới vào hàng đợi là:
“2” + s + “2”
“3” + s + “3”
“5” + s + “5”
“7” + s + “7”
Cần lưu ý nếu khi lấy giá trị string s từ hàng đợi ra mà xét nếu thấy độ dài của s tức
s.length() mà bằng N tức là ta kết thúc quy trình lấy ngay vì giờ đây toàn bộ hàng đợi đều
đang chứa các số có độ dài N rồi, lúc này quy trình đi tạo ra số đã hoàn thành. Giờ đây
chỉ việc lấy từng số đó trong hàng đợi ra đi xét kiểm tra số nguyên tố và lấy hết cho đến
khi hàng đợi rỗng thì ta xuất đáp án số lượng các số là số nguyên tố, lúc này đó chính là
các số nguyên tố hoàn hảo.
Chạy tay thử, ví dụ đề cho N = 3 đi ha. Thì ban đầu xét thấy N là số lẻ nên ta khởi tạo i =
1 và hàng đợi mang 4 giá trị chuỗi lần lượt từ đầu hàng đợi trở xuống là:
2
3
5
7
Xét i = 1 khác N = 3 nên thoả vào vòng lặp
Lúc này đây lấy giá trị 2 ra khỏi hàng đợi, hàng đợi lúc này còn:
3
5
7
Với giá trị 2 lấy ra, ta thấy độ dài của nó là 1 đang khác N = 3 nên ta sẽ đi sinh ra 4 số
tiếp theo để đưa vào hàng đợi, 4 số được sinh ra là: 222, 323, 525, 727 và đưa 4 số này
vào hàng đợi, lúc này đây các giá trị hàng đợi đang chứa xét từ đầu đến cuối là:
3
5
7
222
323
525
727
Lúc này đây lấy giá trị 2 ra khỏi hàng đợi, hàng đợi lúc này còn:
3
5
7
Tiếp tục lấy giá trị đầu hàng đợi, lúc này là 3, ta thấy độ dài của nó là 1 đang khác N = 3
nên ta sẽ đi sinh ra 4 số tiếp theo để đưa vào hàng đợi, 4 số được sinh ra là: 232, 333,
535, 737 và đưa 4 số này vào hàng đợi, lúc này đây các giá trị hàng đợi đang chứa xét từ
đầu đến cuối là:
5
7
222
323
525
727
232
333
535
737
Tiếp tục lấy giá trị đầu hàng đợi, lúc này là 5, ta thấy độ dài của nó là 1 đang khác N = 3
nên ta sẽ đi sinh ra 4 số tiếp theo để đưa vào hàng đợi, 4 số được sinh ra là: 252, 353,
555, 757 và đưa 4 số này vào hàng đợi, lúc này đây các giá trị hàng đợi đang chứa xét từ
đầu đến cuối là:
7
222
323
525
727
232
333
535
737
252
353
555
757
Tiếp tục lấy giá trị đầu hàng đợi, lúc này là 7, ta thấy độ dài của nó là 1 đang khác N = 3
nên ta sẽ đi sinh ra 4 số tiếp theo để đưa vào hàng đợi, 4 số được sinh ra là: 272, 373,
575, 777 và đưa 4 số này vào hàng đợi, lúc này đây các giá trị hàng đợi đang chứa xét từ
đầu đến cuối là:
222
323
525
727
232
333
535
737
252
353
555
757
272
373
575
777
Tiếp tục lấy giá trị đầu hàng đợi, lúc này là 222, ta thấy độ dài của nó là 3 lúc này bằng
với N = 3 nên ta sẽ dừng quy trình sinh số lại vì từ giờ trở đi trong hàng đợi chỉ đang
chứa toàn giá trị các số có độ dài 3 nên không cần sinh nữa.
Lúc này i += 2 để thành i = 3. Lúc này i đã bằng N nên ta kết thúc quy trình. Chứ nếu giả
sử chỗ này mà N = 5 chẳng hạn thì ta phải tiếp tục lặp thêm 1 lần nữa nhé các bạn.
Rồi đó ở trên là mình đã chạy tay để các bạn hiểu quy trình, sau cùng thì hàng đợi chỉ
đang chứa toàn bộ các số đối xứng có độ dài N và chỉ gồm toàn các chữ số nguyên tố 2,
3, 5, 7. Giờ đây chỉ là lấy các giá trị này ra xét đi kiểm tra số nguyên tố, số nào thoả số
nguyên tố thì tăng biến đếm lên. Sau cùng xuất ra biến đếm thì đó là số lượng các số
nguyên tố hoàn hảo có N chữ số.
Source code để các bạn tham khảo thấy rõ những gì mình phân tích ở trên:
#include <bits/stdc++.h>
using namespace std;
bool isPrime(long long x){
for(long long i = 2; i * i <= x; ++i){
if(x % i == 0){
return false;
}
}
return x >= 2;
}
int main(){
int n;
cin >> n;
queue<string> q;
int i;
if(n % 2 != 0){
q.push("2");
q.push("3");
q.push("5");
q.push("7");
i = 1;
}
else{
q.push("22");
q.push("33");
q.push("55");
q.push("77");
i = 2;
}
while(i != n){
while(!q.empty()){
string s = q.front();
if(s.length() == n){
break;
}
q.pop();
q.push("2" + s + "2");
q.push("3" + s + "3");
q.push("5" + s + "5");
q.push("7" + s + "7");
}
i += 2;
}
int cnt = 0;
while(!q.empty()){
if(isPrime(stoll(q.front()))){
cnt++;
}
q.pop();
}
cout << cnt;
return 0;
}
Ở code ở trên do các giá trị trong hàng đợi đang là kiểu chuỗi nên khi gọi nó vào hàm
kiểm tra số nguyên tố thì ta phải đổi chuỗi thành số nhờ vào hàm stoll (string to long
long).
Đánh giá độ phức tạp của cách làm này:
+ Độ phức tạp không gian (Space Complexity): O(4^X) với X = N/2 nếu N chẵn hoặc X
= N/2 + 1 nếu N lẻ.
+ Độ phức tạp thời gian (Time Complexity): O(4^1 + 4^2 + ... 4^X) với X = N/2 nếu N
chẵn hoặc X = N/2 + 1 nếu N lẻ + O(4^X * Y) với Y là độ phức tạp của bước đi kiểm tra
số nguyên tố với từng số trong tập 4^X.
Thì code ở trên nếu chạy thử ta sẽ có kết luận đáp án tương ứng của từng trường hợp N
như sau:
N = 1: 4
N = 2: 0
N = 3: 4
N = 4: 0
N = 5: 11
N = 6: 0
N = 7: 18
N = 8: 0
N = 9: 77
N = 10: 0
N = 11: 218
N = 12: 0
Vậy nên nếu bạn nào sợ code ở trên lỡ chạy mà có quá giới hạn 1 giây vì lý do gì đó (ví
dụ máy tính chấm ngay thời điểm đó bị yếu đột xuất chẳng hạn ) thì chúng ta có thể
chơi mẹo như sau khi các bạn đã chạy chương trình trên máy tính biết được đáp án của 12
trường hợp N rồi thì ta có thể đưa trực tiếp nó vào bài làm luôn, cụ thể như sau:
#include <bits/stdc++.h>
using namespace std;
int main(){
int a[] = {-1, 4, 0, 4, 0, 11, 0, 18, 0, 77, 0, 218, 0};
int n;
cin >> n;
cout << a[n];
return 0;
}
Ta tạo mảng 13 phần tử (bỏ không dùng index 0) chỉ dùng các index từ 1 đến 12 để với
giá trị N ta đối chiếu gọi vào giá trị tại index N chính là đáp số. Làm như thế này không
ai bắt bẻ được chúng ta vì rõ ràng sẽ ra đáp án đúng như đề bài cần hihi và chắc chắn
cách này sẽ chạy nhanh nhất luôn rồi
Đánh giá độ phức tạp của cách làm này:
+ Độ phức tạp không gian (Space Complexity): O(1) vì mảng chỉ có 13 phần tử thì cũng
là 1 hằng số cố định nên vẫn xem là O(1).
+ Độ phức tạp thời gian (Time Complexity): O(1) vì tạo mảng có 13 phần tử cũng là số
lượng hằng số cố định và 1 phép truy xuất vị trí index N thì cũng chỉ O(1).

You might also like