Chương 0·Bài 08

Arithmetic

N=1012N = 10^{12} có bao nhiêu ước? Thử chia cho mọi số tới NN là 101210^{12} phép chia, quá lâu. Nhưng ước đi theo cặp: dd là ước thì N/dN/d cũng là ước, và một trong hai không vượt N=106\sqrt N = 10^6. 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 aa và m>0m > 0 có đúng một cặp (q,r)(q, r) thỏa a=qm+ra = qm + r và 0≤r<m0 \le r < m. Số rr là phần dư, viết r=a mod mr = a \bmod m; khi r=0r = 0 ta nói aa chia hết cho mm, viết m∣am \mid a. Ba phép ++, −-, ×\times đi qua phần dư được:

(a+b) mod m=((a mod m)+(b mod m)) mod m,(a + b) \bmod m = \big((a \bmod m) + (b \bmod m)\big) \bmod m,

và tương tự cho phép trừ, phép nhân, vì mọi phần chứa mm 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à 33. Muốn phần dư luôn trong [0,m)[0, m) 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 m2≈1018m^2 \approx 10^{18} với m=109+7m = 10^9 + 7, vừa long long nhưng chỉ khi thừa số đã là long long. Với mm vượt khoảng 3⋅1093 \cdot 10^9, 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:

Một trăm triệu phép nhân và cộng theo modulo, trung vị 5 lần chạy, i7-10850H, g++ -O2.ll MOD = 1e9 + 72274 ms · gấp 3,4 lầnconst ll MOD = 1e9 + 7670 ms
Một trăm triệu phép nhân và cộng theo modulo, MOD là biến thường và MOD là hằng, đo trên i7-10850H, g++ -O2.

Chỉ thêm chữ const, vòng lặp nhanh gấp hơn 3 lần.

Hỏi nhanh

Trong C++, với a=−7a = -7 và m=5m = 5 thì a % m bằng −2-2. Biểu thức nào cho phần dư theo định nghĩa toán học, nằm trong [0,m)[0, m), đúng với mọi số nguyên aa?

Ví dụBước Tới Bội Số

Đề. t≤104t \le 10^4 bộ (a,b)(a, b), a,b≤109a, b \le 10^9. Mỗi thao tác tăng aa thêm 1; ít nhất bao nhiêu thao tác để aa chia hết cho bb?

Lời giải. Gọi r=a mod br = a \bmod b. Bội kế tiếp của bb không nhỏ hơn aa là aa nếu r=0r = 0, và a+(b−r)a + (b - r) nếu r>0r > 0. Chỗ dễ sai là in b−rb - r cả khi r=0r = 0, cho bb thay vì 00.

Code đầy đủ
#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

ƯCLN và BCNN

Thuật toán Euclid tính ƯCLN, ký hiệu gcd⁡(a,b)\gcd(a, b), dựa vào một nhận xét: dd chia hết cả aa lẫn bb khi và chỉ khi dd chia hết cả bb lẫn r=a mod br = a \bmod b, vì r=a−qbr = a - qb và a=r+qba = r + qb. Hai cặp có cùng tập ước chung, nên

gcd⁡(a,b)=gcd⁡(b,a mod b),gcd⁡(a,0)=a.\gcd(a, b) = \gcd(b, a \bmod b), \qquad \gcd(a, 0) = a.

Với (252,105)(252, 105): 252=2⋅105+42252 = 2 \cdot 105 + 42, 105=2⋅42+21105 = 2 \cdot 42 + 21, 42=2⋅2142 = 2 \cdot 21, nên gcd⁡=21\gcd = 21. 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.

Mỗi bước cắt đi các hình vuông lớn nhất có thể; hình vuông cuối cùng có cạnh là ƯCLN.

Số bước ít vì phần dư giảm rất nhanh: với a≥ba \ge b ta có a mod b<a/2a \bmod b < a/2 (nếu b≤a/2b \le a/2 thì phần dư nhỏ hơn bb, còn nếu b>a/2b > a/2 thì phần dư là a−ba - b). Cứ hai bước số lớn giảm ít nhất một nửa, nên có O(log⁡min⁡(a,b))O(\log \min(a, b)) bước, khoảng 85 phép chia với giá trị tới 101810^{18}.

C++
ll gcd(ll a, ll b) {
    return b == 0 ? a : gcd(b, a % b);
}

