Bài 9.1: Kiểm tra số nguyên tố trong C++
Bài 9.1: Kiểm tra số nguyên tố trong C++
Chào mừng bạn đến với bài viết tiếp theo trong chuỗi blog về C++! Hôm nay, chúng ta sẽ cùng khám phá một chủ đề quen thuộc nhưng đầy thú vị: kiểm tra xem một số có phải là số nguyên tố hay không trong ngôn ngữ C++. Đây là một bài toán kinh điển thường gặp, không chỉ giúp chúng ta rèn luyện kỹ năng lập trình mà còn hiểu sâu hơn về các thuật toán tối ưu.
Vậy, số nguyên tố là gì? Rất đơn giản, một số nguyên tố là số tự nhiên lớn hơn 1 và chỉ có hai ước dương phân biệt là 1 và chính nó. Ví dụ: 2, 3, 5, 7, 11... Ngược lại, các số như 1 (chỉ có một ước là 1), 4 (có ước 1, 2, 4), 6 (có ước 1, 2, 3, 6) không phải là số nguyên tố.
Mục tiêu của chúng ta là viết một hàm trong C++ nhận vào một số nguyên dương và trả về true nếu nó là số nguyên tố, false nếu không phải.
Hãy bắt đầu với cách tiếp cận đơn giản nhất nhé!
Phương pháp 1: Kiểm tra từ 2 đến n-1 (Phương pháp cơ bản)
Ý tưởng ban đầu rất trực quan: Để kiểm tra xem một số n có phải là số nguyên tố hay không, chúng ta chỉ cần xem liệu có bất kỳ số nào từ 2 đến n-1 mà n chia hết cho nó hay không. Nếu tìm thấy một số như vậy, thì n chắc chắn không phải là số nguyên tố. Nếu duyệt qua tất cả các số trong phạm vi đó mà không tìm thấy ước nào, thì n là số nguyên tố.
Chúng ta cần lưu ý xử lý các trường hợp đặc biệt:
- Các số <= 1 không phải là số nguyên tố.
- Số 2 là số nguyên tố duy nhất là số chẵn.
Đây là cách triển khai bằng C++:
bool laSNT(int n) {
if (n <= 1) return false;
for (int i = 2; i < n; ++i) {
if (n % i == 0) return false;
}
return true;
}
Giải thích code:
- Hàm
laSNTnhận vào một số nguyênnvà trả vềbool(truehoặcfalse). - Dòng
if (n <= 1): Kiểm tra các trường hợpnnhỏ hơn hoặc bằng 1. Các số này theo định nghĩa không phải là số nguyên tố, nên hàm trả vềfalsengay lập tức. - Vòng lặp
for (int i = 2; i < n; ++i): Bắt đầu kiểm tra từi = 2(ước nhỏ nhất có thể khác 1) cho đếni = n-1. - Dòng
if (n % i == 0): Sử dụng toán tử modulo (%) để kiểm tra xemncó chia hết choikhông. Nếun % ibằng 0, tức làilà một ước củanvàikhông phải là 1 (vìibắt đầu từ 2) vàikhông phải làn(vì vòng lặp dừng trướcn). - Nếu tìm thấy một ước như vậy, hàm trả về
falsengay lập tức mà không cần kiểm tra thêm. - Nếu vòng lặp kết thúc (duyệt hết các số từ 2 đến
n-1) mà không tìm thấy bất kỳ ước nào, điều đó có nghĩa lànchỉ chia hết cho 1 và chính nó. Hàm trả vềtrue, xác nhậnnlà số nguyên tố.
Phương pháp này rất dễ hiểu, nhưng bạn có thấy vấn đề gì không? Khi số n trở nên rất lớn, vòng lặp for sẽ phải chạy hàng tỷ lần, điều này làm cho chương trình của chúng ta chạy rất chậm! May mắn thay, chúng ta có thể cải thiện đáng kể hiệu suất.
Phương pháp 2: Kiểm tra đến căn bậc hai của n (Tối ưu 1)
Có một tính chất toán học rất hữu ích ở đây: Nếu một số n có một ước a lớn hơn căn bậc hai của n (a > sqrt(n)), thì n chắc chắn cũng phải có một ước b nhỏ hơn căn bậc hai của n (b < sqrt(n)). Điều này là do n = a * b. Nếu a lớn hơn sqrt(n), thì để tích a * b bằng n, b nhất định phải nhỏ hơn sqrt(n).
Vậy, thay vì kiểm tra tất cả các số từ 2 đến n-1, chúng ta chỉ cần kiểm tra các số từ 2 đến căn bậc hai của n (sqrt(n)). Nếu tìm thấy bất kỳ ước nào trong phạm vi này, n không phải là số nguyên tố. Nếu không tìm thấy, n là số nguyên tố.
Việc này giúp giảm đáng kể số lần lặp của vòng lặp! Ví dụ, để kiểm tra số 100,000,000, phương pháp cơ bản sẽ lặp gần 100 triệu lần, trong khi phương pháp này chỉ lặp khoảng sqrt(100,000,000) = 10,000 lần. Một sự cải thiện khổng lồ!
Hãy xem code C++ cho phương pháp này:
bool laSNT(int n) {
if (n <= 1) return false;
if (n == 2) return true;
for (int i = 2; i * i <= n; ++i) {
if (n % i == 0) return false;
}
return true;
}
Giải thích code:
- Chúng ta cần
#include <cmath>để sử dụng hàmsqrt(). - Các trường hợp đặc biệt
n <= 1vẫn được xử lý. - Trường hợp
n == 2được thêm vào để xử lý số 2 một cách nhanh chóng (vì 2 là số nguyên tố và là số chẵn duy nhất). - Vòng lặp
forbây giờ chỉ chạy từi = 2đến khii * ivượt quán. Điều kiệni * i <= ntương đương vớii <= sqrt(n), nhưng sử dụng phép nhân số nguyên an toàn hơn. - Bên trong vòng lặp, logic kiểm tra
n % i == 0vẫn giữ nguyên.
Phương pháp này hiệu quả hơn rất nhiều! Nhưng chúng ta có thể làm cho nó còn nhanh hơn nữa không? Chắc chắn rồi!
Phương pháp 3: Tối ưu hơn nữa (Chỉ kiểm tra số lẻ sau số 2)
Hãy suy nghĩ thêm một chút: Sau khi đã kiểm tra và loại bỏ số 2 (số nguyên tố chẵn duy nhất), tất cả các số nguyên tố khác (nếu có) phải là số lẻ. Điều này có nghĩa là chúng ta không cần kiểm tra tính chia hết cho bất kỳ số chẵn nào lớn hơn 2! Nếu một số n (lớn hơn 2) chia hết cho một số chẵn nào đó (ví dụ 4, 6, 8...), thì n chắc chắn cũng phải chia hết cho 2, và chúng ta đã loại trừ trường hợp đó.
Vì vậy, sau khi kiểm tra số 2 và các số chẵn > 2, chúng ta chỉ cần kiểm tra tính chia hết cho các số lẻ bắt đầu từ 3 cho đến căn bậc hai của n. Điều này giảm số lần kiểm tra đi một nửa so với phương pháp trước!
Đây là cách triển khai:
bool laSNT(int n) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0 || n % 3 == 0) return false;
for (int i = 5; i * i <= n; i = i + 6) {
if (n % i == 0 || n % (i + 2) == 0) return false;
}
return true;
}
Giải thích code:
- Các trường hợp
n <= 1,n <= 3được xử lý nhanh gọn. Số 2 và 3 được xác định là nguyên tố. - Dòng
if (n % 2 == 0 || n % 3 == 0): Kiểm tra xemncó chia hết cho 2 hoặc 3 không (đối vớin > 3). Nếu có, nó không phải số nguyên tố. - Vòng lặp
forbắt đầu từi = 5. - Điều kiện lặp
i * i <= nvẫn giới hạn phạm vi kiểm tra đến căn bậc hai. - Phần
i = i + 6: Đây là một thủ thuật tối ưu dựa trên quan sát rằng mọi số nguyên tố lớn hơn 3 đều có dạng6k - 1hoặc6k + 1. Vòng lặp này kiểm tra các ước tiềm năng có dạngi(ban đầu là 5, rồi 11, 17,...) vài + 2(ban đầu là 7, rồi 13, 19,...). Bằng cách tăngilên 6 mỗi lần, chúng ta bỏ qua các số chia hết cho 2 hoặc 3 (ví dụ: 6, 9, 10, 12, 15, 16,...). - Dòng
if (n % i == 0 || n % (i + 2) == 0): Kiểm tra xemncó chia hết choihoặci+2không. Nếu có,nkhông phải số nguyên tố.
Phương pháp này là một trong những cách phổ biến và khá hiệu quả để kiểm tra số nguyên tố cho các giá trị n không quá lớn.
Sử dụng các hàm đã viết
Bây giờ, hãy xem cách chúng ta có thể sử dụng một trong các hàm này trong chương trình C++ hoàn chỉnh, ví dụ dùng hàm tối ưu nhất:
#include <iostream>
bool laSNT(int n) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0 || n % 3 == 0) return false;
for (int i = 5; i * i <= n; i = i + 6) {
if (n % i == 0 || n % (i + 2) == 0) return false;
}
return true;
}
int main() {
int n;
cout << "Nhap so: ";
cin >> n;
if (laSNT(n)) {
cout << n << " la so nguyen to." << endl;
} else {
cout << n << " khong phai la so nguyen to." << endl;
}
return 0;
}
Output:
Nhap so: 17
17 la so nguyen to.
Giải thích code:
- Chúng ta
#include <iostream>để có thể sử dụngcinđể đọc dữ liệu từ bàn phím vàcout,endlđể in kết quả ra màn hình. - Hàm
laSNTđược định nghĩa (hoặc đảm bảo đã được đưa vào từ một file khác). - Trong hàm
main, chúng ta khai báo biếnnđể lưu trữ số người dùng nhập. - Dòng
cout << "Nhap so: ";hiển thị lời nhắc cho người dùng. - Dòng
cin >> n;đọc số nguyên do người dùng nhập và lưu vào biếnn. - Hàm
laSNT(n)được gọi. Kết quả trả về (truehoặcfalse) sẽ quyết định nhánhifnào được thực thi. - Dựa vào kết quả, chúng ta in ra thông báo phù hợp cho người dùng.
Tổng kết các phương pháp
Bạn thấy đấy, từ một bài toán đơn giản, chúng ta đã đi qua ba phương pháp khác nhau, mỗi phương pháp lại tối ưu hơn phương pháp trước đó về mặt hiệu suất:
- Cơ bản: Kiểm tra tất cả các số từ 2 đến
n-1. Rất chậm vớinlớn. - Tối ưu 1: Kiểm tra tất cả các số từ 2 đến
sqrt(n). Nhanh hơn đáng kể. - Tối ưu 2: Kiểm tra số 2 riêng, sau đó chỉ kiểm tra các số lẻ (hoặc theo mẫu 6k +/- 1) từ 3 đến
sqrt(n). Nhanh hơn nữa.
Bài tập ví dụ: C++ Bài 4.A6: Đồng hồ kỳ lạ
Một ngày nọ, có một người đàn ông cầm chiếc đồng hồ của anh ấy qua chỗ bạn và nhờ bạn một việc. Chiếc đồng hồ này rất kỳ lạ, nó chỉ có thể hiển thị một số nguyên không âm \(n\) là giây hiện tại của một ngày. Người này chia sẻ rằng chiếc đồng hồ này dùng để đáp ứng thói quen của mình, nhưng giờ anh ấy cần chỉnh lại để đồng hồ hiển thị giờ, phút, giây như mọi đồng hồ bình thường khác để tặng cho người bạn gái của anh ta.
Bạn cảm động trước tình yêu của người đàn ông này dành cho cô gái nên đã nhanh chóng mở máy tính lên và chuẩn bị viết chương trình chuyển đổi giúp người này.
INPUT FORMAT
Một dòng duy nhất chứa một số nguyên dương \(n\ (0\leq n < 86400)\).
OUTPUT FORMAT
In ra trên một dòng theo định dạng hh:mm:ss (hh là hai chữ số của giờ, mm là hai chữ số của phút, ss là hai chữ số của giây).
Ví dụ:
Input
3659
Output
01:00:59
Giải thích ví dụ mẫu:
Ví dụ:
- Giải thích: Số giây 3659 tương đương với 1 giờ, 0 phút, và 59 giây, nên định dạng là
01:00:59.
Đây là hướng dẫn giải bài tập chuyển đổi giây sang định dạng giờ:phút:giây (hh:mm:ss) trong C++. Yêu cầu là không cung cấp code hoàn chỉnh mà chỉ đưa ra phương pháp giải và gợi ý sử dụng các công cụ của thư viện chuẩn C++.
Phân tích bài toán:
Bạn được cho một số nguyên không âm n biểu thị tổng số giây đã trôi qua kể từ 00:00:00 của một ngày. Giá trị n nhỏ hơn 86400 (số giây trong một ngày). Nhiệm vụ là chuyển đổi n thành định dạng hh:mm:ss, trong đó hh, mm, ss luôn có hai chữ số (có thêm số 0 ở đầu nếu cần).
Hướng dẫn giải:
Đọc dữ liệu:
- Bạn cần đọc một số nguyên
ntừ đầu vào chuẩn. - Sử dụng
cinđể thực hiện việc này.
- Bạn cần đọc một số nguyên
Tính toán giờ (
hh), phút (mm), giây (ss):- Tính giờ (
hh): Mỗi giờ có 3600 giây (60 phút * 60 giây/phút). Để tìm số giờ đầy đủ trongngiây, bạn cần chiancho 3600. Sử dụng phép chia lấy phần nguyên (/) để có số giờ. - Tính số giây còn lại sau khi lấy giờ: Sau khi xác định được số giờ, số giây còn lại chưa đủ một giờ là phần dư khi chia
ncho 3600. Sử dụng phép chia lấy dư (%). - Tính phút (
mm): Số giây còn lại (từ bước trước) sẽ được dùng để tính số phút. Mỗi phút có 60 giây. Để tìm số phút đầy đủ trong số giây còn lại, bạn chia số giây còn lại cho 60. Sử dụng phép chia lấy phần nguyên (/). - Tính giây (
ss): Số giây cuối cùng (ss) là số giây còn lại sau khi lấy hết các phút đầy đủ. Đây chính là phần dư khi chia số giây còn lại (từ bước tính phút) cho 60. Sử dụng phép chia lấy dư (%).
Tóm lại, bạn có thể tính toán như sau:
hh = n / 3600mm = (n % 3600) / 60ss = n % 60
- Tính giờ (
Định dạng và In kết quả:
- Bạn cần in kết quả theo định dạng
hh:mm:ss. - Quan trọng là
hh,mm,ssphải luôn có hai chữ số. Nếu giá trị nhỏ hơn 10, cần thêm số 0 ở phía trước (zero-padding). - Thư viện chuẩn C++ cung cấp các công cụ để định dạng đầu ra trên
cout. Bạn có thể sử dụng các manipulators từ header<iomanip>. - Cụ thể,
setw(2)sẽ thiết lập chiều rộng trường in là 2 ký tự, vàsetfill('0')sẽ sử dụng ký tự '0' để lấp đầy khoảng trống nếu giá trị không đủ chiều rộng. - Để in
hhvới zero-padding, bạn sẽ sử dụngcout << setw(2) << setfill('0') << hh;. - Sau đó, in ký tự
:giữa các phần. - Lặp lại quy trình định dạng cho
mmvàss. - Cuối cùng, nhớ xuống dòng sau khi in xong.
- Bạn cần in kết quả theo định dạng
Các bước thực hiện trong code:
- Include các header cần thiết (
iostreamcho nhập/xuất,iomanipcho định dạng). - Đọc giá trị
ntừcin. - Thực hiện các phép tính để tìm
hh,mm,ss. - In
hhvới định dạng hai chữ số có zero-padding. - In ký tự
:. - In
mmvới định dạng hai chữ số có zero-padding. - In ký tự
:. - In
ssvới định dạng hai chữ số có zero-padding. - In ký tự xuống dòng (
endl).
Gợi ý sử dụng std:
cinđể đọc input.coutđể in output.setw()từ<iomanip>.setfill()từ<iomanip>.
Comments