Bài 3.1. Kiểm tra số nguyên tố và sàng Eratosthenes

Bài 3.1. Kiểm tra số nguyên tố và sàng Eratosthenes
Chào mừng quay trở lại với series blog về Cấu trúc dữ liệu và Giải thuật của chúng ta! Hôm nay, chúng ta sẽ khám phá một chủ đề kinh điển nhưng vô cùng quan trọng trong lập trình và toán học: số nguyên tố. Chúng ta sẽ không chỉ tìm hiểu cách kiểm tra xem một số có phải là nguyên tố hay không, mà còn khám phá một giải thuật cực kỳ hiệu quả để tìm tất cả các số nguyên tố trong một phạm vi nhất định: Sàng Eratosthenes. Hãy cùng bắt đầu hành trình khám phá này!
Số Nguyên Tố Là Gì?
Trước khi đi sâu vào các giải thuật, hãy làm rõ định nghĩa. Một số nguyên tố là một số nguyên dương lớn hơn 1 chỉ có hai ước số dương phân biệt là 1 và chính nó.
Ví dụ:
- 2 là số nguyên tố (ước: 1, 2)
- 3 là số nguyên tố (ước: 1, 3)
- 4 không phải là số nguyên tố (ước: 1, 2, 4)
- 5 là số nguyên tố (ước: 1, 5)
- 6 không phải là số nguyên tố (ước: 1, 2, 3, 6)
- 7 là số nguyên tố (ước: 1, 7)
Các số 0 và 1 theo định nghĩa không phải là số nguyên tố. Số 1 chỉ có một ước dương duy nhất là 1, còn số 0 có vô số ước.
Số nguyên dương lớn hơn 1 mà không phải là số nguyên tố được gọi là hợp số.
Việc kiểm tra và tìm kiếm số nguyên tố có rất nhiều ứng dụng trong thực tế, từ mã hóa (cryptography) cho đến các bài toán trong lý thuyết số và tin học. Do đó, việc nắm vững các phương pháp hiệu quả là cực kỳ cần thiết.
Phương Pháp 1: Kiểm Tra Số Nguyên Tố "Ngây Ngô" (Naive Check)
Phương pháp đơn giản nhất để kiểm tra một số n
có phải là số nguyên tố hay không dựa trực tiếp vào định nghĩa. Nếu n <= 1
, chắc chắn nó không phải số nguyên tố. Nếu n > 1
, ta chỉ cần kiểm tra xem nó có ước nào khác 1 và n
hay không. Các ước có thể có chỉ nằm trong khoảng từ 2 đến n-1
.
Chúng ta có thể duyệt từ 2 đến n-1
. Nếu tìm thấy bất kỳ số i
nào trong khoảng này mà n
chia hết cho i
(tức là n % i == 0
), thì n
có ít nhất 3 ước là 1, i
, và n
, do đó nó là hợp số. Nếu duyệt hết khoảng này mà không tìm thấy ước nào, thì n
là số nguyên tố.
Đây là code minh họa bằng C++:
#include <iostream>
// Hàm kiểm tra số nguyên tố theo phương pháp ngây ngô
bool isPrimeNaive(int n) {
// Số nhỏ hơn hoặc bằng 1 không phải số nguyên tố
if (n <= 1) {
return false;
}
// Duyệt từ 2 đến n-1
for (int i = 2; i < n; ++i) {
// Nếu n chia hết cho i, thì n không phải số nguyên tố
if (n % i == 0) {
return false;
}
}
// Nếu không tìm thấy ước nào trong khoảng từ 2 đến n-1, n là số nguyên tố
return true;
}
int main() {
int num1 = 7;
int num2 = 10;
int num3 = 1;
cout << num1 << " is prime? " << (isPrimeNaive(num1) ? "Yes" : "No") << endl;
cout << num2 << " is prime? " << (isPrimeNaive(num2) ? "Yes" : "No") << endl;
cout << num3 << " is prime? " << (isPrimeNaive(num3) ? "Yes" : "No") << endl;
return 0;
}
Giải thích code:
#include <iostream>
: Thư viện nhập xuất cơ bản trong C++.bool isPrimeNaive(int n)
: Định nghĩa một hàm nhận vào số nguyênn
và trả vềtrue
nếun
là nguyên tố,false
nếu ngược lại.if (n <= 1) { return false; }
: Xử lý trường hợp đặc biệt: số <= 1 không phải nguyên tố.for (int i = 2; i < n; ++i)
: Vòng lặp duyệt qua các sối
từ 2 đếnn-1
.if (n % i == 0)
: Kiểm tra xemn
có chia hết choi
hay không. Toán tử%
là toán tử lấy phần dư. Nếu phần dư bằng 0, tức làn
chia hết choi
.return false;
: Nếu tìm thấy ướci
, hàm kết thúc ngay lập tức và trả vềfalse
.return true;
: Nếu vòng lặp kết thúc mà không tìm thấy ước nào (tức là không có lần nàoreturn false
được gọi), thìn
là số nguyên tố, hàm trả vềtrue
.- Hàm
main
chỉ đơn giản là gọiisPrimeNaive
với vài ví dụ và in kết quả ra màn hình.
Đánh giá: Phương pháp này đúng, nhưng hiệu quả không cao lắm. Đối với một số n
, trong trường hợp xấu nhất (khi n
là số nguyên tố hoặc là hợp số của hai số nguyên tố lớn gần n/2
), ta phải thực hiện gần n
phép chia. Độ phức tạp thời gian của giải thuật này là O(n). Với các số n
lớn, điều này có thể rất chậm.
Phương Pháp 2: Cải Tiến Kiểm Tra Số Nguyên Tố (Optimized Check)
Chúng ta có thể nhận thấy một điểm quan trọng: Nếu i
là một ước của n
, thì n/i
cũng là một ước của n
. Ví dụ, nếu n = 100
, các cặp ước là (1, 100), (2, 50), (4, 25), (5, 20), (10, 10).
Quan sát các cặp ước (trừ trường hợp căn bậc hai chính xác): trong mỗi cặp (i, n/i)
, một số sẽ nhỏ hơn hoặc bằng căn bậc hai của n
, và số kia sẽ lớn hơn hoặc bằng căn bậc hai của n
. Ví dụ với n=100
, sqrt(100)=10
. Các cặp là (2, 50), (4, 25), (5, 20) - trong mỗi cặp, một số <= 10, một số >= 10. Cặp (10, 10) cả hai đều bằng 10.
Điều này có nghĩa là, nếu n
có bất kỳ ước nào khác 1 và n
, nó nhất định phải có một ước nhỏ hơn hoặc bằng căn bậc hai của n
. Ngược lại, nếu n
không có ước nào trong khoảng từ 2 đến sqrt(n)
, thì nó cũng sẽ không có ước nào lớn hơn sqrt(n)
(vì nếu có ước j > sqrt(n)
, thì n/j
sẽ là ước và n/j < sqrt(n)
, mâu thuẫn với giả định ban đầu).
Do đó, để kiểm tra số nguyên tố n
, ta chỉ cần duyệt các số i
từ 2 đến phần nguyên của sqrt(n)
. Nếu tìm thấy ước nào trong khoảng này, n
không phải là nguyên tố. Nếu duyệt hết mà không thấy, n
là nguyên tố.
Đây là code minh họa bằng C++:
#include <iostream>
#include <cmath> // Cần thư viện cmath cho hàm sqrt()
// Hàm kiểm tra số nguyên tố theo phương pháp cải tiến
bool isPrimeOptimized(int n) {
// Số nhỏ hơn hoặc bằng 1 không phải số nguyên tố
if (n <= 1) {
return false;
}
// Số 2 là số nguyên tố duy nhất chẵn
if (n == 2) {
return true;
}
// Các số chẵn lớn hơn 2 không phải số nguyên tố
if (n % 2 == 0) {
return false;
}
// Duyệt từ 3 đến sqrt(n), chỉ xét các số lẻ
// i*i <= n tương đương với i <= sqrt(n)
for (int i = 3; i * i <= n; i += 2) {
// Nếu n chia hết cho i, thì n không phải số nguyên tố
if (n % i == 0) {
return false;
}
}
// Nếu không tìm thấy ước nào trong khoảng này, n là số nguyên tố
return true;
}
int main() {
int num1 = 7;
int num2 = 10;
int num3 = 1;
int num4 = 97; // Một số nguyên tố lớn hơn
cout << num1 << " is prime? " << (isPrimeOptimized(num1) ? "Yes" : "No") << endl;
cout << num2 << " is prime? " << (isPrimeOptimized(num2) ? "Yes" : "No") << endl;
cout << num3 << " is prime? " << (isPrimeOptimized(num3) ? "Yes" : "No") << endl;
cout << num4 << " is prime? " << (isPrimeOptimized(num4) ? "Yes" : "No") << endl;
return 0;
}
Giải thích code:
#include <cmath>
: Thêm thư việncmath
để sử dụng hàmsqrt
(căn bậc hai).if (n <= 1) { return false; }
: Vẫn xử lý trường hợp <= 1.if (n == 2) { return true; }
: Xử lý trường hợp số 2 riêng. Số 2 là số nguyên tố chẵn duy nhất.if (n % 2 == 0) { return false; }
: Sau khi xử lý số 2, bất kỳ số chẵn nào lớn hơn 2 đều không phải nguyên tố (vì nó chia hết cho 2), nên ta loại bỏ ngay. Điều này giúp ta chỉ cần kiểm tra các ước lẻ sau này.for (int i = 3; i * i <= n; i += 2)
: Vòng lặp bắt đầu từ 3, chỉ tăngi
thêm 2 mỗi lần (i += 2
) để chỉ xét các số lẻ (3, 5, 7, 9...). Điều kiện dừng lài * i <= n
. Việc sử dụngi * i <= n
thay vìi <= sqrt(n)
thường được ưu tiên hơn trong lập trình nguyên vì nó tránh việc tính toán căn bậc hai với số thực, có thể gây sai số nhỏ hoặc chậm hơn trên một số kiến trúc máy tính.if (n % i == 0) { return false; }
: Kiểm tra chia hết cho các số lẻi
.return true;
: Nếu vòng lặp kết thúc,n
là nguyên tố.
Đánh giá: Phương pháp này hiệu quả hơn đáng kể. Thay vì duyệt n
lần, ta chỉ duyệt khoảng sqrt(n)
lần. Hơn nữa, việc loại bỏ các số chẵn (trừ 2) giúp giảm số lượng phép chia xuống gần một nửa. Độ phức tạp thời gian của giải thuật này là O(sqrt(n)). Với các số n
rất lớn (ví dụ, hàng tỷ), sqrt(n)
nhỏ hơn n
rất nhiều (sqrt(1 tỷ) khoảng 31622), giúp việc kiểm tra trở nên khả thi hơn nhiều.
Tuy nhiên, cả hai phương pháp trên đều chỉ dùng để kiểm tra một số duy nhất. Điều gì xảy ra nếu chúng ta cần tìm tất cả các số nguyên tố trong một phạm vi lớn, ví dụ từ 1 đến 1.000.000? Việc gọi isPrimeOptimized
cho từng số từ 1 đến 1.000.000 vẫn sẽ rất tốn kém. Lúc này, chúng ta cần một giải thuật tối ưu hơn cho bài toán tìm kiếm hàng loạt: Sàng Eratosthenes.
Phương Pháp 3: Sàng Eratosthenes
Sàng Eratosthenes (Sieve of Eratosthenes) là một giải thuật cổ xưa (khoảng thế kỷ thứ 3 TCN) dùng để tìm tất cả các số nguyên tố nhỏ hơn hoặc bằng một giới hạn N
cho trước. Ý tưởng chính rất đơn giản và thông minh: thay vì kiểm tra từng số xem nó có phải nguyên tố hay không, ta sẽ đánh dấu các số không phải nguyên tố (hợp số) một cách có hệ thống. Những số còn lại không bị đánh dấu chính là số nguyên tố.
Các bước thực hiện Sàng Eratosthenes để tìm số nguyên tố đến giới hạn N
:
- Tạo một danh sách (hoặc mảng) các số nguyên liên tiếp từ 0 đến
N
. Ban đầu, giả định tất cả các số (trừ 0 và 1) đều là số nguyên tố. Ta thường dùng một mảng booleanisPrime[0...N]
và khởi tạo tất cả các giá trị từisPrime[2]
trở đi làtrue
.isPrime[0]
vàisPrime[1]
được đánh dấufalse
. - Bắt đầu với số nguyên tố nhỏ nhất là
p = 2
. - Đánh dấu bội số: Duyệt qua tất cả các bội số của
p
lớn hơnp*p
và nhỏ hơn hoặc bằngN
, và đánh dấu chúng là hợp số (gánisPrime[bội_số] = false
). Tại sao lại bắt đầu từp*p
? Bởi vì các bội số nhỏ hơnp*p
(tức làp * k
vớik < p
) đã bị đánh dấu là hợp số bởi các số nguyên tố nhỏ hơnp
(ví dụ, 4 và 6 bị đánh dấu bởi 2; 9 bị đánh dấu bởi 3; 10 bị đánh dấu bởi 2...). - Tìm số tiếp theo lớn hơn
p
mà chưa bị đánh dấu là hợp số. Gán số đó chop
. - Nếu
p * p > N
, dừng giải thuật. Tất cả các số còn lại trong danh sách chưa bị đánh dấu là số nguyên tố. (Tại sao lại dừng ởp*p > N
? Giống như ý tưởng tối ưu ở phương pháp 2, nếu một sốm
nhỏ hơn hoặc bằngN
là hợp số, nó phải có một ước nguyên tốp
nhỏ hơn hoặc bằngsqrt(m)
. Nếum <= N
, thìsqrt(m) <= sqrt(N)
. Do đó, nếu ta đã xử lý tất cả các số nguyên tốp
sao chop <= sqrt(N)
(tức làp*p <= N
), thì tất cả các hợp số đếnN
đã bị đánh dấu.) - Nếu
p * p <= N
, quay lại bước 3.
Kết thúc giải thuật, những chỉ số i
mà isPrime[i]
vẫn là true
chính là các số nguyên tố nhỏ hơn hoặc bằng N
.
Hãy xem code C++ minh họa Sàng Eratosthenes:
#include <iostream>
#include <vector>
#include <cmath> // Cần cho sqrt, mặc dù ta dùng i*i <= n để tránh
#include <numeric> // Để sử dụng iota nếu muốn khởi tạo mảng số
// Hàm thực hiện Sàng Eratosthenes để tìm tất cả số nguyên tố đến N
vector<bool> sieveOfEratosthenes(int N) {
// 1. Tạo mảng boolean và khởi tạo
// Kích thước N+1 để bao gồm cả số N
vector<bool> isPrime(N + 1, true);
// 0 và 1 không phải số nguyên tố
isPrime[0] = isPrime[1] = false;
// 2. Bắt đầu sàng từ p = 2
// Chỉ cần duyệt đến sqrt(N)
// i*i <= N tương đương i <= sqrt(N)
for (int p = 2; p * p <= N; ++p) {
// Nếu isPrime[p] vẫn là true, tức là p là số nguyên tố
if (isPrime[p] == true) {
// 3. Đánh dấu tất cả các bội số của p là hợp số
// Bắt đầu từ p*p, vì các bội nhỏ hơn đã được xử lý bởi các số nguyên tố nhỏ hơn p
// Tăng lên p mỗi lần (p*p + p, p*p + 2p, ...)
for (int i = p * p; i <= N; i += p)
isPrime[i] = false;
}
}
// Mảng isPrime[0...N] bây giờ chứa kết quả:
// isPrime[i] == true nghĩa là i là số nguyên tố
return isPrime;
}
int main() {
int limit = 100; // Tìm tất cả số nguyên tố đến 100
vector<bool> primes = sieveOfEratosthenes(limit);
cout << "So nguyen to nho hon hoac bang " << limit << " la:" << endl;
for (int p = 2; p <= limit; ++p) {
if (primes[p]) {
cout << p << " ";
}
}
cout << endl;
// Ví dụ khác: tìm số nguyên tố đến 30
int limit2 = 30;
vector<bool> primes2 = sieveOfEratosthenes(limit2);
cout << "So nguyen to nho hon hoac bang " << limit2 << " la:" << endl;
for (int p = 2; p <= limit2; ++p) {
if (primes2[p]) {
cout << p << " ";
}
}
cout << endl;
return 0;
}
Giải thích code:
#include <vector>
: Thư viện cần thiết để sử dụngvector
.vector<bool> sieveOfEratosthenes(int N)
: Hàm nhận vào giới hạnN
và trả về mộtvector<bool>
có kích thướcN+1
. Phần tử tại chỉ sối
của vector này sẽ làtrue
nếui
là số nguyên tố, vàfalse
nếu ngược lại.vector<bool> isPrime(N + 1, true);
: Khởi tạo vectorisPrime
vớiN+1
phần tử, tất cả đều làtrue
ban đầu. Chỉ số của vector tương ứng với số nguyên mà ta đang xét.isPrime[0] = isPrime[1] = false;
: Đánh dấu 0 và 1 không phải là số nguyên tố.for (int p = 2; p * p <= N; ++p)
: Vòng lặp ngoài duyệt qua các sốp
bắt đầu từ 2. Ta chỉ cần duyệt đếnsqrt(N)
như đã giải thích.if (isPrime[p] == true)
: Kiểm tra xem sốp
hiện tại có còn được đánh dấu là nguyên tố không. Nếu có, nó là một số nguyên tố thực sự (chưa bị các số nhỏ hơn sàng loại).for (int i = p * p; i <= N; i += p)
: Nếup
là nguyên tố, vòng lặp trong sẽ duyệt qua tất cả các bội số củap
từp*p
trở đi, tăngp
mỗi lần (p*p
,p*p + p
,p*p + 2p
, ...).isPrime[i] = false;
: Đánh dấu các bội số này là hợp số.return isPrime;
: Sau khi vòng lặp ngoài kết thúc, vectorisPrime
chứa kết quả sàng.
Đánh giá: Sàng Eratosthenes cực kỳ hiệu quả khi cần tìm nhiều số nguyên tố trong một phạm vi.
- Độ phức tạp thời gian: O(N log log N). Nghe có vẻ phức tạp, nhưng trên thực tế,
log log N
tăng trưởng rất chậm. Đối với các giá trịN
thông thường trong lập trình thi đấu hoặc ứng dụng thực tế, độ phức tạp này gần như là tuyến tính vớiN
. Nó nhanh hơn rất nhiều so với việc gọiisPrimeOptimized
cho từng số từ 1 đếnN
(độ phức tạp sẽ là O(N * sqrt(N))). - Độ phức tạp không gian: O(N). Sàng Eratosthenes yêu cầu một mảng boolean có kích thước
N+1
để lưu trữ trạng thái của mỗi số. Điều này có nghĩa là bộ nhớ sử dụng tăng tuyến tính theoN
. Đối với các giới hạnN
rất lớn (ví dụ, hàng tỷ), bộ nhớ có thể trở thành vấn đề. Tuy nhiên, với các giới hạnN
phổ biến (ví dụ, đến 10^6 hoặc 10^7), O(N) bộ nhớ là hoàn toàn chấp nhận được.
Sàng Eratosthenes là giải thuật tiêu chuẩn khi bạn cần một danh sách các số nguyên tố lên đến một giới hạn nhất định.
Khi Nào Sử Dụng Phương Pháp Nào?
- Kiểm tra MỘT số duy nhất: Sử dụng phương pháp cải tiến O(sqrt(n)) (
isPrimeOptimized
). Nó nhanh và không tốn nhiều bộ nhớ. - Tìm TẤT CẢ số nguyên tố trong một phạm vi đến N: Sử dụng Sàng Eratosthenes O(N log log N). Mặc dù tốn O(N) bộ nhớ, nhưng thời gian tính toán để tìm tất cả các số nguyên tố trong phạm vi là vượt trội so với việc kiểm tra từng số riêng lẻ.
Phương pháp ngây ngô O(n) hiếm khi được sử dụng trong thực tế khi n có thể lớn, nó chủ yếu mang tính giới thiệu về ý tưởng cơ bản.
Bài tập ví dụ:
Sức khỏe của mọi người
Trong một dự án về dinh dưỡng cộng đồng, FullHouse Dev được giao nhiệm vụ theo dõi sức khỏe của người dân trong thành phố thông qua việc đi bộ. Với kinh nghiệm trong việc phân tích dữ liệu, nhóm bắt đầu nghiên cứu mô hình để tối ưu hóa việc theo dõi sức khỏe của từng người dân.
Bài toán
Có \(N\) người trong thành phố. Người thứ \(i\) có \(H_i\) điểm sức khỏe. FullHouse Dev sẽ tổ chức đi bộ trong \(D\) ngày. Vào ngày thứ \(j\), mỗi lần đi bộ sẽ làm giảm \(L_j\) điểm sức khỏe của người tham gia. Trong ngày thứ \(j\), một người chỉ có thể tham gia đi bộ nếu họ vẫn còn khỏe mạnh và \(j\) là bội số của \(i\). Mỗi người chỉ có thể đi bộ một lần mỗi ngày.
Một người được coi là không khỏe nếu điểm sức khỏe của họ nhỏ hơn hoặc bằng 0. Cuối mỗi ngày, điểm sức khỏe sẽ được phục hồi về mức ban đầu nếu họ chưa rơi vào trạng thái không khỏe.
INPUT FORMAT:
- Dòng đầu tiên: Số nguyên \(T\) là số lượng test case
- Với mỗi test case:
- Dòng đầu: Hai số nguyên \(N\) và \(D\) lần lượt là số người và số ngày đi bộ
- Dòng tiếp theo: \(N\) số nguyên, số thứ \(i\) biểu thị điểm sức khỏe của người thứ \(i\)
- Dòng tiếp theo: \(D\) số nguyên, số thứ \(j\) biểu thị mức giảm sức khỏe trong ngày thứ \(j\)
OUTPUT FORMAT:
- In ra \(N\) dòng, dòng thứ \(i\) chứa một số nguyên biểu thị ngày mà người thứ \(i\) trở nên không khỏe. Nếu người đó luôn khỏe mạnh, in ra -1.
Ràng buộc:
- \(1 \leq T \leq 100\)
- \(1 \leq N, D \leq 100\)
- \(1 \leq H_i \leq 100\)
- \(1 \leq L_j \leq 100\)
Ví dụ
INPUT
2
6 5
2 1 6 4 3 7
2 3 7 3 4
3 3
2 1 3
2 2 2
OUTPUT
1
1
3
-1
5
3
1
1
-1
Giải thích
Trong test case đầu tiên:
- Người thứ 1 và 2 trở nên không khỏe vào ngày 1
- Người thứ 3 trở nên không khỏe vào ngày 3
- Người thứ 4 luôn khỏe mạnh
- Người thứ 5 trở nên không khỏe vào ngày 5
- Người thứ 6 trở nên không khỏe vào ngày 3 Tuyệt vời! Dưới đây là hướng giải ngắn gọn cho bài toán "Sức khỏe của mọi người" bằng C++:
Phân tích bài toán và quy tắc:
- Có
N
người, ngườii
có sức khỏe ban đầuH_i
. - Có
D
ngày, ngàyj
có mức giảm sức khỏeL_j
. - Quy tắc mấu chốt: Người
i
(với chỉ số/số thứ tựi
từ 1 đến N) chỉ có thể tham gia đi bộ vào ngàyj
(với chỉ số ngàyj
từ 1 đến D) nếui
là bội số củaj
(nghĩa lài % j == 0
) VÀ người đó chưa không khỏe. - Nếu tham gia, sức khỏe giảm đi
L_j
. - Người đó trở nên không khỏe vĩnh viễn nếu sức khỏe <= 0 sau khi đi bộ.
- Cuối mỗi ngày, sức khỏe phục hồi về mức ban đầu (
H_i
) nếu người đó chưa rơi vào trạng thái không khỏe. Điều này ngụ ý rằng mỗi lần một người tham gia đi bộ vào ngàyj
(khii % j == 0
), ta kiểm tra xemH_i - L_j <= 0
hay không. Nếu có, ngàyj
là ngày đầu tiên họ trở nên không khỏe.
- Có
Cách tiếp cận:
- Với ràng buộc
N, D <= 100
, chúng ta có thể sử dụng phương pháp mô phỏng (simulation) trực tiếp từng ngày một. - Duyệt qua từng ngày từ ngày 1 đến ngày D.
- Trong mỗi ngày, duyệt qua từng người từ người 1 đến người N.
- Kiểm tra xem người
i
có đủ điều kiện để đi bộ vào ngàyj
hay không dựa trên quy tắc (i % j == 0
) và trạng thái sức khỏe hiện tại của họ (chưa không khỏe). - Nếu đủ điều kiện, tính toán xem việc đi bộ trong ngày
j
có làm cho sức khỏe của họ (H_i
) giảm xuống <= 0 hay không (tức làH_i - L_j <= 0
). - Nếu có, ghi nhận ngày
j
là ngày mà ngườii
trở nên không khỏe và đánh dấu người này là đã không khỏe để không xét họ trong các ngày tiếp theo.
- Với ràng buộc
Lưu trữ trạng thái:
- Cần lưu trữ điểm sức khỏe ban đầu của N người (ví dụ: trong một mảng hoặc vector
H
). - Cần lưu trữ mức giảm sức khỏe của D ngày (ví dụ: trong một mảng hoặc vector
L
). - Cần lưu trữ kết quả: ngày mà mỗi người trở nên không khỏe. Khởi tạo mảng/vector kết quả với giá trị -1 cho tất cả N người, biểu thị họ vẫn khỏe mạnh. Khi một người không khỏe vào ngày
j
, cập nhật giá trị tại vị trí tương ứng trong mảng kết quả thànhj
. - Cần một cách để đánh dấu những người đã không khỏe để không xử lý họ nữa (ví dụ: một mảng boolean
is_unhealthy
).
- Cần lưu trữ điểm sức khỏe ban đầu của N người (ví dụ: trong một mảng hoặc vector
Thuật toán chi tiết:
- Đọc số lượng test case
T
. - Lặp
T
lần:- Đọc
N
vàD
. - Đọc
N
giá trị sức khỏe ban đầu vào vectorH
(kích thướcN
). - Đọc
D
giá trị mức giảm sức khỏe vào vectorL
(kích thướcD
). - Khởi tạo vector
unhealthy_day
kích thướcN
với tất cả giá trị là -1. - Khởi tạo vector boolean
is_unhealthy
kích thướcN
với tất cả giá trị làfalse
. - Lặp qua các ngày
j
từ 1 đếnD
:- Lặp qua các người
i
từ 1 đếnN
:- Sử dụng chỉ số 0-based cho các vector (
H
,L
,unhealthy_day
,is_unhealthy
). Ngườii
tương ứng với chỉ sối-1
, ngàyj
tương ứng với chỉ sốj-1
. - Nếu người
i-1
chưa không khỏe (is_unhealthy[i-1]
làfalse
):- Kiểm tra điều kiện đi bộ:
i % j == 0
. - Nếu điều kiện đúng:
- Kiểm tra xem việc đi bộ ngày hôm nay có làm họ không khỏe không:
H[i-1] - L[j-1] <= 0
. Nếu đúng: Ghi nhận ngày không khỏe:unhealthy_day[i-1] = j;
Đánh dấu là không khỏe:is_unhealthy[i-1] = true;
- Kiểm tra xem việc đi bộ ngày hôm nay có làm họ không khỏe không:
- Kiểm tra điều kiện đi bộ:
- Sử dụng chỉ số 0-based cho các vector (
- Lặp qua các người
- In ra các giá trị trong vector
unhealthy_day
, mỗi giá trị trên một dòng.
- Đọc
- Đọc số lượng test case
Lưu ý cài đặt:
- Sử dụng
vector
để lưu trữ dữ liệu. - Cẩn thận với chỉ số mảng/vector (0-based vs. 1-based). Người
i
(1-based) tương ứng với chỉ sối-1
(0-based). Ngàyj
(1-based) tương ứng với chỉ sốj-1
(0-based). - Để tăng tốc độ nhập/xuất trong C++, có thể thêm
ios_base::sync_with_stdio(false); cin.tie(NULL);
ở đầu hàmmain
.
- Sử dụng
Comments