Hướng dẫn giải của Số đặc biệt
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.
Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.
Bạn phải đăng nhập để mở lời giải này.
Đăng nhậpTác giả: , , ,
Hiểu bài toán
Xác định số lượng số đặc biệt trong khoảng [A, B]. Một số được gọi là số đặc biệt nếu nó có dạng:
- 3^8 = 6561.
- 9 * p^2, trong đó p là số nguyên tố khác 3.
Bài toán yêu cầu xử lý nhiều truy vấn (T test cases). Với mỗi truy vấn, nhập A và B, xuất ra số lượng số đặc biệt trong đoạn [A, B].
Các cách tiếp cận
Cách Sàng nguyên tố và kiểm tra trực tiếp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000000;
vector<int> special;
void init() {
// Thêm số 3^8
special.push_back(6561);
// Sàng nguyên tố
const int LIM = 333; // 9 * 333^2 > 10^6
vector<bool> isprime(LIM + 1, true);
isprime[0] = isprime[1] = false;
for (int i = 2; i * i <= LIM; i++) {
if (isprime[i]) {
for (int j = i * i; j <= LIM; j += i)
isprime[j] = false;
}
}
// Tạo các số 9 * p^2
for (int p = 2; p <= LIM; p++) {
if (p != 3 && isprime[p]) {
long long n = 9LL * p * p;
if (n <= MAXN) special.push_back(n);
}
}
sort(special.begin(), special.end());
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// Nếu cần đọc file
// freopen("SDB.INP", "r", stdin);
// freopen("SDB.OUT", "w", stdout);
init();
int T;
cin >> T;
while (T--) {
long long a, b;
cin >> a >> b;
auto L = lower_bound(special.begin(), special.end(), a);
auto R = upper_bound(special.begin(), special.end(), b);
cout << (R - L) << '\n';
}
return 0;
}
- Time Complexity: O(K log K + T log K) (K là số lượng số đặc biệt, rất nhỏ ~15)
- Space Complexity: O(K)
Phương pháp này dựa trên việc nhận thấy số lượng số đặc biệt rất ít. Ta có thể liệt kê tất cả các số đặc biệt nhỏ hơn giới hạn (ví dụ 10^6) bằng cách:
- Thêm 6561 vào danh sách.
- Sàng nguyên tố để tìm các số p (p != 3).
- Tính 9 * p^2 và thêm vào danh sách.
- Sau khi có danh sách đã sắp xếp, với mỗi truy vấn [A, B], dùng binary search (lowerbound và upperbound) để đếm số lượng phần tử trong đoạn.
Cách Công thức toán học kết hợp sàng
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define pb push_back
#define el '\n'
#define all(x) x.begin(), x.end()
typedef long long ll;
const int MOD = 1e9 + 7;
const int maxN = (int) 1e6 + 7;
bool ok[maxN];
int pre[maxN];
ll spe = 6561; // 3^8
// Sàng nguyên tố
void snt(){
ok[0] = ok[1] = 1;
for(int i = 2; i*i < maxN; i++){
if(ok[i] == 0){
for(int j = i*i; j < maxN; j += i) ok[j] = 1;
}
}
// pre[i] lưu số lượng số nguyên tố <= i
for(int i = 1; i < maxN; i++) pre[i] = pre[i - 1] + !ok[i];
}
ll get(ll N){
if(N < 0) return 0;
ll ans = 0;
// Kiểm tra 3^8
if(spe <= N) ans++;
// Với 9*p^2 <= N => p^2 <= N/9 => p <= sqrt(N/9)
// Công thức đúng là p <= sqrt(N/9)
// Tuy nhiên code dùng sqrt(N)/3 là sai lệch nhỏ nhưng về bản chất p_max = floor(sqrt(N/9))
// Code gốc dùng M = sqrt(N)/3, ta cần điều chỉnh lại cho chính xác
// p_max = floor(sqrt(N/9))
ll M = sqrt(N / 9.0);
// Chỉ tính các p >= 2 và p != 3
// pre[M] là số lượng số nguyên tố <= M
// Nếu M >= 3, ta trừ đi 1 để loại bỏ p = 3
ll count_p = pre[M];
if (M >= 3) count_p--;
ans += count_p;
return ans;
}
void solve(){
ll a, b;
cin >> a >> b;
cout << get(b) - get(a - 1) << '\n';
}
int main(){
ios_base::sync_with_stdio(0);
cin.tie(0);
snt();
int T;
if (cin >> T) {
while(T--) solve();
}
return 0;
}
- Time Complexity: O(MAXN log log MAXN + T)
- Space Complexity: O(MAXN)
Phương pháp này tối ưu bằng cách dùng công thức. Số lượng số đặc biệt dạng 9*p^2 <= N bằng số lượng số nguyên tố p sao cho p^2 <= N/9 (tức p <= sqrt(N/9)), loại trừ p=3.
Ta chỉ cần sàng nguyên tố một lần để có mảng pre đếm số lượng số nguyên tố đến giới hạn nào đó. Với mỗi truy vấn, ta tính số lượng số nguyên tố cần thiết trong thời gian O(1) sau khi tính toán căn bậc hai.
Lưu ý: Cài đặt của Solution 1 có vẻ hơi sai lệch trong cách tính M, công thức chuẩn là sqrt(N/9), nhưng logic chung là dùng số lượng số nguyên tố để đếm.
Cách Tối ưu hóa sàng theo truy vấn (Offline)
// Nguon: blablablabkabcu
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll isqrt_floor(ll x){
if (x < 0) return -1;
ll l = 0, r = 2000000000LL;
while (l < r){
ll m = (l + r + 1) >> 1;
if (m*m <= x) l = m;
else r = m - 1;
}
return l;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
// freopen("SDB.inp","r",stdin);
// freopen("SDB.OUT","w",stdout);
int T;
if (!(cin >> T)) return 0;
vector<pair<long long,long long>> qs(T);
ll maxB = 0;
for (int i = 0; i < T; ++i){
ll a,b; cin >> a >> b;
qs[i] = {a,b};
if (b > maxB) maxB = b;
}
// Chỉ sàng đến giới hạn cần thiết
ll maxDiv9 = maxB / 9;
ll limit = isqrt_floor(maxDiv9);
if (limit < 2) limit = 2;
// Sieve prime
vector<bool> is_prime(limit + 1, true);
is_prime[0] = is_prime[1] = false;
for (ll i = 2; i * i <= limit; ++i) {
if (is_prime[i]) {
for (ll j = i * i; j <= limit; j += i)
is_prime[j] = false;
}
}
// Đếm số lượng số đặc biệt <= X
// Có thể dùng prefix sum hoặc binary search
vector<ll> special;
if (6561 <= maxB) special.push_back(6561);
for (ll p = 2; p <= limit; ++p) {
if (p != 3 && is_prime[p]) {
special.push_back(9 * p * p);
}
}
sort(special.begin(), special.end());
for (auto [a, b] : qs) {
auto it1 = lower_bound(special.begin(), special.end(), a);
auto it2 = upper_bound(special.begin(), special.end(), b);
cout << (it2 - it1) << '\n';
}
return 0;
}
- Time Complexity: O(Sqrt(MaxB) log log MaxB + T log K)
- Space Complexity: O(Sqrt(MaxB))
Đây là biến thể của Approach 2 và 1. Thay vì sàng với giới hạn cố định lớn (ví dụ 10^6), ta chỉ sàng đến giới hạn cần thiết dựa trên giá trị lớn nhất của B trong các truy vấn. Điều này giúp tiết kiệm bộ nhớ và thời gian nếu MaxB nhỏ. Tuy nhiên, trong bài này MaxB có thể lên tới 10^12 (theo đề bài gốc), nhưng sqrt(10^12/9) vẫn khá nhỏ (~333,333), nên sàng ở mức này rất nhanh.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(K log K + T log K) (K là số lượng số đặc biệt, rất nhỏ ~15) | O(K) | Sàng nguyên tố và kiểm tra trực tiếp |
| 2 | O(MAXN log log MAXN + T) | O(MAXN) | Công thức toán học kết hợp sàng |
| 3 | O(Sqrt(MaxB) log log MaxB + T log K) | O(Sqrt(MaxB)) | Tối ưu hóa sàng theo truy vấn (Offline) |
Bài học kinh nghiệm
- Số lượng số đặc biệt rất ít (khoảng 15 số nếu giới hạn là 10^6, hoặc ~30k số nếu giới hạn lớn hơn), nên có thể liệt kê hoặc dùng công thức đếm.
- Dạng 9*p^2 cho thấy p chỉ cần nhỏ hơn sqrt(N/9), nên ta có thể dùng số lượng số nguyên tố để đếm thay vì lặp từng số.
- Binary search là công cụ hiệu quả để xử lý truy vấn trên danh sách đã sắp xếp.
Lỗi thường gặp
- Lỗi trong công thức tính căn bậc hai:
sqrt(N)/3khácsqrt(N/9)(ví dụ N=100, cái đầu là 3.33, cái sau là 3.33, nhưng với số nguyên thì có thể chênh lệch). Nên dùngsqrt(N/9.0)hoặcsqrt(N)/3một cách chính xác. - Quên loại trừ p=3 trong công thức (phải dùng p là số nguyên tố và p != 3).
- Lỗi tràn số nguyên khi tính
p*pnếu p lớn (nên dùnglong long).
Bình luận