Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học

Đếm ước (Tin học trẻ toàn quốc 2025 – Vòng loại quốc gia đợt 3 – Bảng B, C1, C2)

Đếm ước
Nguồn: Tin học trẻ toàn quốc 2025 Độ khó: Trung bình Số học Sàng nguyên tố

Đề bài

Với một số nguyên dương $n$, Alice đặc biệt chú ý tới những ước nguyên dương của $n^2$ thoả mãn đồng thời hai điều kiện: nhỏ hơn $n$ và không phải là ước của $n$.

Yêu cầu: cho số nguyên dương $m$, đặt $n = m \times (m + 1) \times (m + 2)$. Hãy đếm số ước nguyên dương của $n^2$ nhỏ hơn $n$ và không là ước của $n$.

Dữ liệu vào:

  • Dòng đầu tiên chứa số nguyên $T$ – số bộ dữ liệu.
  • $T$ dòng tiếp theo, mỗi dòng chứa một số nguyên dương $m$.

Kết quả: gồm $T$ dòng, dòng thứ $i$ là đáp án của bộ dữ liệu thứ $i$.

Subtask:

  • Subtask 1 (25% số điểm): $m \le 10^3$, $T \le 10$.
  • Subtask 2 (25% số điểm): $m \le 10^6$, $T \le 10$.
  • Subtask 3 (25% số điểm): $m \le 10^6$, $T \le 10^3$.
  • Subtask 4 (25% số điểm): $m \le 10^6$, $T \le 10^5$.

Giới hạn thời gian: 0,2 giây (rất chặt). Dữ liệu nhập từ bàn phím và in ra màn hình.

Ví dụ – input
2
1
2
Ví dụ – output
1
3

Giải thích:

  • $m = 1$: $n = 6$, $n^2 = 36$. Các ước của 36 nhỏ hơn 6 là $1, 2, 3, 4$; chỉ có $4$ không là ước của 6 → đáp án 1.
  • $m = 2$: $n = 24$, $n^2 = 576$. Các ước của 576 nhỏ hơn 24 mà không là ước của 24 là $9, 16, 18$ → đáp án 3.

Nguồn: Hội thi Tin học trẻ toàn quốc 2025 – Vòng loại quốc gia đợt 3 – Bảng B, C1, C2 (bài DIV). Đề được mình viết lại cho dễ đọc; phần giải thích ví dụ là mình tự thêm.

Lời mở đầu

Bài này là một bài số học "chuẩn mực": vét cạn thì dễ nhưng chậm khủng khiếp ($n$ có thể tới $10^{18}$), còn lời giải đẹp chỉ cần một nhận xét ghép cặp và công thức số ước. Phần còn lại là cuộc đua tốc độ: giới hạn thời gian chỉ 0,2 giây cho $10^5$ truy vấn, nên phải biết dùng sàng ước nguyên tố nhỏ nhất. Mình đi lần lượt 3 cách, mỗi cách ăn thêm một subtask.

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.

Nộp bài trên CLUE OJ

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

Bộ testBộ test có gì?

Bộ test của mình gồm 20 test, chia đều 5 test cho mỗi subtask đúng tỉ lệ 25% của đề. Test 1 là ví dụ của đề. Lời giải cuối cùng (cách 3, bản đọc nhanh) đã được nộp thử trên hệ thống chấm chính thức tinhoctre.vn và đạt 100/100 điểm.

NhómTest$T$ lớn nhất$m$ nhỏ nhất – lớn nhấtĐáp án lớn nhất
Subtask 11–5101 – 1.00012.868
Subtask 26–10101 – 1.000.000807.734
Subtask 311–151.0001 – 1.000.0002.842.790
Subtask 416–20100.0001 – 1.000.0003.075.497
  • Test 19 có $10^5$ truy vấn đều là $m = 10^6$ (khó nhất cho cách thử chia vì $m, m+1, m+2$ đều lớn). Các test khác có $m$ ngẫu nhiên, $m$ sát $10^6$, $m$ có nhiều ước (như 720, 840, 554400...).
  • Đáp án lớn nhất khoảng $3 \cdot 10^6$ – vừa int, nhưng $n$ thì tới khoảng $10^{18}$ nên phải dùng long long khi tính $n$.

Kết quả chạy thật (giới hạn 0,2 giây, đo thời gian CPU) của các cách theo từng subtask:

