Số học cho lập trình thi đấu · Buổi 1

Thừa số nguyên tố: Nền tảng Số học

  1. 1Đánh giá độ phức tạp: Tại sao duyệt đến √n?
  2. 4Ước chung lớn nhất (GCD) và Bội chung nhỏ nhất (LCM)
  3. 2Tại sao số lượng ước số tuân theo quy tắc nhân?
  4. 5100! có bao nhiêu chữ số 0 ở cuối?
  5. 3Thuật toán Sàng Eratosthenes: Tiếp cận loại bỏ
  6. 6Xử lý số lớn: Kỹ thuật Đồng dư thức

100 cánh tủ: cái nào còn mở?

100 tủ, ban đầu đều đóng. Bạn thứ k (k = 1, 2, …, 100) đảo trạng thái mọi tủ có số là bội của k.

Ghi dự đoán ra bảng con: cuối cùng những tủ nào còn mở?
Tủ mở: 1, 4, 9, 16, 25, 36, 49, 64, 81, 100
Vì sao toàn là số chính phương?

Tủ số 12 bị chạm bao nhiêu lần?

123456789101112

Bạn thứ k chạm tủ 12 khi 12 là bội của k, tức k là ước của 12.

Tủ 12 bị chạm 6 lần, đúng bằng số lượng ước số của 12.

Tủ mở ⟺ bị chạm một số lẻ lần ⟺ số lượng ước số là một số lẻ.

Các ước đi thành cặp

24: 4 cặp, tức 8 ước. Số chẵn.

36: 4 cặp, cộng thêm số 6 tự ghép với chính nó. 9 ước, số lẻ.

Điều kiện để số lượng ước số là số lẻ

Chứng minh
1Ước d luôn đi cùng ước n/d.
2Ước khác nhau ghép thành cặp: tổng số lượng ước số là số chẵn.
3Ước lẻ loi chỉ có khi d = n/d, tức n = d2.
4Vậy số lượng ước số lẻ ⟺ n là số chính phương.∎
→Tủ mở: 1, 4, 9, …, 100, tức 10 tủ.

Vì sao mỗi cặp luôn có một ước ≤ √n?

1Giả sử cả d và n/d đều lớn hơn √n.
2Nhân lại: d · (n/d) > √n · √n = n.
3Nhưng d · (n/d) = n. Vô lý.
4Vậy một trong hai ước phải ≤ √n.∎
→Tìm ước: chỉ cần thử d từ 1 tới √n.

Từ cặp ước đến thuật toán

for (int i = 1; i * i <= n; i++)
  if (n % i == 0) {
    d.push_back(i);
    if (i != n / i)
      d.push_back(n / i);
  }

Với n = 1012: thử tới √n mất khoảng 3 ms; thử hết mất khoảng 48 phút.

n = 36
in % iước thêm vào
101, 36
202, 18
303, 12
404, 9
51—
606 (một lần)
36 có 9 ước: 1, 2, 3, 4, 6, 9, 12, 18, 36

Đoạn code này in ra mấy ước của 36?

for (int i = 1; i * i <= n; i++)
  if (n % i == 0) {
    d.push_back(i);
    d.push_back(n / i);
  }

Kiểm tra nguyên tố: dừng ở √n

using ll = long long;
bool isPrime(ll n) {
  if (n < 2) return false;
  for (ll i = 2; i * i <= n; i++)
    if (n % i == 0) return false;
  return true;
}
n = 1: dừng ngay. Số 1 không phải số nguyên tố.
n = 91: tới i = 7 thì chia hết, vì 91 = 7 · 13.
n = 97: thử i = 2 … 9, không chia hết. Là số nguyên tố.

Tính phân diễn duy nhất của Thừa số nguyên tố

Cả hai cây đều ra 2 · 2 · 2 · 3 · 3 = 23 · 32.

Định lý cơ bản của số học. Mọi số n ≥ 2 đều được phân diễn duy nhất dưới dạng tích các thừa số nguyên tố (không kể thứ tự). Phần “duy nhất” hôm nay ta thừa nhận.

Vì sao mọi số từ 2 trở lên đều tách được?