Bội chung nhỏ nhất, ký hiệu lcm⁡(a,b)\operatorname{lcm}(a, b), đi theo ƯCLN: khi phân tích ra nguyên tố, gcd⁡\gcd lấy số mũ nhỏ hơn còn lcm⁡\operatorname{lcm} lấy số mũ lớn hơn, nên gcd⁡⋅lcm⁡=ab\gcd \cdot \operatorname{lcm} = ab và lcm⁡(a,b)=agcd⁡(a,b)⋅b\operatorname{lcm}(a, b) = \frac{a}{\gcd(a, b)} \cdot b. 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 g←gcd⁡(g,ai)g \leftarrow \gcd(g, a_i) từ g=0g = 0; hai số nguyên dương liên tiếp luôn nguyên tố cùng nhau vì gcd⁡(n,n+1)=gcd⁡(n,1)=1\gcd(n, n + 1) = \gcd(n, 1) = 1.

Thừa số và ước

Mọi số n>1n > 1 viết được duy nhất thành n=p1e1⋯pkekn = p_1^{e_1} \cdots p_k^{e_k} với pip_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ố n=d⋅en = d \cdot e với 1<d≤e1 < d \le e thì d2≤nd^2 \le n, nên chỉ cần thử d=2,3,…d = 2, 3, \dots trong khi d2≤nd^2 \le n. Viết d * d <= n với long long, đừng qua số thực sqrt. Với n≤1012n \le 10^{12} là 10610^6 lần chia.

Phân tích thừa số. Chạy đúng vòng lặp ấy, mỗi khi dd chia hết số còn lại thì chia sạch và ghi số mũ. Mọi dd 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 d2d^2 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ụ 360=23⋅32⋅5360 = 2^3 \cdot 3^2 \cdot 5: chia hết 2 ba lần, 3 hai lần, còn 55, tới d=4d = 4 thì 16>516 > 5, thêm 55.

C++
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ừ 00 tới eie_i cho từng nguyên tố, nên số ước của nn, ký hiệu τ(n)\tau(n), bằng (e1+1)(e2+1)⋯(ek+1)(e_1 + 1)(e_2 + 1) \cdots (e_k + 1); 360360 có 4⋅3⋅2=244 \cdot 3 \cdot 2 = 24 ước. Số ước nhỏ hơn nn rất nhiều: tối đa 1344 khi n≤109n \le 10^9, 6720 khi n≤1012n \le 10^{12}. Liệt kê ước: các ước đi theo cặp (d,n/d)(d, n/d) với một ước không vượt n\sqrt n; duyệt dd 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 nn là số chính phương.

Mỗi ước d không vượt √n đi cặp với n/d; đổi n để xem.

Hỏi nhanh

Chạy đoạn phân tích thừa số trên với n=14n = 14: vòng for chia hết 22 rồi dừng, phần còn lại là 77. Với nn 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í dụLũy Thừa Chưa Dùng

Đề. Cho N≤1012N \le 10^{12}. Mỗi thao tác chọn z=pez = p^e (pp nguyên tố, e≥1e \ge 1) chia hết giá trị hiện tại của NN và chưa từng được chọn, rồi chia NN cho zz. Tìm số thao tác lớn nhất.

Lời giải. Phân tích NN thành tích lũy thừa các số nguyên tố, nguyên tố pp có số mũ EpE_p. Các thao tác dùng nguyên tố pp chọn các số mũ đôi một khác nhau có tổng không vượt EpE_p, 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ũ EE, kk số nguyên dương phân biệt có tổng ít nhất 1+2+⋯+k1 + 2 + \dots + k, nên chọn các số mũ 1,2,3,…1, 2, 3, \dots chừng nào còn đủ là tối ưu. Kiểm với 72=23⋅3272 = 2^3 \cdot 3^2: số mũ 3 cho hai thao tác (22 và 44), số mũ 2 cho một (33), tổng 3.

Code đầy đủ
#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

Sàng Eratosthenes

Cần biết số nào trong 1..N1..N là nguyên tố, với NN cỡ 10710^7. Thử chia từng số tốn tổng cỡ NNN\sqrt N. 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 p=2,3,…p = 2, 3, \dots; nếu pp chưa bị gạch thì pp nguyên tố, và gạch p2,p2+p,…p^2, p^2 + p, \dots. Bắt đầu từ p2p^2 vì bội kpkp với k<pk < p đã bị gạch bởi ước nguyên tố của kk; chỉ cần p≤Np \le \sqrt N vì hợp số x≤Nx \le N có ước nguyên tố không quá N\sqrt N.

Ô cam là số nguyên tố đang dùng để gạch, ô đỏ là bội vừa gạch ở bước này.
C++
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ố pp gạch khoảng N/pN/p số, cộng lại chỉ cỡ Nlog⁡log⁡NN \log \log N phép, gần như tuyến tính: N=107N = 10^7 chừng 50 ms. Mảng bool tốn NN byte; bitset hoặc vector<bool> chỉ N/8N/8 byte khi NN cỡ 10810^8.