SubtaskĐiều kiệnTestCách 1
vét cạn
Cách 2
thử chia
Cách 2
Python
Cách 3
sàng
Cách 3
Python
Subtask 1 (25%)$m \le 10^3$, $T \le 10$1–52/55/55/55/55/5
Subtask 2 (25%)$m \le 10^6$, $T \le 10$6–100/55/55/55/50/5
Subtask 3 (25%)$m \le 10^6$, $T \le 10^3$11–150/55/55/55/51/5
Subtask 4 (25%)$m \le 10^6$, $T \le 10^5$16–200/51/50/55/50/5

Ý tưởngGhép cặp các ước của n²

Nhận xét 1 – ghép cặp. Nếu $d$ là ước của $n^2$ thì $\dfrac{n^2}{d}$ cũng là ước của $n^2$, và:

  • $d < n \iff \dfrac{n^2}{d} > n$. Vậy các ước nhỏ hơn $n$ và các ước lớn hơn $n$ ghép thành từng cặp, số lượng bằng nhau.
  • Riêng $d = n$ ghép với chính nó.

Gọi $\tau(x)$ là số ước dương của $x$. Suy ra: số ước của $n^2$ nhỏ hơn $n$ là $\dfrac{\tau(n^2) - 1}{2}$.

Nhận xét 2 – trừ đi các ước của $n$. Mọi ước của $n$ đều là ước của $n^2$. Các ước của $n$ nhỏ hơn $n$ có $\tau(n) - 1$ số (bỏ chính $n$). Vậy:

Công thức
$$\text{Đáp án} = \frac{\tau(n^2) - 1}{2} - \big(\tau(n) - 1\big).$$

Nhận xét 3 – công thức số ước. Nếu $n = p_1^{e_1} p_2^{e_2} \cdots p_k^{e_k}$ thì $n^2 = p_1^{2e_1} \cdots p_k^{2e_k}$, nên

$$\tau(n) = \prod (e_i + 1), \qquad \tau(n^2) = \prod (2e_i + 1).$$

Và ta không cần phân tích số $n$ khổng lồ: chỉ cần phân tích riêng $m$, $m + 1$, $m + 2$ (mỗi số $\le 10^6 + 2$) rồi cộng dồn số mũ của cùng một số nguyên tố. Thử với $m = 2$: $n = 2 \cdot 3 \cdot 4 = 2^3 \cdot 3^1$, $\tau(n) = 4 \cdot 2 = 8$, $\tau(n^2) = 7 \cdot 3 = 21$, đáp án $= 10 - 7 = 3$. Đúng!

Em nhập $m$ (nhỏ) rồi bấm Tiếp để xem các ước của $n^2$ được ghép cặp và đếm thế nào:

Ví dụ nhanh:

Cách 1Vét cạn theo định nghĩa

Duyệt mọi $d$ từ 1 tới $n - 1$, đếm các $d$ chia hết $n^2$ nhưng không chia hết $n$. Code ngắn, chắc chắn đúng – rất nên viết để kiểm tra các cách sau.

C++▶ Chạy tay từng dòng codeCách 1: thử mọi d từ 1 tới n − 1.
Dữ liệu mẫu:
Không gian$O(1)$
Thời gian$O(T \cdot n)$, $n \approx m^3$

Kết quả chạy thật: Bản C++ đúng 2/20 test, quá thời gian test 3–20, chạy lâu nhất 1.003s. Bản Python đúng 2/20 test, quá thời gian test 3–20, chạy lâu nhất 1.072s. Chỉ 2 test có $m \le 10$ là kịp; với $m = 1000$ thì $n \approx 10^9$, mỗi truy vấn đã là $10^9$ phép chia – quá chậm ngay từ subtask 1.

Cách 2Công thức + phân tích bằng thử chia

Dùng công thức ở phần ý tưởng. Để phân tích $x$ ra thừa số nguyên tố, ta thử chia $x$ cho $p = 2, 3, 4, \ldots$ khi $p^2 \le x$; mỗi lần chia hết thì tăng số mũ của $p$. Phần còn lại $> 1$ (nếu có) là một số nguyên tố. Số mũ được cộng dồn vào map f cho cả ba số $m, m+1, m+2$.

C++▶ Chạy tay từng dòng codeCách 2: công thức số ước, phân tích m, m+1, m+2 bằng thử chia.
Dữ liệu mẫu:
  1. Không cần kiểm tra $p$ có nguyên tố không: khi tới $p$ hợp số thì các ước nguyên tố nhỏ hơn của nó đã bị chia hết khỏi $x$, nên $x$ không thể chia hết cho $p$.
  2. Vì $m, m+1, m+2$ đôi một chỉ có thể chung ước nguyên tố 2 (giữa $m$ và $m+2$), nên việc "cộng dồn" thật ra chỉ xảy ra với $p = 2$ – nhưng dùng map thì không cần nghĩ tới chuyện đó.