Ví dụ: 60 = 6 · 10 = (2 · 3) · (2 · 5)

1Nếu n là số nguyên tố: xong.
2Nếu không, n = a · b với 2 ≤ a, b < n.
3a và b nhỏ hơn n nên tách được (quy nạp).
4Ghép hai cách tách lại: n tách được.∎
→Phần “chỉ một cách” cần thêm một bổ đề; hôm nay ta thừa nhận.

Các ước của 72 nằm vừa trong một lưới 4 × 3

72 = 23 · 32

72 có bao nhiêu ước? Ghi ra bảng con.

Mỗi ước có dạng 2a · 3b.

a ∈ {0, 1, 2, 3}: 4 cách.
b ∈ {0, 1, 2}: 3 cách.

4 × 3 = 12 ước.

Thêm một số nguyên tố, thêm một chiều

360 = 23 · 32 · 5

không có thừa số 5
82472412362618139
có một thừa số 5
40120360206018010309051545
4 × 3 × 2 = 24 ước

Vì sao số lượng ước số là tích các (ai + 1)?

n = p₁a₁ · p₂a₂ · p₃a₃ · …

1Ước d của n chỉ chứa các nguyên tố p₁, p₂, …
2Số mũ bi của pi trong d: 0 ≤ bi ≤ ai.
3Mỗi bộ (b₁, b₂, …) cho đúng một ước.
4Có (ai + 1) cách chọn mỗi bi, độc lập nhau.
5Vậy số lượng ước số = (a₁ + 1)(a₂ + 1)(a₃ + 1)…∎

Giải bài toán tủ: Phân tích số mũ của Thừa số nguyên tố

Ví dụ: 36 = 22 · 32 có (2 + 1)(2 + 1) = 9 ước.

1Số lượng ước số = (a₁ + 1)(a₂ + 1)…
2Tích lẻ ⟺ mọi thừa số đều lẻ.
3ai + 1 lẻ ⟺ ai chẵn.
4Mọi số mũ chẵn ⟺ n là số chính phương.∎
Hai cách nhìn, một kết luận: ghép cặp ước (slide 5) và số mũ chẵn. Tủ mở: 1, 4, 9, …, 100.

Viên gạch cuối cùng dễ bị bỏ sót

for (int p = 2; p * p <= n; p++)
  while (n % p == 0) {
    f.push_back(p);
    n /= p;
  }
if (n > 1) f.push_back(n);
n = 102
pnviệc làm
2102 → 51thêm 2
351 → 17thêm 3
417không chia hết
51725 > 17: dừng
—17 > 1thêm 17
102 = 2 · 3 · 17. Vì sao 17 là số nguyên tố? Nó không chia hết cho 2, 3, 4, tức mọi số từ 2 tới √17.

Số nhỏ nhất có đúng 6 ước là số nào?

Khi nào tích i · j là số chính phương?

s(n) = tích các nguyên tố có số mũ lẻ trong n. Ví dụ s(12) = 3.

1Số mũ của p trong i · j là x + y.
2x + y chẵn ⟺ x và y cùng chẵn lẻ.
3Mọi p đều vậy ⟺ i, j có cùng nguyên tố mũ lẻ.
4Vậy i · j chính phương ⟺ s(i) = s(j).∎
→s(12) = s(27) = 3, và 12 · 27 = 324 = 182.

Luyện tập: số lẻ ≤ 200 có đúng 8 ước

Có bao nhiêu số lẻ từ 1 đến 200 có đúng 8 ước? Làm trên giấy.

Gợi ý 1. 8 = 8 = 4 · 2 = 2 · 2 · 2, nên số đó có dạng p7, p3 · q hoặc p · q · r.
Gợi ý 2. Số lẻ thì không có thừa số 2, mà 37 = 2187 đã lớn hơn 200.
Đáp án: 5 số. 105 = 3 · 5 · 7, 135 = 33 · 5, 165 = 3 · 5 · 11, 189 = 33 · 7, 195 = 3 · 5 · 13.
dạng ABC106 B

Nghỉ 5 phút

Nghĩ thử trong lúc nghỉ: vì sao số 1 không được coi là số nguyên tố?

