Flowers (ICTU 56)
Đề bài
Flowers
Điểm số: 1. Giới hạn thời gian: 1s. Giới hạn bộ nhớ: 512 MB. Người đăng: ICTU.
Đề bài: Có lẽ sau một thời gian rất dài chỉ ngồi coding, An cảm thấy thực sự căng thẳng và mệt mỏi. Vì vậy hôm nay An quyết định chọn ra một hoạt động mới giúp An cảm thấy thư giãn hơn. An ngồi đắn đo suy nghĩ. Các môn thể thao thì thực sự rất vui, nhưng mọi người cũng phải ở nhà hết rồi! Nếu chuyển qua chơi game, vậy cũng không khác nào ngồi coding với máy tính cả.
An quyết định chia sẻ những đắn đo này với Nam, và Nam gợi ý rằng: "Hay là làm vườn? Làm vườn tốt cho sức khỏe. Làm vườn ở ngoài trời, làn da tiếp xúc với ánh nắng có thể giúp có thể tăng cường Vitamin D. Làm vườn cũng giúp giảm huyết áp nè. Ngoài ra đây cũng là hoạt động nhẹ nhàng, giúp giảm căng thẳng rất hiệu quả!"
An thấy ý tưởng của Nam quả là tuyệt vời, vì vậy An đã quyết định tạo ra một vườn hoa của riêng mình. Vườn hoa mà An định trồng có dạng một hình chữ nhật có kích thước $n \times m$. Hình chữ nhật được chia thành các hàng và cột, với các hàng được đánh số từ 1 đến $n$ từ trên xuống dưới và các cột được đánh số từ 1 đến $m$ từ trái qua phải. Ô ở hàng thứ $r$ và cột thứ $c$ được thể hiện bởi cặp số $(r, c)$.
Là người yêu thích toán học, nên An muốn khu vườn của mình cũng có tính chất đặc biệt. An chỉ trồng hoa vào các ô $(r, c)$ nếu như $r + c$ là số nguyên tố.
Cho kích thước khu vườn của An, hãy giúp An tìm xem An cần phải trồng bao nhiêu bông hoa trong khu vườn nhé!
Đầu vào: Gồm một dòng duy nhất chứa hai số nguyên $n$ và $m$ $(1 \le n, m \le 10^7)$ là kích thước khu vườn.
Đầu ra: In ra một số nguyên duy nhất là số bông hoa mà An cần trồng.
5 51110 726Lờ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 😇.
Giải thích ví dụ cho bạn nào chưa hiểu đề: hãy nhìn vào hình minh hoạ ví dụ 2, ma trận có 10 dòng, 7 cột, chỉ số dòng và cột tính từ 1. Ở dòng đầu tiên ($r = 1$), các giá trị $r + c$ sẽ là từ $2$ đến $8$; trong đó $2, 3, 5, 7$ là số nguyên tố nên các cột $1, 2, 4, 6$ được trồng hoa. Dòng thứ 2 có $r + c$ từ $3$ đến $9$, dòng tiếp theo từ $4$ đến $10$... Đếm hết cả 10 dòng được 26 bông hoa.
Với ví dụ 1 ($5 \times 5$), số bông hoa theo từng dòng lần lượt là $3, 3, 2, 2, 1$ ⇒ tổng 11.
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 OJLộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Làm theo đề: duyệt từng ô – O(n · m · √(n + m))
Cách tự nhiên nhất: duyệt mọi ô $(r, c)$, kiểm tra $r + c$ có phải số nguyên tố không bằng cách thử chia cho các số từ $2$ đến $\sqrt{r + c}$, nếu đúng thì đếm thêm 1 bông hoa.
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(long long x){
if(x < 2){
return false;
}
for(long long i = 2; i * i <= x; ++i){
if(x % i == 0){
return false;
}
}
return true;
}
int main(){
long long n, m, cnt = 0;
cin >> n >> m;
for(long long r = 1; r <= n; ++r){
for(long long c = 1; c <= m; ++c){
if(isPrime(r + c)){
cnt++;
}
}
}
cout << cnt;
return 0;
}
def isPrime(x):
if x < 2:
return False
i = 2
while i * i <= x:
if x % i == 0:
return False
i += 1
return True
n, m = map(int, input().split())
cnt = 0
for r in range(1, n + 1):
for c in range(1, m + 1):
if isPrime(r + c):
cnt += 1
print(cnt)
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(1)$.
- Thời gian: có $n \cdot m$ ô, mỗi ô kiểm tra nguyên tố mất tới $O(\sqrt{n + m})$ ⇒ $O(n \cdot m \cdot \sqrt{n + m})$.
Cách 2Sàng Eratosthenes + cửa sổ trượt – O(X log log X)
Bước 1: cần biết số nào là số nguyên tố
Hãy nhìn vào giới hạn của bài: $n, m$ tối đa có thể là $10^7$. Như thế $r + c$ tối đa có thể lên đến $2 \cdot 10^7$, còn tối thiểu thì tất nhiên là $2$ (vì $1 + 1 = 2$). Vậy ta có nhu cầu xét các số nguyên tố trong đoạn từ $2$ đến $n + m$, lớn nhất là khi $n = m = 10^7$, tức là xét từ $2$ đến 20 triệu.
Nếu dùng thuật toán kiểm tra từng số có phải số nguyên tố không thì độ phức tạp là $O(\sqrt{X})$ với $X$ là số cần kiểm tra. Giá trị lớn nhất $X = 2 \cdot 10^7$ thì $\sqrt{X} = \sqrt{20} \cdot \sqrt{10^6} \approx 4.47 \cdot 10^3$. Mà phải làm việc đó cho cả $2 \cdot 10^7$ số, tổng cộng $2 \cdot 10^7 \cdot 4.47 \cdot 10^3 \approx 9 \cdot 10^{10}$, coi như $10^{11}$ luôn rồi. Vượt xa giới hạn khoảng $10^8$ phép tính cho 1 giây ⇒ chắc chắn TLE.
Vậy ta phải dùng cách kiểm tra số nguyên tố tốt hơn cho toàn bộ các số từ $2$ đến $2 \cdot 10^7$, đó chính là sàng nguyên tố Eratosthenes. Các bạn có thể đọc lại bài viết về sàng trên VNOI Wiki: Lý thuyết số – phần 2.
isPrime[x] cho biết ngay $x$ có nguyên tố không trong $O(1)$.Độ phức tạp của sàng là $O(X \log \log X)$. Với $X = 2 \cdot 10^7$ thì $\log_2 X \approx 24$, log thêm lần nữa chỉ còn cỡ $5$. Tóm lại khoảng $2 \cdot 10^7 \cdot 5 = 10^8$, vừa vặn giới hạn 1 giây.
Bước 2: đếm theo từng dòng bằng cửa sổ trượt
Có bảng sàng rồi nhưng nếu vẫn duyệt từng ô thì vẫn là $n \cdot m$ ô, vẫn TLE. Hãy nhìn lại các dòng cần kiểm tra:
- Dòng đầu tiên cần kiểm tra các giá trị từ $2$ đến $m + 1$.
- Dòng tiếp theo cần kiểm tra các giá trị từ $3$ đến $m + 2$.
- Dòng tiếp theo cần kiểm tra các giá trị từ $4$ đến $m + 3$.
- …
- Dòng cuối cùng cần kiểm tra các giá trị từ $n + 1$ đến $n + m$.
Ta chỉ cần đếm xem có bao nhiêu số nguyên tố trong đoạn $[2, m + 1]$ của dòng đầu tiên. Rồi đoạn $[3, m + 2]$ của dòng 2 tính ra chỉ là: loại bỏ 1 giá trị đầu của dòng trước (loại số $2$) và nhận thêm 1 giá trị mới phía sau (số $m + 2$). Đoạn $[4, m + 3]$ cũng vậy: loại số $3$, nhận thêm số $m + 3$. Giống hệt một cái cửa sổ độ dài $m$ trượt dần sang phải trên trục số.
sum1 là số bông hoa trên dòng hiện tại. Chuyển từ dòng $i - 1$ sang dòng $i$ (đoạn $[i + 1, i + m]$): nếu $i$ nguyên tố thì sum1--; nếu $m + i$ nguyên tố thì sum1++. Cộng sum1 của mọi dòng vào sum2 là đáp án.Như thế chỉ phải duyệt hết dòng đầu tiên mất $O(m)$, sau đó lặp $n - 1$ lần còn lại, mỗi lần chỉ kiểm tra 2 giá trị đầu cuối bằng cách tra bảng sàng $O(1)$ ⇒ phần này chỉ $O(n + m)$.
Minh hoạ cửa sổ trượt
Em nhập $n$, $m$ rồi bấm Tiếp. Mỗi ô của vườn ghi giá trị $r + c$, ô xanh là số nguyên tố (có hoa). Bên dưới là trục số với cửa sổ (viền xanh dương) của dòng đang xét: ô đỏ gạch ngang là giá trị vừa bị loại, ô viền cam là giá trị vừa được nhận thêm.
Source code để các bạn tham khảo nếu làm theo hướng này. Code này mình code vội đúng theo tư tưởng đã trình bày ở trên (đã nộp thành công trên web), mình chỉ căn lại thụt lề cho dễ đọc:
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
long long n, m;
cin >> n >> m;
vector<int> isPrime(n + m + 1);
long long sz = n + m;
for(int i = 0; i <= sz; ++i){
isPrime[i] = true;
}
isPrime[0] = false;
isPrime[1] = false;
long long sum1 = 0;
for(int i = 2; i * i <= sz; ++i){
if(isPrime[i] == true){
for(int j = i * i; j <= sz; j += i){
isPrime[j] = false;
}
}
}
for(int i = 2; i <= m + 1; ++i){
if(isPrime[i]){
sum1++;
}
}
long long sum2 = sum1;
for(int i = 2; i <= n; ++i){
if(isPrime[i]){
sum1--;
}
if(isPrime[m + i]){
sum1++;
}
sum2 += sum1;
}
cout << sum2;
return 0;
}
n, m = map(int, input().split())
sz = n + m
# Sàng Eratosthenes trên đoạn 0..n+m
isPrime = bytearray([1]) * (sz + 1)
isPrime[0] = 0
isPrime[1] = 0
i = 2
while i * i <= sz:
if isPrime[i]:
# gạch bỏ i*i, i*i + i, ... (gán cả đoạn một lần cho nhanh)
isPrime[i * i::i] = bytes(len(range(i * i, sz + 1, i)))
i += 1
# Dòng 1: đếm số nguyên tố trong đoạn [2, m + 1]
sum1 = sum(isPrime[2:m + 2])
sum2 = sum1
# Dòng i: bỏ giá trị i (đầu dòng trước), thêm giá trị m + i
for i in range(2, n + 1):
sum1 += isPrime[m + i] - isPrime[i]
sum2 += sum1
print(sum2)
Một vài ghi chú về code:
vector<int> isPrime(n + m + 1)có tối đa $2 \cdot 10^7 + 1$ phần tửint, tốn khoảng 80 MB, vẫn nằm thoải mái trong giới hạn 512 MB của đề. Nếu đề chặt bộ nhớ hơn thì đổi sangvector<bool>hoặcvector<char>.- Vòng sàng dùng
int ivới điều kiệni * i <= sz: $i$ chỉ lên tới khoảng $4473$ nêni * ikhông trànint;jbắt đầu từi * itối đa $2 \cdot 10^7$, cũng an toàn. - Đáp án có thể lên tới cỡ $n \cdot m = 10^{14}$ (thực tế với $n = m = 10^7$ là $6\,254\,632\,112\,109$), nên
sum2bắt buộc làlong long. - Bản Python dùng
bytearrayvà gán cả đoạnisPrime[i * i::i] = ...để sàng nhanh, thay cho vòng lặp từng phần tử.
Đánh giá độ phức tạp của cách làm này (đặt $X = n + m$):
- Không gian: mảng sàng $X + 1$ phần tử ⇒ $O(X)$.
- Thời gian: sàng mất $O(X \log \log X)$, đếm dòng đầu $O(m)$, trượt cửa sổ $O(n)$. Lấy cái lớn nhất ⇒ $O(X \log \log X)$, dễ dàng AC trong 1 giây.
- 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