Hỏi nhanh

Một bạn sàng tới n=107n = 10^7 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?

Khi RR tới 10910^9 nhưng chỉ hỏi trên một đoạn [L,R][L, R] ngắn cỡ 10610^6 số, ta sàng đoạn (segmented sieve): dùng các nguyên tố p≤Rp \le \sqrt R, mỗi pp gạch từ bội đầu tiên của nó trong đoạn, nhưng không sớm hơn p2p^2.

Ví dụBa Ước Dương

Đề. n≤105n \le 10^5 số xi≤1012x_i \le 10^{12}. Với mỗi số, có đúng ba ước dương không?

Lời giải. τ(x)=(e1+1)(e2+1)⋯=3\tau(x) = (e_1 + 1)(e_2 + 1) \cdots = 3 chỉ khi xx có đúng một nguyên tố với số mũ 2, tức x=p2x = p^2. Vậy cần: xx là số chính phương và căn của nó là nguyên tố. Căn tới 10610^6 nên sàng tới 10610^6 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ỡ 101210^{12}, nên sửa lại bằng hai vòng while.

Code đầy đủ
#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

Đếm theo bội

Có q=106q = 10^6 số x≤106x \le 10^6, mỗi số cần phân tích. Thử chia từng số tốn tổng cỡ 10910^9. Sàng sẵn spf[x]\mathrm{spf}[x], ước nguyên tố nhỏ nhất (smallest prime factor) của mọi x≤Mx \le M: duyệt ii tăng dần, nếu spf[i]\mathrm{spf}[i] chưa có thì ii nguyên tố và gán ii cho mọi bội còn trống. Phân tích xx khi đó chỉ là chia liên tiếp cho spf[x]\mathrm{spf}[x], không quá log⁡2x\log_2 x bước.

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 τ(x)\tau(x) hay tổng các ước σ(x)\sigma(x) cho mọi x≤Mx \le M cùng lúc, ta đảo vòng lặp: thay vì hỏi “số xx có những ước nào”, hỏi “ước dd nằm trong những số nào”, tức các bội d,2d,3d,…d, 2d, 3d, \dots. Số phép tính là M/1+M/2+⋯+M/MM/1 + M/2 + \dots + M/M, khoảng 1,4⋅1071{,}4 \cdot 10^7 với M=106M = 10^6.

Hàm phi Euler φ(n)\varphi(n) là số các kk với 1≤k≤n1 \le k \le n nguyên tố cùng nhau với nn, bằng n(1−1/p1)(1−1/p2)⋯n (1 - 1/p_1)(1 - 1/p_2) \cdots, với p1,p2,…p_1, p_2, \dots là các ước nguyên tố của nn. Sàng tương ứng: đặt φ[x]=x\varphi[x] = x, rồi với mỗi nguyên tố pp và mỗi bội xx của pp thì φ[x]−=φ[x]/p\varphi[x] \mathrel{-}= \varphi[x]/p. Khi MM cỡ 10710^7 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 O(M)O(M).

Lũy thừa nhanh

Tính ab mod ma^b \bmod m với bb tới 101810^{18}. Viết bb ở hệ nhị phân: 13=11012=8+4+113 = 1101_2 = 8 + 4 + 1 nên a13=a8⋅a4⋅aa^{13} = a^8 \cdot a^4 \cdot a. Các lũy thừa a2ia^{2^i} có từ nhau bằng bình phương, nên chỉ cần khoảng log⁡2b\log_2 b phép bình phương và nhiều nhất log⁡2b\log_2 b phép nhân vào kết quả.

Cột k là bit thứ k của số mũ, tính từ bit thấp; chỉ bit 1 mới nhân vào res.
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;
}

Vòng lặp giữ bất biến res * a^b luôn bằng đáp án: bb lẻ thì chuyển một thừa số aa sang res; rồi ab=(a2)b/2a^b = (a^2)^{b/2}. 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ố 101810^{18} (bài 18).

Nghịch đảo và Fermat