Thử từng số thì mất bao lâu?

Muốn tìm mọi số nguyên tố ≤ 107.

Cách đã biết: với mỗi số, thử chia tới √ của nó.

thử chia≈ 1,7 tỉ phép chia
Có cách nào khỏi phải hỏi từng số không?

Sàng Eratosthenes: Loại bỏ dần các bội số

Gạch hết bội của 2 và 3 thì còn bao nhiêu số chưa bị gạch, từ 2 đến 100?

bội của 2, gạch từ 4

bội của 3, gạch từ 9

bội của 5, gạch từ 25

bội của 7, gạch từ 49

Vì sao bắt đầu gạch từ p2, không phải 2p?

5 · 2 = 102 đã gạch
5 · 3 = 153 đã gạch
5 · 4 = 202 đã gạch
5 · 5 = 25bắt đầu từ đây
1Xét bội k · p với 2 ≤ k < p.
2k có ước nguyên tố q ≤ k < p.
3q chia hết k · p, nên q đã gạch nó rồi.
4Chỉ còn k ≥ p: bắt đầu từ p · p.∎

Vì sao chỉ cần gạch với p ≤ √N?

Ví dụ: 91 = 7 · 13, và 7 ≤ √100 = 10, nên 91 đã bị 7 gạch.

1Cho hợp số n ≤ N. Viết n = a · b, 2 ≤ a ≤ b.
2a2 ≤ a · b = n ≤ N, nên a ≤ √N.
3Ước nguyên tố p của a: p ≤ a ≤ √N.
4p chia hết n, nên n đã bị p gạch.∎

Cài đặt thuật toán Sàng Eratosthenes

for (int i = 2; i <= N; i++)
  spf[i] = i;
for (int p = 2; p * p <= N; p++)
  if (spf[p] == p)
    for (int j = p * p; j <= N; j += p)
      if (spf[j] == j) spf[j] = p;

Ban đầu: spf[i] = i.

spf[p] == p: chưa ai gạch p, nên p là số nguyên tố.

Gạch từ p2; chỉ ghi spf ở lần gạch đầu tiên.

thử chia≈ 1,7 tỉ
sàng≈ 23 triệu lần gạch

Tiền xử lý SPF để phân tích trong $\mathcal{O}(\log N)$

84 ÷ 2 →42 ÷ 2 →21 ÷ 3 →7 ÷ 7 →1

Mỗi bước chia cho spf của số hiện tại: 84 = 22 · 3 · 7.

while (n > 1) {
  f.push_back(spf[n]);
  n /= spf[n];
}

Mỗi bước chia cho một số ≥ 2, nên n giảm ít nhất một nửa: không quá 23 bước với n ≤ 107.

Sàng đến 100: số nguyên tố lớn nhất phải đi gạch là số nào?

Có bao nhiêu số nguyên tố?

1Giả sử chỉ có hữu hạn: p₁, p₂, …, pk.
2Đặt N = p₁ · p₂ · … · pk + 1.
3N có ước nguyên tố p. p là một trong các pi.
4Nên p chia hết p₁ · … · pk, và chia hết N.
5Vậy p | N − p₁ · … · pk = 1. Vô lý.∎
→N chưa chắc là số nguyên tố: 2 · 3 · 5 · 7 · 11 · 13 + 1 = 30031 = 59 · 509.

GCD và LCM dưới góc nhìn số mũ nguyên tố

72 = 23 · 32
60 = 22 · 3 · 5
gcd = 22 · 3 = 12
lcm = 23 · 32 · 5 = 360

Vì sao gcd · lcm = a · b?

1Với mỗi nguyên tố p, số mũ trong a, b là x, y.
2gcd lấy số mũ nhỏ hơn: min(x, y).
3lcm lấy số mũ lớn hơn: max(x, y).
4min(x, y) + max(x, y) = x + y.∎
→Kiểm tra: 72 · 60 = 4320 = 12 · 360.

Thuật toán Euclid: Khi phân tích nguyên tố bất khả thi

Cắt hình vuông lớn nhất có thể, rồi lặp lại với phần còn thừa.