Không gian$O(\log m)$
Thời gian$O(T \sqrt{m})$

Kết quả chạy thật: Bản C++ đúng 16/20 test, quá thời gian test 16–19, chạy lâu nhất 0.333s. Bản Python đúng 15/20 test, quá thời gian test 16–20, chạy lâu nhất 1.008s. Mỗi truy vấn tốn khoảng $3 \cdot \sqrt{10^6} = 3000$ phép chia; với $T = 10^3$ là $3 \cdot 10^6$ phép – nhẹ nhàng, nhưng với $T = 10^5$ là $3 \cdot 10^8$ phép chia, vượt xa 0,2 giây → mất subtask 4.

Cách 3Sàng ước nguyên tố nhỏ nhất (full điểm)

Thay vì thử chia, ta sàng trước mảng spf[x] = ước nguyên tố nhỏ nhất của $x$, với mọi $x \le 10^6 + 2$. Khi đó phân tích $x$ cực nhanh: lấy $p = \text{spf}[x]$, chia $x$ cho $p$ tới khi hết, lặp lại với phần còn lại. Mỗi số $x \le 10^6$ có không quá 19 thừa số nguyên tố (tính cả lặp), nên mỗi truy vấn chỉ vài chục phép tính.

C++▶ Chạy tay từng dòng codeCách 3: sàng spf rồi phân tích m, m+1, m+2 bằng cách tra bảng.
Dữ liệu mẫu:
Hàm sieve() chạy hàng triệu bước nên khung chạy tay coi như đã sàng xong; các bước sau đó được mô phỏng đầy đủ.
  1. Sàng: với mỗi $i$ chưa có spf (tức $i$ nguyên tố), duyệt các bội $j$ của $i$ và gán spf[j] = i nếu chưa gán. Tổng số bước khoảng $N \log \log N$.
  2. Thay map bằng vector các cặp (p, e) – $n$ chỉ có khoảng chục ước nguyên tố khác nhau nên tìm tuyến tính còn nhanh hơn map.
  3. Dùng scanf/printf cho nhanh. Bản mình nộp lên tinhoctre.vn còn đọc bằng fread và dùng sàng tuyến tính, nhưng bản trên đã chạy dưới 0,06 giây CPU trên test lớn nhất.
Không gian$O(M)$, $M = 10^6$
Thời gian$O(M \log \log M + T \log m)$

Kết quả chạy thật: Bản C++ đúng 20/20 test, chạy lâu nhất 0.060s – full điểm. Bản Python đúng 6/20 test, quá thời gian test 6–13, 15–20, chạy lâu nhất 0.638s: riêng việc sàng $10^6$ phần tử bằng vòng lặp Python đã tốn khoảng 0,5 giây, vượt giới hạn 0,2 giây – với giới hạn này Python chỉ ăn được những test có $m$ nhỏ. Bạn nào thi bằng Python thì cách 2 (thử chia) lại có lợi hơn ở subtask 1–3.

Tổng kếtKết quả chạy thật trên bộ test

CáchNgôn ngữĐúngLâu nhấtGhi chú
1. Vét cạn mọi dC++2/201.003s0 điểm (tính theo subtask)
1. Vét cạn mọi dPython2/201.072s0 điểm (tính theo subtask)
2. Công thức + thử chiaC++16/200.333s75 điểm (tính theo subtask)
2. Công thức + thử chiaPython15/201.008s75 điểm (tính theo subtask)
3. Công thức + sàngC++20/200.060s100 điểm (tính theo subtask)
3. Công thức + sàngPython6/200.638s25 điểm (tính theo subtask)
Ghi chú của mình: thời gian là thời gian CPU đo trên máy mình với giới hạn 0,2 giây; điểm ở cột ghi chú tính theo subtask (đúng hết test của subtask mới được điểm subtask đó). Hai "đồ nghề" cần nhớ từ bài này: (1) ước của $n^2$ ghép cặp $d \leftrightarrow n^2/d$ quanh $n$; (2) sàng spf để phân tích thừa số nguyên tố nhiều số trong $O(\log x)$ mỗi số. Lời giải cách 3 đã đạt 100/100 trên tinhoctre.vn.
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