có bao nhiêu ước? Thử chia cho mọi số tới là phép chia, quá lâu. Nhưng ước đi theo cặp: là ước thì cũng là ước, và một trong hai không vượt . Chỉ cần thử tới căn là đủ, nhanh hơn một triệu lần.
Số học trong thi đấu gồm vài công cụ như thế, mỗi cái chỉ vài dòng code: thuật toán Euclid, thử chia tới căn, sàng Eratosthenes, lũy thừa nhanh, nghịch đảo theo modulo. Chỗ sai thì luôn quen thuộc: phần dư âm, tràn số, sót thừa số nguyên tố cuối. Bài này đi qua từng công cụ cùng lý do nó đúng; các bài 47 (Euclid mở rộng), 54 (định lý thặng dư Trung Hoa) và 70 (hàm Mobius) xây tiếp trên nền này.
Phần dư và chia hết
Với số nguyên và có đúng một cặp thỏa và . Số là phần dư, viết ; khi ta nói chia hết cho , viết . Ba phép , , đi qua phần dư được:
và tương tự cho phép trừ, phép nhân, vì mọi phần chứa biến mất khi lấy phần dư. Nhờ vậy ta lấy dư sau từng phép tính thay vì dồn tới cuối. Phép chia thì không đi qua được; mục nghịch đảo bên dưới xử lý riêng.
Chỗ vấp đầu tiên nằm ở C++: % cắt thương về phía 0 nên phần dư mang dấu của số bị chia, -7 % 5 bằng -2, trong khi phần dư toán học là . Muốn phần dư luôn trong thì viết (a % m + m) % m; phép trừ hai phần dư viết (x - y + m) % m. Chỗ vấp thứ hai là tràn số: tích hai phần dư lên tới với , vừa long long nhưng chỉ khi thừa số đã là long long. Với vượt khoảng , nhân trung gian bằng __int128.
Một chi tiết nhỏ mà đáng: khai báo modulo là const. Biết trước số chia, trình biên dịch thay phép chia chậm bằng phép nhân và dịch bit:
Chỉ thêm chữ const, vòng lặp nhanh gấp hơn 3 lần.
Hỏi nhanh
Trong C++, với và thì a % m bằng . Biểu thức nào cho phần dư theo định nghĩa toán học, nằm trong , đúng với mọi số nguyên ?
a % m nằm trong , cộng được số trong , lấy dư thêm một lần thì chắc chắn vào . a % m + m cho ở , ngoài khoảng. (a + m) % m chỉ sửa được khi . abs(a) % m cho phần dư của chứ không phải của .
Ví dụBước Tới Bội Số
Đề. bộ , . Mỗi thao tác tăng thêm 1; ít nhất bao nhiêu thao tác để chia hết cho ?
Lời giải. Gọi . Bội kế tiếp của không nhỏ hơn là nếu , và nếu . Chỗ dễ sai là in cả khi , cho thay vì .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll t, a, b;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> t;
while (t--) {
cin >> a >> b;
ll r = a % b;
cout << (r == 0 ? 0 : b - r) << '\n';
}
}
4
8 4
8 5
1 9
999999999 1000000000
0
2
8
1
ƯCLN và BCNN
Thuật toán Euclid tính ƯCLN, ký hiệu , dựa vào một nhận xét: chia hết cả lẫn khi và chỉ khi chia hết cả lẫn , vì và . Hai cặp có cùng tập ước chung, nên
Với : , , , nên . Hình học của phép tính là lát hình chữ nhật bằng hình vuông lớn nhất có thể, cạnh hình vuông cuối cùng là ƯCLN.
Số bước ít vì phần dư giảm rất nhanh: với ta có (nếu thì phần dư nhỏ hơn , còn nếu thì phần dư là ). Cứ hai bước số lớn giảm ít nhất một nửa, nên có bước, khoảng 85 phép chia với giá trị tới .
ll gcd(ll a, ll b) {
return b == 0 ? a : gcd(b, a % b);
}
Bội chung nhỏ nhất, ký hiệu , đi theo ƯCLN: khi phân tích ra nguyên tố, lấy số mũ nhỏ hơn còn lấy số mũ lớn hơn, nên và . Chia trước nhân sau là cố ý: mọi số trung gian không vượt kết quả. C++17 có sẵn __gcd và gcd. ƯCLN cả dãy thì gộp dần từ ; hai số nguyên dương liên tiếp luôn nguyên tố cùng nhau vì .
Thừa số và ước
Mọi số viết được duy nhất thành với nguyên tố đôi một khác nhau. Mọi câu hỏi về ước, về chia hết, về chính phương đều đọc được từ biểu diễn này.
Kiểm tra nguyên tố. Hợp số với thì , nên chỉ cần thử trong khi . Viết d * d <= n với long long, đừng qua số thực sqrt. Với là lần chia.
Phân tích thừa số. Chạy đúng vòng lặp ấy, mỗi khi chia hết số còn lại thì chia sạch và ghi số mũ. Mọi chia được đều nguyên tố, vì thừa số nguyên tố nhỏ hơn của nó đã bị chia sạch từ trước. Vòng lặp dừng khi vượt số còn lại; lúc đó nếu số còn lại lớn hơn 1 thì nó là một nguyên tố với số mũ 1. Thừa số cuối này hay bị quên. Ví dụ : chia hết 2 ba lần, 3 hai lần, còn , tới thì , thêm .
for (ll d = 2; d * d <= n; d++) {
if (n % d) continue;
ll e = 0;
while (n % d == 0) n /= d, e++;
p[++k] = d, ex[k] = e;
}
if (n > 1) p[++k] = n, ex[k] = 1;
Đếm ước. Mỗi ước chọn độc lập số mũ từ tới cho từng nguyên tố, nên số ước của , ký hiệu , bằng ; có ước. Số ước nhỏ hơn rất nhiều: tối đa 1344 khi , 6720 khi . Liệt kê ước: các ước đi theo cặp với một ước không vượt ; duyệt tới căn, lấy cả hai (một lần nếu bằng nhau), rồi sắp xếp. Số ước lẻ chỉ khi là số chính phương.
Hỏi nhanh
Chạy đoạn phân tích thừa số trên với : vòng for chia hết rồi dừng, phần còn lại là . Với bất kỳ, nếu vòng lặp dừng mà phần còn lại lớn hơn 1 thì ta kết luận được gì?
Vòng lặp dừng khi vượt phần còn lại, lúc đó mọi số nhỏ hơn đã được chia sạch. Nếu phần còn lại là hợp số thì nó có ước nguyên tố không quá căn của nó, tức nhỏ hơn , mâu thuẫn. Với thì đúng là thừa số cuối; quên thêm nó là mất thừa số .
Ví dụLũy Thừa Chưa Dùng
Đề. Cho . Mỗi thao tác chọn ( nguyên tố, ) chia hết giá trị hiện tại của và chưa từng được chọn, rồi chia cho . Tìm số thao tác lớn nhất.
Lời giải. Phân tích thành tích lũy thừa các số nguyên tố, nguyên tố có số mũ . Các thao tác dùng nguyên tố chọn các số mũ đôi một khác nhau có tổng không vượt , và các nguyên tố không tranh nhau gì, nên bài tách theo từng nguyên tố. Với số mũ , số nguyên dương phân biệt có tổng ít nhất , nên chọn các số mũ chừng nào còn đủ là tối ưu. Kiểm với : số mũ 3 cho hai thao tác ( và ), số mũ 2 cho một (), tổng 3.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, ans;
void take(ll e) {
for (ll k = 1; e >= k; k++) {
e -= k;
ans++;
}
}
signed main() {
cin >> n;
for (ll d = 2; d * d <= n; d++) {
if (n % d) continue;
ll e = 0;
while (n % d == 0) n /= d, e++;
take(e);
}
if (n > 1) take(1);
cout << ans << '\n';
}
72
3
Sàng Eratosthenes
Cần biết số nào trong là nguyên tố, với cỡ . Thử chia từng số tốn tổng cỡ . Sàng Eratosthenes lật ngược cách hỏi: thay vì mỗi số đi tìm ước, mỗi số nguyên tố tự gạch các bội của nó. Duyệt ; nếu chưa bị gạch thì nguyên tố, và gạch . Bắt đầu từ vì bội với đã bị gạch bởi ước nguyên tố của ; chỉ cần vì hợp số có ước nguyên tố không quá .
for (ll i = 2; i * i <= n; i++)
if (!comp[i])
for (ll j = i * i; j <= n; j += i) comp[j] = true;
Mỗi nguyên tố gạch khoảng số, cộng lại chỉ cỡ phép, gần như tuyến tính: chừng 50 ms. Mảng bool tốn byte; bitset hoặc vector<bool> chỉ byte khi cỡ .
Hỏi nhanh
Một bạn sàng tới nhưng cho vòng ngoài chạy for (int i = 2; i <= n; i++), và nếu i chưa bị gạch thì vòng trong bắt đầu for (int j = i * i; j <= n; j += i). Mọi biến đều kiểu int. Điều gì xảy ra?
vượt . Phép nhân int tràn, thường quay thành số âm, j <= n vẫn đúng và chương trình ghi ngoài mảng. Lập luận “j <= n sai ngay” đúng với số toán học nhưng không đúng với số trên máy. Sửa bằng cách cho vòng ngoài dừng khi i * i > n và dùng long long.
Khi tới nhưng chỉ hỏi trên một đoạn ngắn cỡ số, ta sàng đoạn (segmented sieve): dùng các nguyên tố , mỗi gạch từ bội đầu tiên của nó trong đoạn, nhưng không sớm hơn .
Ví dụBa Ước Dương
Đề. số . Với mỗi số, có đúng ba ước dương không?
Lời giải. chỉ khi có đúng một nguyên tố với số mũ 2, tức . Vậy cần: là số chính phương và căn của nó là nguyên tố. Căn tới nên sàng tới một lần, mỗi truy vấn tính căn nguyên đúng rồi tra sàng. Căn số thực có thể lệch một đơn vị ở cỡ , nên sửa lại bằng hai vòng while.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll M = 1e6;
ll n;
bool comp[M + 5];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
comp[0] = comp[1] = true;
for (ll i = 2; i * i <= M; i++)
if (!comp[i])
for (ll j = i * i; j <= M; j += i) comp[j] = true;
cin >> n;
while (n--) {
ll x;
cin >> x;
ll r = sqrtl(x);
while (r * r > x) r--;
while ((r + 1) * (r + 1) <= x) r++;
cout << (r * r == x && !comp[r] ? "YES" : "NO") << '\n';
}
}
6
4 5 9 16 25 49
YES
NO
YES
NO
YES
YES
Đếm theo bội
Có số , mỗi số cần phân tích. Thử chia từng số tốn tổng cỡ . Sàng sẵn , ước nguyên tố nhỏ nhất (smallest prime factor) của mọi : duyệt tăng dần, nếu chưa có thì nguyên tố và gán cho mọi bội còn trống. Phân tích khi đó chỉ là chia liên tiếp cho , không quá bước.
for (ll i = 2; i <= M; i++)
if (!spf[i])
for (ll j = i; j <= M; j += i)
if (!spf[j]) spf[j] = i;
Muốn số ước hay tổng các ước cho mọi cùng lúc, ta đảo vòng lặp: thay vì hỏi “số có những ước nào”, hỏi “ước nằm trong những số nào”, tức các bội . Số phép tính là , khoảng với .
Hàm phi Euler là số các với nguyên tố cùng nhau với , bằng , với là các ước nguyên tố của . Sàng tương ứng: đặt , rồi với mỗi nguyên tố và mỗi bội của thì . Khi cỡ trở lên, sàng tuyến tính gạch mỗi hợp số đúng một lần bởi ước nguyên tố nhỏ nhất của nó, chạy trong .
Lũy thừa nhanh
Tính với tới . Viết ở hệ nhị phân: nên . Các lũy thừa có từ nhau bằng bình phương, nên chỉ cần khoảng phép bình phương và nhiều nhất phép nhân vào kết quả.
ll power(ll a, ll b, ll m) {
ll res = 1 % m;
a %= m;
while (b) {
if (b & 1) res = res * a % m;
a = a * a % m;
b >>= 1;
}
return res;
}
Vòng lặp giữ bất biến res * a^b luôn bằng đáp án: lẻ thì chuyển một thừa số sang res; rồi . Khởi tạo 1 % m vì modulo 1 thì mọi phần dư là 0. Cùng khung “bình phương rồi nhân theo bit” dùng được cho mọi phép nhân kết hợp được, như nhân ma trận để tính Fibonacci chỉ số (bài 18).
Nghịch đảo và Fermat
Phép chia không đi qua phần dư: , nhưng theo modulo 5 hai số này là và , và không nguyên. Lối thoát là thay “chia cho ” bằng “nhân với nghịch đảo” , số mà chia cho dư 1. Từ đây ta viết (đọc là đồng dư theo modulo ) khi và có cùng phần dư khi chia cho , nên điều kiện trên là . Nghịch đảo tồn tại khi và chỉ khi (bài 47); với nguyên tố, mọi không chia hết cho đều có.
Định lý Fermat nhỏ: nếu không chia hết cho (viết ) thì . Chứng minh ngắn: các số đôi một khác phần dư và đều khác , nên là một hoán vị của ; nhân tất cả lại rồi giản ước . Suy ra
tính bằng lũy thừa nhanh. Để tính tổ hợp cho nhiều câu hỏi, tiền xử lý fact[i] và inv[i] : một lần lũy thừa cho inv[N], rồi đi lùi inv[i - 1] = inv[i] * i. Mỗi câu hỏi . Cách này cần và nguyên tố.
Fermat còn cho hệ quả: nếu thì
Số mũ chỉ cần biết modulo , không phải modulo . Nhờ đó một số mũ khổng lồ như vẫn xử lý được. Nếu thì với mọi , công thức không áp dụng.
Hỏi nhanh
Tính . Số mũ quá lớn nên phải rút gọn trước. Kết quả là bao nhiêu?
nguyên tố và , nên số mũ lấy modulo . Các lũy thừa của 2 modulo 6 luân phiên , nên và kết quả là . Đáp án là cái bẫy: nó ra khi rút gọn số mũ theo , nhầm modulo của số mũ với modulo của kết quả.
Ví dụLũy Thừa Hai Tầng
Đề. bộ với . Tính , quy ước ở cả hai tầng.
Lời giải. Số mũ có thể có cỡ chữ số. Đặt ; vì , chỉ khi . Ba trường hợp: nếu và thì , đáp án là kể cả khi ; ngược lại , nên cho ; còn lại tính rồi in . Nếu trong khi thì và , vẫn khớp. Lỗi hay gặp nhất là rút gọn số mũ theo thay vì .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll P = 1e9 + 7;
ll q, a, b, c;
ll power(ll a, ll b, ll m) {
ll res = 1 % m;
a %= m;
while (b) {
if (b & 1) res = res * a % m;
a = a * a % m;
b >>= 1;
}
return res;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> q;
while (q--) {
cin >> a >> b >> c;
if (b == 0 && c > 0) cout << 1 << '\n';
else if (a == 0) cout << 0 << '\n';
else cout << power(a, power(b, c, P - 1), P) << '\n';
}
}
5
2 3 2
5 0 7
0 0 0
0 2 3
7 10 0
512
1
0
0
7
Đếm bằng phần nguyên
Số bội của trong là . Từ đây có hai công cụ.
Công thức Legendre. Số mũ của nguyên tố trong , ký hiệu , là
vì có số chia hết , mỗi số góp một thừa số cho lũy thừa ấy. . Vòng lặp chỉ cần chia dần: for (ll q = n / p; q; q /= p) s += q;.
Hỏi nhanh
có bao nhiêu chữ số ở cuối khi viết trong hệ thập phân?
Mỗi chữ số 0 cuối ứng với một thừa số , nên đáp án là với và . Đáp án quên rằng mỗi số chứa hai thừa số ; là ; cộng thêm một số hạng của , vốn đã vượt .
Gộp khối. Tổng với có quá nhiều số hạng, nhưng thương chỉ có giá trị khác nhau: với có không quá giá trị của , còn với thương nhỏ hơn . Mỗi giá trị chiếm một khối liên tiếp; khối bắt đầu từ kết thúc ở
Với , các thương chia thành năm khối. Trong mỗi khối thương không đổi, nên những tổng như tổng các chỉ còn là tổng cấp số cộng nhân với thương.
for (ll l = 1, r; l <= n; l = r + 1) {
r = n / (n / l);
ans += (r - l + 1) * (n / l);
}
Ví dụTổng Các Ước Trên Một Đoạn Đầu
Đề. Cho , tính modulo , với là tổng các ước của .
Lời giải. Đếm theo ước: ước có mặt trong đúng số , mỗi lần góp , nên tổng là . Gộp khối: trong khối có thương , phần góp là . Tổng ; hai thừa số khác tính chẵn lẻ nên chia 2 trên thừa số chẵn trước, rồi mới lấy modulo, tránh tích tràn. Khoảng khối.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7;
ll n;
ll sum(ll l, ll r) {
ll a = l + r, b = r - l + 1;
if (a % 2 == 0) a /= 2;
else b /= 2;
return a % MOD * (b % MOD) % MOD;
}
signed main() {
cin >> n;
ll ans = 0;
for (ll l = 1, r; l <= n; l = r + 1) {
ll q = n / l;
r = n / q;
ans = (ans + q % MOD * sum(l, r)) % MOD;
}
cout << ans << '\n';
}
10
87
Khi đi thi
- Đọc giới hạn trước khi chọn công cụ: một số thì thử chia tới căn ( phép), nhiều số cùng cỡ thì sàng, số mũ cỡ hoặc có hai tầng lũy thừa thì lũy thừa nhanh và rút gọn số mũ theo .
- Viết
long longngay từ đầu, vòng lặpd * d <= n, chia trước nhân sau trong BCNN, và đưa phần dư âm về ; phần lớn lỗi ở bài số học là tràn số hoặc sai dấu, không phải sai ý. - Khi bài đếm theo ước hay bội, thử đảo cách đếm: mỗi chạy qua các bội của nó, hoặc gộp theo ; thấy điều kiện chính phương, chia hết, ƯCLN thì đổi sang điều kiện trên số mũ nguyên tố.
Tóm tắt
- chạy bước, với phép chia làm trước.
- Hợp số có ước không quá : kiểm tra nguyên tố, phân tích thừa số (nhớ thừa số cuối) và liệt kê ước theo cặp đều là ; .
- Sàng gạch bội từ với trong ; cho phân tích , đếm theo bội cho , , của mọi .
- trong ; với nguyên tố, và số mũ rút gọn theo khi .
- có giá trị nên gộp khối; .
Bài tập
Đọc thêm
- Sàng nguyên tốVNOI Wiki
- Lũy thừa nhị phânVNOI Wiki
- Euclidean algorithmcp-algorithms
- Modular Multiplicative Inversecp-algorithms
- 数论分块OI Wiki