Vì sao gcd(a, b) = gcd(b, a mod b)?

a = q · b + r, với 0 ≤ r < b

1Nếu d | a và d | b thì d | a − q · b = r.
2Nếu d | b và d | r thì d | q · b + r = a.
3Hai cặp có cùng các ước chung.
4Nên có cùng ước chung lớn nhất.∎
→Vì sao dừng? r < b, số thứ hai giảm dần; tới 0 thì gcd(x, 0) = x.
→gcd(21, 15) = gcd(15, 6) = gcd(6, 3) = gcd(3, 0) = 3

gcd và lcm bằng code, kèm một cái bẫy

int a = 100000, b = 99999;
int g = gcd(a, b);   // g = 1
int l = a * b / g;   // l = ?
using ll = long long;
ll gcd(ll a, ll b) {
  return b == 0 ? a : gcd(b, a % b);
}
ll lcm(ll a, ll b) {
  return a / gcd(a, b) * b;
}

Chia trước rồi mới nhân, và dùng long long: 109 · 109 = 1018 vẫn vừa.

Đánh giá độ phức tạp thuật toán Euclid

1Xét a ≥ b. Nếu b ≤ a/2: a mod b < b ≤ a/2.
2Nếu b > a/2: a mod b = a − b < a/2.
3Vậy a mod b < a/2 trong cả hai trường hợp.
4Sau hai bước, số lớn giảm ít nhất một nửa.∎
→Với số 64-bit: nhiều nhất 90 bước (trường hợp xấu nhất là hai số Fibonacci liên tiếp).

Luyện tập: ABC142 D, tính tay

Chọn nhiều nhất bao nhiêu ước chung của A và B, sao cho hai số bất kỳ đã chọn nguyên tố cùng nhau?

(12, 18) → ?    (420, 660) → ?

Gợi ý 1. Ước chung của A và B chính là ước của gcd(A, B).
Gợi ý 2. gcd(12, 18) = 6 = 2 · 3. Chọn 1, 2, 3: được 3 số.
Đáp án = số lượng ước nguyên tố của gcd, cộng 1 (cho số 1). (420, 660): gcd = 60 = 22 · 3 · 5, nên 3 + 1 = 4.
ABC142 D

100! có bao nhiêu chữ số 0 ở cuối?

10! = 3 628 800

10! có 2 chữ số 0 ở cuối.

100! có bao nhiêu chữ số 0 ở cuối? Ghi dự đoán.
Đáp án: 24. Vì sao?

Chữ số 0 tận cùng được đếm như thế nào?

10! = 28 · 34 · 52 · 7: hai thừa số 5, nên hai số 0.

1Mỗi số 0 ở cuối là một thừa số 10 = 2 · 5.
2Với mỗi i: 2i < 5i, nên bội của 2i nhiều hơn.
3Vậy N! có nhiều thừa số 2 hơn thừa số 5.
4Số 0 ở cuối = số thừa số 5 của N!.∎

Công thức Legendre: Đếm phân hoạch theo bội số

Theo cột: 7

Hàng 1: ⌊30/5⌋ = 6

Hàng 2: ⌊30/25⌋ = 1

1Mỗi k góp một thừa số 5 cho mỗi 5i chia hết k.
2Đếm theo hàng i: có ⌊N/5i⌋ số k như vậy.
3Cộng lại: ⌊N/5⌋ + ⌊N/25⌋ + ⌊N/125⌋ + …∎
→100!: ⌊100/5⌋ + ⌊100/25⌋ = 20 + 4 = 24

Từ công thức đến code

ll zeros(ll n) {
  ll ans = 0;
  while (n > 0) {
    n /= 5;
    ans += n;
  }
  return ans;
}
n = 100
vòngnans
12020
2424
3024

Trả về 24. Mỗi vòng là một hàng: ⌊n/5⌋, ⌊n/25⌋, …

125! có bao nhiêu chữ số 0 ở cuối?

N nhỏ nhất để K chia hết N!

1Phân tích K thành tích các pe.
2Với mỗi pe: tìm N nhỏ nhất mà N! có ít nhất e thừa số p (Legendre và chặt nhị phân).
3Đáp án là N lớn nhất trong các N vừa tìm.

