Bài 10.5: Bài tập thực hành lý thuyết số với hàm trong C++
Bài 10.5: Bài tập thực hành lý thuyết số với hàm trong C++
Chào mừng các bạn quay trở lại với hành trình khám phá C++ cùng FullhouseDev! Trong các bài viết trước, chúng ta đã tìm hiểu về những kiến thức cơ bản và cấu trúc điều khiển. Hôm nay, chúng ta sẽ đi sâu vào một lĩnh vực vừa cổ điển vừa hiện đại: lý thuyết số, và quan trọng hơn, cách chúng ta áp dụng công cụ mạnh mẽ là hàm (functions) trong C++ để giải quyết các bài toán thuộc lĩnh vực này một cách hiệu quả và có tổ chức.
Lý thuyết số là một nhánh của toán học nghiên cứu về các số nguyên và tính chất của chúng. Tuy nghe có vẻ hàn lâm, nhưng lý thuyết số lại có vô số ứng dụng thực tế trong khoa học máy tính, từ mật mã học (cryptography), thuật toán (algorithms), cho đến những vấn đề tối ưu hóa.
Khi giải quyết các bài toán lý thuyết số trong lập trình, chúng ta thường gặp phải những thao tác lặp đi lặp lại, ví dụ như kiểm tra một số có phải số nguyên tố không, hay tìm ước chung lớn nhất của hai số. Đây chính là lúc hàm phát huy vai trò của mình! Bằng cách đóng gói các logic xử lý cụ thể vào các hàm riêng biệt, chúng ta không chỉ làm cho code của mình dễ đọc, dễ bảo trì mà còn có thể tái sử dụng chúng ở nhiều nơi khác nhau trong cùng một chương trình hoặc thậm chí là các chương trình khác.
Hãy cùng đi qua một số bài toán lý thuyết số cơ bản và xem cách chúng ta xây dựng các hàm C++ để giải quyết chúng nhé!
Kiểm tra số nguyên tố (Primality Test)
Bài toán kinh điển nhất trong lý thuyết số là kiểm tra xem một số nguyên dương có phải là số nguyên tố hay không. Một số nguyên tố là số tự nhiên lớn hơn 1, chỉ có hai ước dương phân biệt là 1 và chính nó.
Để kiểm tra một số n có phải số nguyên tố không, chúng ta chỉ cần kiểm tra xem nó có chia hết cho bất kỳ số nào từ 2 đến căn bậc hai của n hay không. Nếu có, nó không phải số nguyên tố. Nếu không, nó là số nguyên tố.
Dưới đây là hàm C++ thực hiện kiểm tra này:
#include <iostream>
#include <cmath>
bool nguyenTo(int n) {
if (n <= 1) {
return false;
}
for (int i = 2; i * i <= n; ++i) {
if (n % i == 0) {
return false;
}
}
return true;
}
int main() {
int so1 = 17;
int so2 = 4;
if (nguyenTo(so1)) {
cout << so1 << " là số nguyên tố." << endl;
} else {
cout << so1 << " không phải là số nguyên tố." << endl;
}
if (nguyenTo(so2)) {
cout << so2 << " là số nguyên tố." << endl;
} else {
cout << so2 << " không phải là số nguyên tố." << endl;
}
return 0;
}
Output:
17 là số nguyên tố.
4 không phải là số nguyên tố.
Giải thích code:
- Hàm
isPrimenhận vào một số nguyênnvà trả vềtruenếunlà số nguyên tố, ngược lại trả vềfalse. - Chúng ta xử lý trường hợp
n <= 1ngay đầu vì chúng không phải số nguyên tố theo định nghĩa. - Vòng lặp
forbắt đầu từi = 2. Điều kiện lặpi * i <= ntương đương vớii <= sqrt(n)nhưng tránh sử dụng số thực, giúp tăng độ chính xác và hiệu quả một chút. - Bên trong vòng lặp, nếu
n % i == 0, nghĩa lànchia hết choi(một số khác 1 và nhỏ hơn hoặc bằng căn bậc hai củan), thìnkhông phải số nguyên tố, chúng ta trả vềfalsengay lập tức. - Nếu vòng lặp kết thúc mà không tìm thấy ước nào, điều đó có nghĩa
nchỉ chia hết cho 1 và chính nó (đối vớin > 1), nên nó là số nguyên tố, chúng ta trả vềtrue. - Hàm
mainminh họa cách gọi hàmisPrimevới hai số khác nhau và in ra kết quả.
Tìm ước chung lớn nhất (GCD - Greatest Common Divisor)
Ước chung lớn nhất (GCD) của hai số nguyên a và b là số nguyên dương lớn nhất chia hết cho cả a và b mà không dư. Ví dụ, GCD của 48 và 18 là 6.
Để tìm GCD, chúng ta có thể sử dụng thuật toán Euclidean, một trong những thuật toán lâu đời và hiệu quả nhất. Thuật toán này dựa trên nguyên lý: GCD của hai số a và b (với a > b) bằng GCD của b và phần dư của a khi chia cho b (a % b). Quá trình này lặp lại cho đến khi phần dư bằng 0; khi đó, số còn lại chính là GCD.
Đây là hàm C++ dựa trên thuật toán Euclidean (phiên bản lặp):
#include <iostream>
#include <cmath>
int ucln(int a, int b) {
a = abs(a);
b = abs(b);
while (b != 0) {
int t = b;
b = a % b;
a = t;
}
return a;
}
int main() {
int so1 = 48;
int so2 = 18;
int uc = ucln(so1, so2);
cout << "Ước chung lớn nhất của " << so1 << " và " << so2 << " là: " << uc << endl;
return 0;
}
Output:
Ước chung lớn nhất của 48 và 18 là: 6
Giải thích code:
- Hàm
findGCDnhận hai số nguyênavàb. Chúng ta sử dụngabsđể đảm bảo tính toán với giá trị tuyệt đối, giúp xử lý đúng cả khi số nhập vào là âm. - Vòng lặp
while (b != 0)tiếp tục cho đến khi số thứ hai (ban đầu làb, sau mỗi lần lặp là phần dư) bằng 0. - Bên trong vòng lặp:
int temp = b;lưu trữ giá trị hiện tại củab.b = a % b;tính phần dư củaachia chobvà gán chobmới.a = temp;gán giá trịbcũ (đã lưu trongtemp) choamới.
- Khi vòng lặp dừng lại (do
bbằng 0), giá trị cuối cùng củaachính là GCD của hai số ban đầu. - Hàm
mainminh họa cách gọi hàmfindGCDvà in kết quả.
Tìm bội chung nhỏ nhất (LCM - Least Common Multiple)
Bội chung nhỏ nhất (LCM) của hai số nguyên a và b là số nguyên dương nhỏ nhất chia hết cho cả a và b. Ví dụ, LCM của 12 và 18 là 36.
LCM có mối liên hệ chặt chẽ với GCD qua công thức sau:
lcm(a, b) = (|a * b|) / gcd(a, b)
Chúng ta có thể sử dụng lại hàm findGCD đã xây dựng để tính LCM.
Dưới đây là hàm C++ tìm LCM:
#include <iostream>
#include <cmath>
int ucln(int a, int b) {
a = abs(a);
b = abs(b);
while (b != 0) {
int t = b;
b = a % b;
a = t;
}
return a;
}
long long bcnn(int a, int b) {
if (a == 0 || b == 0) {
return 0;
}
return (long long)abs(a) / ucln(a, b) * abs(b);
}
int main() {
int so1 = 12;
int so2 = 18;
long long bc = bcnn(so1, so2);
cout << "Bội chung nhỏ nhất của " << so1 << " và " << so2 << " là: " << bc << endl;
return 0;
}
Output:
Bội chung nhỏ nhất của 12 và 18 là: 36
Giải thích code:
- Hàm
findLCMnhận hai số nguyênavàb. - Chúng ta xử lý trường hợp
a == 0hoặcb == 0vì LCM trong trường hợp này là 0. - Để tính LCM, chúng ta sử dụng công thức
(|a * b|) / gcd(a, b). - Một điểm quan trọng cần lưu ý là tích
a * bcó thể vượt quá giới hạn của kiểuintnếuavàbđủ lớn. Do đó, chúng ta sử dụng kiểulong longcho giá trị trả về của hàmfindLCMvà khi thực hiện phép tính. - Cách tính
(long long)abs(a) / findGCD(a, b) * abs(b)được ưu tiên hơn là(long long)abs(a) * abs(b) / findGCD(a, b)để giảm thiểu nguy cơ tràn số trung gian. Bằng cách chiaabs(a)cho GCD trước rồi mới nhân vớiabs(b), chúng ta làm việc với các số có giá trị trung gian nhỏ hơn. - Hàm
mainminh họa cách gọi hàmfindLCMvà in kết quả.
Phân tích thừa số nguyên tố (Prime Factorization)
Phân tích thừa số nguyên tố là quá trình biểu diễn một số nguyên dương lớn hơn 1 dưới dạng tích của các số nguyên tố. Ví dụ: 12 = 2 * 2 * 3, 30 = 2 * 3 * 5, 72 = 2 * 2 * 2 * 3 * 3.
Để phân tích một số n thành thừa số nguyên tố, chúng ta có thể áp dụng thuật toán đơn giản sau:
- Chia
ncho 2 liên tục cho đến khi nó không chia hết cho 2 nữa. In ra số 2 mỗi lần chia. - Sau khi xử lý số 2,
ntrở thành một số lẻ. Bắt đầu kiểm tra các số lẻ từ 3 trở lên. - Với mỗi số lẻ
i(bắt đầu từ 3, tăng dần 2 đơn vị), chianchoiliên tục cho đến khi nó không chia hết choinữa. In raimỗi lần chia. Chỉ cần kiểm tra các sốicho đến khii * i > n. - Nếu sau khi vòng lặp kết thúc mà
nvẫn còn lớn hơn 1, điều đó có nghĩa lànhiện tại là một số nguyên tố (và là thừa số nguyên tố lớn nhất), in ra giá trị củan.
Đây là hàm C++ thực hiện phân tích thừa số nguyên tố:
#include <iostream>
#include <cmath>
void phanTich(int n) {
if (n <= 1) {
cout << n << endl;
return;
}
while (n % 2 == 0) {
cout << 2 << " ";
n /= 2;
}
for (int i = 3; i * i <= n; i = i + 2) {
while (n % i == 0) {
cout << i << " ";
n /= i;
}
}
if (n > 1) {
cout << n << " ";
}
cout << endl;
}
int main() {
int so1 = 72;
cout << "Phân tích " << so1 << " thành thừa số nguyên tố: ";
phanTich(so1);
int so2 = 29;
cout << "Phân tích " << so2 << " thành thừa số nguyên tố: ";
phanTich(so2);
int so3 = 1;
cout << "Phân tích " << so3 << " thành thừa số nguyên tố: ";
phanTich(so3);
return 0;
}
Output:
Phân tích 72 thành thừa số nguyên tố: 2 2 2 3 3
Phân tích 29 thành thừa số nguyên tố: 29
Phân tích 1 thành thừa số nguyên tố: 1
Giải thích code:
- Hàm
primeFactorizationnhận một số nguyên dươngn. Hàm này sử dụngvoidvì nó chỉ thực hiện việc in ra các thừa số, không trả về một giá trị cụ thể. - Trường hợp
n <= 1được xử lý riêng. - Vòng lặp
while (n % 2 == 0)xử lý tất cả các thừa số 2. - Vòng lặp
forsau đó xử lý các thừa số lẻ. Nó bắt đầu từi = 3và chỉ kiểm tra các số lẻ (i = i + 2). Điều kiện lặpi * i <= ngiúp tối ưu hóa, vì nếuncó một thừa số nguyên tố lớn hơn căn bậc hai của nó, thì thừa số đó phải là chínhnsau khi đã chia hết cho các thừa số nhỏ hơn. - Vòng lặp
while (n % i == 0)bên trong vòng lặpforđảm bảo rằng chúng ta in ra và chiancho cùng một thừa sốinhiều lần nếu nó xuất hiện lặp lại trong phân tích. - Sau cùng, nếu
ncòn lại lớn hơn 1, nó chắc chắn là một số nguyên tố và là thừa số cuối cùng cần in ra. - Hàm
mainminh họa cách gọi hàmprimeFactorizationvới các số khác nhau.
Comments