Phép chia không đi qua phần dư: 124=3\frac{12}{4} = 3, nhưng theo modulo 5 hai số này là 22 và 44, và 24\frac{2}{4} không nguyên. Lối thoát là thay “chia cho bb” bằng “nhân với nghịch đảo” b−1b^{-1}, số mà b⋅b−1b \cdot b^{-1} chia cho mm dư 1. Từ đây ta viết x≡y(modm)x \equiv y \pmod m (đọc là xx đồng dư yy theo modulo mm) khi xx và yy có cùng phần dư khi chia cho mm, nên điều kiện trên là b⋅b−1≡1(modm)b \cdot b^{-1} \equiv 1 \pmod m. Nghịch đảo tồn tại khi và chỉ khi gcd⁡(b,m)=1\gcd(b, m) = 1 (bài 47); với m=pm = p nguyên tố, mọi bb không chia hết cho pp đều có.

Định lý Fermat nhỏ: nếu bb không chia hết cho pp (viết p∤bp \nmid b) thì bp−1≡1(modp)b^{p-1} \equiv 1 \pmod p. Chứng minh ngắn: các số b,2b,…,(p−1)bb, 2b, \dots, (p-1)b đôi một khác phần dư và đều khác 00, nên là một hoán vị của 1,…,p−11, \dots, p - 1; nhân tất cả lại rồi giản ước (p−1)!(p-1)!. Suy ra

b−1≡bp−2(modp),b^{-1} \equiv b^{p-2} \pmod p,

tính bằng lũy thừa nhanh. Để tính tổ hợp (nk)=n!k!(n−k)!\binom{n}{k} = \frac{n!}{k!(n-k)!} cho nhiều câu hỏi, tiền xử lý fact[i] và inv[i] =(i!)−1= (i!)^{-1}: 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 O(1)O(1). Cách này cần n<pn < p và pp nguyên tố.

Fermat còn cho hệ quả: nếu p∤ap \nmid a thì

ae≡a e mod (p−1)(modp).a^e \equiv a^{\,e \bmod (p-1)} \pmod p.

Số mũ chỉ cần biết modulo p−1p - 1, không phải modulo pp. Nhờ đó một số mũ khổng lồ như bcb^c vẫn xử lý được. Nếu p∣ap \mid a thì ae≡0a^e \equiv 0 với mọi e≥1e \ge 1, công thức không áp dụng.

Hỏi nhanh

Tính 3(2100) mod 73^{(2^{100})} \bmod 7. Số mũ 21002^{100} quá lớn nên phải rút gọn trước. Kết quả là bao nhiêu?

Ví dụLũy Thừa Hai Tầng

Đề. q≤105q \le 10^5 bộ (a,b,c)(a, b, c) với 0≤a,b,c≤1090 \le a, b, c \le 10^9. Tính abc mod (109+7)a^{b^c} \bmod (10^9 + 7), quy ước 00=10^0 = 1 ở cả hai tầng.

Lời giải. Số mũ E=bcE = b^c có thể có cỡ 101010^{10} chữ số. Đặt P=109+7P = 10^9 + 7; vì a<Pa < P, P∣aP \mid a chỉ khi a=0a = 0. Ba trường hợp: nếu b=0b = 0 và c>0c > 0 thì E=0E = 0, đáp án là 11 kể cả khi a=0a = 0; ngược lại E≥1E \ge 1, nên a=0a = 0 cho 00; còn lại tính e=bc mod (P−1)e = b^c \bmod (P - 1) rồi in ae mod Pa^e \bmod P. Nếu e=0e = 0 trong khi E≥1E \ge 1 thì (P−1)∣E(P-1) \mid E và aE≡1=a0a^E \equiv 1 = a^0, vẫn khớp. Lỗi hay gặp nhất là rút gọn số mũ theo PP thay vì P−1P - 1.

Code đầy đủ
#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

Đếm bằng phần nguyên

Số bội của dd trong 1..N1..N là ⌊N/d⌋\lfloor N/d \rfloor. Từ đây có hai công cụ.

Công thức Legendre. Số mũ của nguyên tố pp trong n!n!, ký hiệu vp(n!)v_p(n!), là

vp(n!)=⌊np⌋+⌊np2⌋+⌊np3⌋+⋯ ,v_p(n!) = \left\lfloor \frac{n}{p} \right\rfloor + \left\lfloor \frac{n}{p^2} \right\rfloor + \left\lfloor \frac{n}{p^3} \right\rfloor + \cdots,

vì có ⌊n/pi⌋\lfloor n/p^i \rfloor số x≤nx \le n chia hết pip^i, mỗi số góp một thừa số pp cho lũy thừa ấy. v5(100!)=20+4=24v_5(100!) = 20 + 4 = 24. Vòng lặp chỉ cần chia dần: for (ll q = n / p; q; q /= p) s += q;.

Hỏi nhanh

100!100! có bao nhiêu chữ số 00 ở cuối khi viết trong hệ thập phân?