K = 30 = 2 · 3 · 5 → 5   ·   K = 280 = 23 · 5 · 7 → 7

Xử lý số lớn với Đồng dư thức (Modular Arithmetic)

Bây giờ là 0 giờ. 100 giờ sau là mấy giờ?

100 = 8 · 12 + 4: chỉ phần dư 4 là quan trọng.

Đề thi thường viết: “in kết quả theo modulo 109 + 7”. Ví dụ 100! mod (109 + 7) = 437 918 130.

Tính chất phân phối của phép Đồng dư

a = q₁ · m + r₁,  b = q₂ · m + r₂

1a + b = (q₁ + q₂) · m + (r₁ + r₂).
2Bỏ bội của m: a + b và r₁ + r₂ cùng số dư.
3a · b = (q₁q₂m + q₁r₂ + q₂r₁) · m + r₁r₂.
4Tương tự: a · b và r₁ · r₂ cùng số dư.∎
→Đồng hồ (m = 12): 7 + 8 = 15 dư 3; 7 · 8 = 56 dư 8.

−7 % 5 trong C++ bằng bao nhiêu?

Muốn số dư không âm: ((a % m) + m) % m.
Và a, b ≤ 109 thì a · b có thể tới 1018: phải dùng long long.

Thuật toán Lũy thừa nhị phân (Binary Exponentiation)

81
41
20
11
13 = 8 + 4 + 1 = 11012
3bình phương → 32 = 9→ 34 = 81→ 38 = 6561

313 = 38 · 34 · 3 = 6561 · 81 · 3 = 1 594 323

n ≤ 109 chỉ có 30 chữ số nhị phân: khoảng 30 lần bình phương, thay vì 109 phép nhân.

Chứng minh tính đúng đắn của Lũy thừa nhị phân

while (e > 0) {
  if (e % 2 == 1) kq = kq * b % m;
  b = b * b % m;
  e /= 2;
}
Bất biến vòng lặp (Loop Invariant) trong hệ đồng dư

kq · be = an

1Đầu: kq = 1, b = a, e = n. Đẳng thức đúng.
2e chẵn: be = (b2)e/2. Đổi b → b2, e → e/2.
3e lẻ: nhân kq với b, rồi e → e − 1 (chẵn).
4Cuối: e = 0 nên kq = an.∎

Về nhà: CSES 1095 Exponentiation

Tính ab mod (109 + 7) cho tối đa 2 · 105 cặp (a, b), với 0 ≤ a, b ≤ 109. Quy ước 00 = 1.

Nộp tại cses.fi/problemset/task/1095

Gợi ý: dùng đúng vòng lặp ở slide trước. Khi b = 0, vòng lặp không chạy và trả về 1, nên 00 = 1 tự đúng.

Một ý tưởng, một mẹo

1Ước đi theo cặp: chỉ cần thử tới √n.
2Số ước là tích các (số mũ + 1).
3Đi gạch thay vì đi hỏi: sàng và spf.
4gcd lấy mũ nhỏ, lcm lấy mũ lớn; Euclid.
5Đếm theo hàng: ⌊N/5⌋ + ⌊N/25⌋ + …
6Chỉ giữ phần dư; lũy thừa nhị phân.
Ý tưởng: phân tích số thông qua các thừa số nguyên tố. Mẹo: đổi góc nhìn khi đếm.

Phiếu ra về

1. 3600 có bao nhiêu ước?3600 = 24 · 32 · 52, nên có 5 · 3 · 3 = 45 ước.
2. gcd và lcm của 84 và 60?84 = 22 · 3 · 7 và 60 = 22 · 3 · 5: gcd = 12, lcm = 420.
3. 20! có bao nhiêu chữ số 0 ở cuối?⌊20/5⌋ = 4, vì 20 < 25.
Bài về nhà

Mức 1: ABC180 C · ABC142 D · CF 472A · CSES 1618

Mức 2: CSES 1713 · ABC106 B · ABC125 C · ABC215 D · CSES 1095

Mức 3: ABC254 D · ABC280 D · ABC172 D