Gộp khối. Tổng ⌊N/1⌋+⌊N/2⌋+⋯+⌊N/N⌋\lfloor N/1 \rfloor + \lfloor N/2 \rfloor + \dots + \lfloor N/N \rfloor với N=1012N = 10^{12} có quá nhiều số hạng, nhưng thương ⌊N/d⌋\lfloor N/d \rfloor chỉ có O(N)O(\sqrt N) giá trị khác nhau: với d≤Nd \le \sqrt N có không quá N\sqrt N giá trị của dd, còn với d>Nd > \sqrt N thương nhỏ hơn N\sqrt N. Mỗi giá trị chiếm một khối dd liên tiếp; khối bắt đầu từ ll kết thúc ở

r=⌊N⌊N/l⌋⌋.r = \left\lfloor \frac{N}{\lfloor N/l \rfloor} \right\rfloor .

Với N=10N = 10, các thương 10,5,3,2,2,1,1,1,1,110, 5, 3, 2, 2, 1, 1, 1, 1, 1 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 d⋅⌊N/d⌋d \cdot \lfloor N/d \rfloor chỉ còn là tổng cấp số cộng l+⋯+rl + \dots + r nhân với thương.

Kéo n: số khối luôn cỡ 2√n, dù số cột là n.
C++
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 N≤1012N \le 10^{12}, tính σ(1)+σ(2)+⋯+σ(N)\sigma(1) + \sigma(2) + \dots + \sigma(N) modulo 109+710^9 + 7, với σ(x)\sigma(x) là tổng các ước của xx.

Lời giải. Đếm theo ước: ước dd có mặt trong đúng ⌊N/d⌋\lfloor N/d \rfloor số x≤Nx \le N, mỗi lần góp dd, nên tổng là 1⋅⌊N/1⌋+2⋅⌊N/2⌋+⋯+N⋅⌊N/N⌋1 \cdot \lfloor N/1 \rfloor + 2 \cdot \lfloor N/2 \rfloor + \dots + N \cdot \lfloor N/N \rfloor. Gộp khối: trong khối [l,r][l, r] có thương qq, phần góp là q⋅(l+⋯+r)q \cdot (l + \dots + r). Tổng l+⋯+r=(l+r)(r−l+1)2l + \dots + r = \frac{(l + r)(r - l + 1)}{2}; 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 102410^{24} tràn. Khoảng 2⋅1062 \cdot 10^6 khối.

Code đầy đủ
#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

Khi đi thi

  • Đọc giới hạn trước khi chọn công cụ: một số N≤1012N \le 10^{12} thì thử chia tới căn (10610^6 phép), nhiều số cùng cỡ 10710^7 thì sàng, số mũ cỡ 101810^{18} hoặc có hai tầng lũy thừa thì lũy thừa nhanh và rút gọn số mũ theo p−1p - 1.
  • Viết long long ngay từ đầu, vòng lặp d * d <= n, chia trước nhân sau trong BCNN, và đưa phần dư âm về [0,m)[0, m); 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 dd chạy qua các bội của nó, hoặc gộp theo ⌊N/d⌋\lfloor N/d \rfloor; 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

  • gcd⁡(a,b)=gcd⁡(b,a mod b)\gcd(a, b) = \gcd(b, a \bmod b) chạy O(log⁡)O(\log) bước, lcm⁡=agcd⁡⋅b\operatorname{lcm} = \frac{a}{\gcd} \cdot b với phép chia làm trước.
  • Hợp số nn có ước không quá n\sqrt n: 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à O(n)O(\sqrt n); τ(n)=(e1+1)(e2+1)⋯\tau(n) = (e_1 + 1)(e_2 + 1) \cdots.
  • Sàng gạch bội từ p2p^2 với p≤Np \le \sqrt N trong O(Nlog⁡log⁡N)O(N \log\log N); spf\mathrm{spf} cho phân tích O(log⁡x)O(\log x), đếm theo bội cho τ\tau, σ\sigma, φ\varphi của mọi x≤Mx \le M.
  • ab mod ma^b \bmod m trong O(log⁡b)O(\log b); với pp nguyên tố, b−1=bp−2b^{-1} = b^{p-2} và số mũ rút gọn theo p−1p - 1 khi p∤ap \nmid a.
  • ⌊N/d⌋\lfloor N/d \rfloor có O(N)O(\sqrt N) giá trị nên gộp khối; vp(n!)=⌊n/p⌋+⌊n/p2⌋+⋯v_p(n!) = \lfloor n/p \rfloor + \lfloor n/p^2 \rfloor + \cdots.

Bài tập

Đọc thêm