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
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Chuyên đề 6: Số học trong đề thi với C++ và Python: đếm bội trong đoạn, bao hàm

Chuyên đề 6: Số học trong đề thi với C++ và Python: đếm bội trong đoạn, bao hàm
Series: Chuyên đề nhập môn Độ khó: Dễ Chuyên đề 6 / Nhập môn lập trình Số học Ước số
// Chuyên đề nhập môn · Chuyên đề 6
Số học trong đề thi: đếm bội, bao hàm – loại trừ, sàng ước, BCNN và số lớn
Đếm số chia hết trong đoạn tới $10^{18}$, đếm ước cho cả triệu số một lúc, BCNN của cả dãy không bị tràn, kiểm tra chia hết với số dài 100000 chữ số: những câu “cho điểm” trong đề thi nếu em biết công thức và tránh được bẫy.

Lời mở đầu

Ở bài 12: Số học cơ bản em đã có đủ “đồ nghề”: phép chia lấy dư, liệt kê ước trong $O(\sqrt{n})$, thuật toán Euclid, sàng Eratosthenes và phân tích thừa số nguyên tố. Nhưng khi vào phòng thi, đề hiếm khi hỏi thẳng “tìm ƯCLN của a và b”. Đề hay hỏi kiểu: đếm các số trong đoạn $[a, b]$ chia hết cho 3 hoặc 5 với $b = 10^{18}$, số nào trong $[1, 10^6]$ có nhiều ước nhất, BCNN của $10^5$ số, hay số có 100000 chữ số có chia hết cho 7 không. Dùng vòng lặp ngây thơ thì quá thời gian, còn dùng sai kiểu dữ liệu thì tràn số.

Chuyên đề này không giảng lại bài 12 mà đi thẳng vào các dạng bài đó: công thức đếm, bẫy số âm, bẫy tràn số, kỹ thuật sàng ước. Em nên nắm chắc bài 12, bài 13: Mảng cộng dồn (dùng lại ở phần bài tập), chuyên đề 2 (chia làm tròn xuống, căn bậc hai chính xác) và chuyên đề 3: Số nguyên lớn trước khi đọc tiếp.

Trong bài này bạn sẽ học: công thức đếm bội của k trong đoạn [a, b] bằng một phép trừ, kể cả khi đoạn chứa số âm; nguyên lý bao hàm – loại trừ với 2 và 3 số chia, vì sao giao là bội của BCNN chứ không phải tích; đếm số chính phương, số lập phương trong đoạn và mẹo “số có lẻ ước là số chính phương”; sàng ước để có số ước, tổng ước của mọi số tới $10^6$ trong nháy mắt; đếm ước từ phân tích thừa số; ƯCLN, BCNN của cả dãy mà không bị tràn số, mảng ƯCLN tiền tố – hậu tố; chia hết với số rất lớn cho dưới dạng xâu, các dấu hiệu chia hết và tổng chữ số lặp. Có 2 khung chạy tay, 3 mô phỏng bấm từng bước và 8 bài tập kèm gợi ý.

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

Phần 1Đếm số chia hết cho k trong đoạn và bao hàm – loại trừ

Câu hỏi khởi động: trong đoạn $[1, n]$ có bao nhiêu số chia hết cho $k$? Các bội của $k$ là $k, 2k, 3k, \ldots$ và bội cuối cùng không vượt $n$ là $\lfloor n / k \rfloor \cdot k$. Vậy có đúng $\lfloor n / k \rfloor$ số. Ví dụ trong $[1, 100]$ có $\lfloor 100 / 7 \rfloor = 14$ số chia hết cho 7. Không cần vòng lặp nào cả!

Với đoạn $[a, b]$ bất kỳ, ta dùng ý tưởng giống mảng cộng dồn ở bài 13: số bội trong $[a, b]$ = số bội trong “$(-\infty, b]$” trừ số bội trong “$(-\infty, a - 1]$”:

$$\text{cnt}(a, b, k) = \left\lfloor \frac{b}{k} \right\rfloor - \left\lfloor \frac{a-1}{k} \right\rfloor$$

Công thức này đúng với mọi $a \le b$, kể cả số âm, với điều kiện $\lfloor \ \rfloor$ là chia làm tròn xuống. Và đây chính là cái bẫy: phép / của C++ làm tròn về 0, không phải làm tròn xuống.

Đoạn, kĐúng (làm tròn xuống)C++ viết b / k - (a - 1) / kCác bội thật sự
$[1, 100]$, $k = 7$$14 - 0 = 14$$14 - 0 = 14$7, 14, …, 98
$[10, 30]$, $k = 4$$7 - 2 = 5$$7 - 2 = 5$12, 16, 20, 24, 28
$[-7, 10]$, $k = 3$$3 - (-3) = 6$$3 - (-2) = 5$ ✗−6, −3, 0, 3, 6, 9
$[-9, -4]$, $k = 5$$-1 - (-2) = 1$$0 - (-2) = 2$ ✗−5

Bảng 1. Với đoạn chứa số âm, phép / của C++ cho kết quả sai. Python dùng // là làm tròn xuống sẵn nên luôn đúng.

Khuôn mẫu đếm bội trong đoạn
long long floorDiv(long long a, long long b){   // b > 0
    long long q = a / b;
    if(a % b < 0) q--;          // a âm và không chia hết: C++ đã làm tròn LÊN, trừ 1
    return q;
}
long long cnt(long long a, long long b, long long k){
    return floorDiv(b, k) - floorDiv(a - 1, k);
}
Hàm floorDiv em đã gặp ở chuyên đề 2. Python chỉ cần b // k - (a - 1) // k. Đừng quên số 0 chia hết cho mọi $k$: đoạn $[-7, 10]$ có số 0 trong danh sách bội.

Bao hàm – loại trừ với hai số

Đề hỏi: trong $[1, 30]$ có bao nhiêu số chia hết cho 4 hoặc cho 6? Gọi $A$ là tập bội của 4 ($|A| = 7$), $B$ là tập bội của 6 ($|B| = 5$). Cộng $7 + 5 = 12$ là sai, vì 12 và 24 nằm trong cả hai tập, bị đếm hai lần. Ta phải trừ phần giao:

$$|A \cup B| = |A| + |B| - |A \cap B|$$

Một số chia hết cho cả 4 và 6 khi và chỉ khi nó chia hết cho BCNN(4, 6) = 12, không phải cho $4 \cdot 6 = 24$. Có $\lfloor 30 / 12 \rfloor = 2$ số như thế, nên đáp án là $7 + 5 - 2 = 10$. Nếu dùng nhầm 24, em được $7 + 5 - 1 = 11$, sai! Tích chỉ đúng khi hai số nguyên tố cùng nhau (như 3 và 5). Em bấm từng bước để thấy những số bị đếm trùng:

MÔ PHỎNGBao hàm – loại trừ trên trục sốNhập đoạn [a, b] và 2 hoặc 3 số chia rồi bấm Tiếp. Mỗi hàng là một tập bội số.
đang đếmbị đếm trùng / phần giaođược đếm đúng 1 lầnthuộc tập

Ba số và hình Venn

Với ba tập, mỗi số trong giao của hai tập bị cộng 2 lần nên phải trừ đi; nhưng số nằm trong cả ba tập bị cộng 3 lần rồi bị trừ 3 lần, thành ra không được đếm lần nào, nên phải cộng lại:

$$|A \cup B \cup C| = |A| + |B| + |C| - |A \cap B| - |A \cap C| - |B \cap C| + |A \cap B \cap C|$$

Quy luật chung: cộng các tập lẻ, trừ các giao chẵn, “lẻ cộng, chẵn trừ”. Ví dụ đếm các số trong $[1, 30]$ chia hết cho 2, 3 hoặc 5:

[1, 30]: 30 sốA: bội của 2B: bội của 3C: bội của 584242118|A| = 15, |B| = 10, |C| = 6|A∩B| = 5 (bội của 6)|A∩C| = 3 (bội của 10)|B∩C| = 2 (bội của 15)|A∩B∩C| = 1 (bội của 30)|A∪B∪C| = 15 + 10 + 6 − 5 − 3 − 2 + 1 = 22ngoài cả ba tập: 30 − 22 = 8

Hình 1. Số trong mỗi vùng của hình Venn. Cộng tất cả các vùng bên trong ba vòng tròn được $8 + 4 + 2 + 4 + 2 + 1 + 1 = 22$, khớp với công thức bao hàm – loại trừ.

Bài này có một hệ quả hay dùng: số các số không chia hết cho số nào trong $x, y, z$ bằng tổng số phần tử trừ đi hợp. Ở hình 1, có $30 - 22 = 8$ số không chia hết cho 2, 3, 5, đó là 1, 7, 11, 13, 17, 19, 23, 29. Code dưới đây đếm các số trong $[a, b]$ ($-10^{18} \le a \le b \le 10^{18}$) chia hết cho ít nhất một trong $x, y, z \le 10^{18}$:

Xem code C++ / Python: đếm số chia hết cho x, y hoặc z trong đoạn [a, b] (bấm để mở)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 4000000000000000000LL;   // "vô cùng": lớn hơn hẳn mọi |a|, |b| <= 10^18

ll floorDiv(ll a, ll b){                // chia làm tròn XUỐNG (b > 0), đúng cả khi a âm
    ll q = a / b;
    if(a % b < 0) q--;
    return q;
}

ll cnt(ll a, ll b, ll k){               // số bội của k trong đoạn [a, b]
    return floorDiv(b, k) - floorDiv(a - 1, k);
}

ll lcmCap(ll x, ll y){                  // BCNN(x, y); nếu vượt 2.10^18 thì trả về INF
    if(x >= INF || y >= INF) return INF;
    x /= __gcd(x, y);                   // chia trước rồi mới nhân
    if(x > 2000000000000000000LL / y) return INF;
    return x * y;
}

int main(){
    ll a, b, x, y, z;
    cin >> a >> b >> x >> y >> z;
    ll xy = lcmCap(x, y), xz = lcmCap(x, z), yz = lcmCap(y, z), xyz = lcmCap(xy, z);
    ll ans = cnt(a, b, x) + cnt(a, b, y) + cnt(a, b, z)
           - cnt(a, b, xy) - cnt(a, b, xz) - cnt(a, b, yz)
           + cnt(a, b, xyz);
    cout << ans;
    return 0;
}
from math import gcd

def cnt(a, b, k):                  # số bội của k trong [a, b]; // của Python đã làm tròn xuống
    return b // k - (a - 1) // k

def lcm(x, y):                     # số nguyên Python không tràn
    return x // gcd(x, y) * y

a, b, x, y, z = map(int, input().split())
xy, xz, yz = lcm(x, y), lcm(x, z), lcm(y, z)
xyz = lcm(xy, z)
ans = (cnt(a, b, x) + cnt(a, b, y) + cnt(a, b, z)
       - cnt(a, b, xy) - cnt(a, b, xz) - cnt(a, b, yz)
       + cnt(a, b, xyz))
print(ans)
BCNN của các số lớn bị tràn
Với $x, y$ tới $10^{18}$, BCNN có thể lên tới $10^{36}$, vượt xa long long. Nhưng nếu BCNN lớn hơn $2 \cdot 10^{18}$ thì trong đoạn $[a, b]$ chỉ có thể có nhiều nhất một bội của nó, đó là số 0 (nếu đoạn chứa 0). Nên ta thay BCNN bằng một số “vô cùng” INF = 4·10^18: công thức floorDiv vẫn đếm đúng số 0 đó. Để phát hiện tràn, kiểm tra x > LIM / y trước khi nhân, đừng nhân xong rồi mới kiểm tra (lúc đó đã tràn rồi). Ở phần 4 ta dùng lại đúng mẹo này.

Phần 2Số chính phương, số lập phương và căn nguyên chính xác

Số chính phương là số có dạng $t^2$: $0, 1, 4, 9, 16, \ldots$ Đếm số chính phương trong $[1, n]$ thật dễ: $t^2 \le n \iff t \le \sqrt{n}$, nên có đúng $\lfloor \sqrt{n} \rfloor$ số. Trong đoạn $[a, b]$ (với $a \ge 1$) lại dùng phép trừ quen thuộc: $\lfloor \sqrt{b} \rfloor - \lfloor \sqrt{a - 1} \rfloor$.

Cái khó nằm ở chỗ tính $\lfloor \sqrt{n} \rfloor$ chính xác khi $n$ tới $10^{18}$. Ở chuyên đề 2 em đã biết sqrt trả về số thực và có thể sai lệch, nên phải sửa bằng hai vòng while. Số lập phương $t^3$ cũng vậy, với cbrt. Ta gom thành hai hàm dùng mãi:

Khuôn mẫu căn nguyên chính xác
long long isqrt_(long long n){                 // lớn nhất r: r*r <= n
    long long r = sqrtl((long double)n);
    while(r * r > n) r--;
    while((r + 1) * (r + 1) <= n) r++;
    return r;
}
long long icbrt_(long long n){                 // lớn nhất r: r*r*r <= n
    long long r = cbrtl((long double)n);
    while(r * r * r > n) r--;
    while((r + 1) * (r + 1) * (r + 1) <= n) r++;
    return r;
}
Trong Python có sẵn math.isqrt(n) chính xác tuyệt đối; căn bậc ba thì tự viết theo cùng khuôn “lấy gần đúng rồi sửa”.

Vừa chính phương vừa lập phương. Đề thi thích hỏi: có bao nhiêu số trong $[a, b]$ là số chính phương hoặc số lập phương? Lại là bao hàm – loại trừ! Phần giao là các số vừa là $u^2$ vừa là $v^3$. Nhìn phân tích thừa số: mọi số mũ phải chia hết cho 2 và cho 3, tức là chia hết cho 6. Vậy giao chính là các số dạng $t^6$.

Đếm $t^6 \le n$ cũng không cần hàm căn bậc sáu: $t^6 \le n \iff t^3 \le \lfloor \sqrt{n} \rfloor$, nên số lượng là icbrt_(isqrt_(n)). Ví dụ trong $[1, 100]$:

Loại sốCác số trong [1, 100]Công thứcSố lượng
Chính phương $t^2$1, 4, 9, 16, 25, 36, 49, 64, 81, 100$\lfloor \sqrt{100} \rfloor$10
Lập phương $t^3$1, 8, 27, 64$\lfloor \sqrt[3]{100} \rfloor$4
Cả hai: $t^6$1, 64$\lfloor \sqrt[3]{\lfloor \sqrt{100} \rfloor} \rfloor = \lfloor \sqrt[3]{10} \rfloor$2
Chính phương hoặc lập phương$10 + 4 - 2$12

Bảng 2. Đếm số chính phương, lập phương trong $[1, 100]$ bằng công thức.

Xem code C++ / Python: đếm số chính phương, lập phương trong đoạn [a, b] tới 10^18 (bấm để mở)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll isqrt_(ll n){                        // lớn nhất r: r*r <= n
    ll r = sqrtl((long double)n);
    while(r * r > n) r--;               // sửa sai số của sqrt
    while((r + 1) * (r + 1) <= n) r++;
    return r;
}

ll icbrt_(ll n){                        // lớn nhất r: r*r*r <= n
    ll r = cbrtl((long double)n);
    while(r * r * r > n) r--;
    while((r + 1) * (r + 1) * (r + 1) <= n) r++;
    return r;
}

ll sq(ll n){ return isqrt_(n); }                // số chính phương trong [1, n]
ll cb(ll n){ return icbrt_(n); }                // số lập phương trong [1, n]
ll six(ll n){ return icbrt_(isqrt_(n)); }       // số dạng t^6 trong [1, n]

int main(){
    ll a, b;
    cin >> a >> b;
    ll s = sq(b) - sq(a - 1), c = cb(b) - cb(a - 1), t = six(b) - six(a - 1);
    cout << s << " " << c << " " << t << " " << s + c - t;
    return 0;
}
from math import isqrt

def icbrt(n):                      # lớn nhất r: r**3 <= n
    r = round(n ** (1 / 3))
    while r ** 3 > n:
        r -= 1
    while (r + 1) ** 3 <= n:
        r += 1
    return r

def six(n):                        # số dạng t^6 trong [1, n]
    return icbrt(isqrt(n))

a, b = map(int, input().split())
s = isqrt(b) - isqrt(a - 1)
c = icbrt(b) - icbrt(a - 1)
t = six(b) - six(a - 1)
print(s, c, t, s + c - t)

Số có lẻ ước chính là số chính phương

Một nhận xét rất hay xuất hiện trong đề thi dưới dạng “bài toán đèn”. Ước của $n$ luôn đi theo cặp $(d, n / d)$: với $n = 12$ có các cặp $(1, 12), (2, 6), (3, 4)$, tổng 6 ước. Một cặp chỉ “gộp làm một” khi $d = n / d$, tức là $d^2 = n$. Vì thế:

  • $n$ không chính phương: mọi ước đều có cặp, số ước chẵn;
  • $n$ chính phương: có đúng một ước $\sqrt{n}$ đứng một mình, số ước lẻ. Ví dụ $36$: $(1, 36), (2, 18), (3, 12), (4, 9)$ và $6$ đứng một mình, tổng 9 ước.

Bài toán đèn: có $n$ bóng đèn đều tắt. Người thứ $i$ ($i = 1..n$) bấm công tắc của mọi đèn có số thứ tự chia hết cho $i$. Cuối cùng đèn nào sáng? Đèn số $j$ bị bấm đúng bằng số ước của $j$ lần, sáng khi bị bấm lẻ lần, tức là $j$ chính phương. Đáp án là $\lfloor \sqrt{n} \rfloor$, dù $n = 10^{18}$ (bài tập 2).

Số chính phương nhìn từ phân tích thừa số
$n$ là số chính phương khi và chỉ khi mọi số mũ trong phân tích thừa số nguyên tố đều chẵn: $3600 = 2^4 \cdot 3^2 \cdot 5^2$. Từ đó: muốn nhân $n$ với số $k$ nhỏ nhất để được số chính phương, ta lấy $k$ = tích các thừa số nguyên tố có số mũ lẻ. Với $n = 360 = 2^3 \cdot 3^2 \cdot 5$ thì $k = 2 \cdot 5 = 10$ và $3600 = 60^2$. Tương tự, số lập phương là số có mọi số mũ chia hết cho 3.

Phần 3Sàng ước: đếm ước và tổng ước cho nhiều số

Bài 12 dạy em liệt kê ước của một số trong $O(\sqrt{n})$. Nhưng nếu đề hỏi số ước của mọi số từ 1 đến $10^6$ (chẳng hạn tìm số có nhiều ước nhất, hay trả lời $10^5$ câu hỏi), làm từng số tốn khoảng $10^6 \cdot 1000 = 10^9$ phép tính, quá chậm.

Mẹo là đảo câu hỏi: thay vì hỏi “$j$ có những ước nào?”, ta hỏi “$i$ là ước của những số nào?”. Câu trả lời rất đơn giản: của $i, 2i, 3i, \ldots$ Vậy với mỗi $i$, ta đi qua tất cả bội của $i$ và cộng 1 vào bộ đếm ước của chúng (và cộng $i$ vào tổng ước). Cách làm này giống hệt sàng Eratosthenes, chỉ khác là ta cộng dồn thay vì gạch bỏ.

MÔ PHỎNGSàng ước: mỗi i cộng vào tất cả bội của nóNhập N (2..24) rồi bấm Tiếp. Hàng d[j] đếm ước, hàng s[j] cộng dồn tổng ước.
bội của i vừa được cộngsố nguyên tố (2 ước)số chính phương (lẻ ước)
Khuôn mẫu sàng ước
for(int i = 1; i <= N; i++)
    for(int j = i; j <= N; j += i){   // j là bội của i  =>  i là ước của j
        d[j]++;                       // số ước
        s[j] += i;                    // tổng ước (nhớ dùng long long)
    }
Thay việc cộng bằng việc khác là có ngay nhiều biến thể: lưu danh sách ước uoc[j].push_back(i), hay tính ước lớn nhất khác $j$.

Vì sao nhanh? Với mỗi $i$, vòng trong chạy $\lfloor N / i \rfloor$ lần. Tổng số phép cộng là

$$\frac{N}{1} + \frac{N}{2} + \frac{N}{3} + \cdots + \frac{N}{N} = N \left(1 + \frac{1}{2} + \cdots + \frac{1}{N}\right) \approx N \ln N$$

Với $N = 10^6$ chỉ khoảng $1{,}4 \cdot 10^7$ phép cộng, chạy trong vài chục mili giây. Em chạy tay với $n$ nhỏ để thấy từng bội được cộng:

Cách làmĐộ phức tạpSố phép tính với $N = 10^6$Kết luận
Duyệt mọi $i$ từ 1 tới $j$ cho từng $j$$O(N^2)$$5 \cdot 10^{11}$quá chậm
Duyệt ước tới $\sqrt{j}$ cho từng $j$ (bài 12)$O(N\sqrt{N})$$\approx 6{,}7 \cdot 10^8$chậm, dễ quá giờ
Sàng ước: cộng vào các bội$O(N \log N)$$\approx 1{,}4 \cdot 10^7$nhanh

Bảng 3. So sánh các cách tính số ước cho mọi số từ 1 tới $N$.

C++▶ Chạy tay từng dòng codeSàng ước: với mỗi i, nhảy qua các bội j = i, 2i, 3i, … và cộng vào d[j], s[j].
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int d[105], s[105];
5
6int main(){
7 int n;
8 cin >> n;
9 for(int i = 1; i <= n; i++)
10 for(int j = i; j <= n; j += i){ // j chạy qua các bội của i
11 d[j]++;
12 s[j] += i;
13 }
14 for(int j = 1; j <= n; j++)
15 cout << j << ": " << d[j] << " " << s[j] << "\n";
16 return 0;
17}

Bài mẫu: có $Q$ câu hỏi, mỗi câu cho $x \le 10^6$, in số ước và tổng các ước của $x$. Ta sàng một lần cho mọi số rồi trả lời mỗi câu trong $O(1)$:

Xem code C++ / Python: sàng số ước và tổng ước tới 10^6 (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const int MAXV = 1000000;
int d[MAXV + 1];
long long s[MAXV + 1];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    for(int i = 1; i <= MAXV; i++)               // sàng ước: i là ước của i, 2i, 3i, ...
        for(int j = i; j <= MAXV; j += i){
            d[j]++;
            s[j] += i;
        }
    int q;
    cin >> q;
    while(q--){
        int x;
        cin >> x;
        cout << d[x] << " " << s[x] << "\n";
    }
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
q = int(data[0])
xs = [int(v) for v in data[1:1 + q]]
M = max(xs)                        # chỉ sàng tới số lớn nhất được hỏi
d = [0] * (M + 1)
s = [0] * (M + 1)
for i in range(1, M + 1):          # sàng ước: i là ước của i, 2i, 3i, ...
    for j in range(i, M + 1, i):
        d[j] += 1
for i in range(1, M + 1):
    for j in range(i, M + 1, i):
        s[j] += i
print('\n'.join(f"{d[x]} {s[x]}" for x in xs))
Python chậm hơn nhiều
Hai vòng lặp lồng nhau với $N = 10^6$ chạy trong C++ khoảng 0,05 giây, nhưng Python mất 2–3 giây. Code Python ở trên chỉ sàng tới số lớn nhất được hỏi và tách hai vòng sàng riêng (mỗi vòng làm ít việc hơn). Nếu đề chỉ cần số ước của vài số, em hãy dùng cách phân tích thừa số ngay dưới đây.

Đếm ước từ phân tích thừa số nguyên tố

Khi $n$ lớn (tới $10^{12}$) mà chỉ có một vài số, ta phân tích $n = p_1^{e_1} \cdot p_2^{e_2} \cdots p_k^{e_k}$ như bài 12. Một ước của $n$ có dạng $p_1^{f_1} \cdots p_k^{f_k}$ với mỗi $f_i$ chọn từ $0$ đến $e_i$, tức là $e_i + 1$ cách chọn cho mỗi số nguyên tố. Nhân lại:

$$d(n) = (e_1 + 1)(e_2 + 1) \cdots (e_k + 1), \qquad \sigma(n) = \prod_{i=1}^{k} \left(1 + p_i + p_i^2 + \cdots + p_i^{e_i}\right)$$

$n$Phân tíchSố ước $d(n)$Tổng ước $\sigma(n)$
12$2^2 \cdot 3$$3 \cdot 2 = 6$$(1+2+4)(1+3) = 28$
36$2^2 \cdot 3^2$$3 \cdot 3 = 9$ (lẻ: chính phương)$7 \cdot 13 = 91$
360$2^3 \cdot 3^2 \cdot 5$$4 \cdot 3 \cdot 2 = 24$$15 \cdot 13 \cdot 6 = 1170$
97$97$ (nguyên tố)$2$$1 + 97 = 98$

Bảng 4. Số ước và tổng ước tính từ phân tích thừa số.

Xem code C++ / Python: số ước và tổng ước của n ≤ 10^12 bằng phân tích thừa số (bấm để mở)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

int main(){
    ll n;
    cin >> n;
    ll cntDiv = 1, sumDiv = 1;
    for(ll p = 2; p * p <= n; p++){
        if(n % p) continue;
        int e = 0;
        ll term = 1, pw = 1;              // term = 1 + p + p^2 + ... + p^e
        while(n % p == 0){
            n /= p; e++;
            pw *= p; term += pw;
        }
        cntDiv *= (e + 1);
        sumDiv *= term;
    }
    if(n > 1){                            // còn lại một thừa số nguyên tố lớn, mũ 1
        cntDiv *= 2;
        sumDiv *= (1 + n);
    }
    cout << cntDiv << " " << sumDiv;
    return 0;
}
n = int(input())
cnt_div, sum_div = 1, 1
p = 2
while p * p <= n:
    if n % p == 0:
        e, term, pw = 0, 1, 1          # term = 1 + p + p^2 + ... + p^e
        while n % p == 0:
            n //= p
            e += 1
            pw *= p
            term += pw
        cnt_div *= e + 1
        sum_div *= term
    p += 1
if n > 1:                              # còn lại một thừa số nguyên tố lớn, mũ 1
    cnt_div *= 2
    sum_div *= 1 + n
print(cnt_div, sum_div)
Ước của một tích rất lớn
Đề có thể hỏi số ước của $P = a_1 \cdot a_2 \cdots a_n$ với $n = 10^5$ số. Đừng nhân $P$ ra (nó có hàng trăm nghìn chữ số)! Hãy phân tích từng $a_i$, cộng dồn số mũ của mỗi số nguyên tố vào một mảng e[p], cuối cùng nhân các $(e[p] + 1)$ lại (thường lấy dư cho $10^9 + 7$, xem bài 26: Toán modulo). Để phân tích nhanh nhiều số, dùng sàng ước nguyên tố nhỏ nhất spf[x]: khi đó tách $x$ chỉ mất $O(\log x)$ phép chia. Bài tập 7 luyện đúng kỹ thuật này.

Phần 4ƯCLN, BCNN của cả dãy và bẫy tràn số

ƯCLN và BCNN đều có tính kết hợp: $\gcd(a, b, c) = \gcd(\gcd(a, b), c)$, BCNN cũng vậy. Nên ƯCLN của cả dãy chỉ là một vòng lặp “gộp dần”, giống cách tính tổng. Mẹo nhỏ: khởi tạo $g = 0$ vì $\gcd(0, x) = x$, và $l = 1$ vì $\text{BCNN}(1, x) = x$.

Với BCNN phải cẩn thận hai lần:

  • Chia trước, nhân sau: viết l / gcd(l, x) * x chứ không viết l * x / gcd(l, x). Cách sau tính tích $l \cdot x$ trước nên tràn sớm hơn rất nhiều.
  • BCNN tăng rất nhanh: BCNN của các số từ 1 đến 50 đã vượt $3 \cdot 10^{20}$. Nếu đề yêu cầu “in -1 nếu BCNN vượt $10^{18}$”, phải kiểm tra trước khi nhân: $t \cdot x > 10^{18} \iff t > \lfloor 10^{18} / x \rfloor$.

Em chạy tay đoạn code sau, nhất là ví dụ “tràn” với ba số nguyên tố gần $10^9$:

C++▶ Chạy tay từng dòng codeƯCLN và BCNN của cả dãy: gộp dần từng phần tử, chia trước rồi nhân, kiểm tra tràn trước khi nhân.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3const long long LIM = 1000000000000000000LL; // 10^18
4
5int main(){
6 int n;
7 cin >> n;
8 long long g = 0, l = 1;
9 bool over = false;
10 for(int i = 0; i < n; i++){
11 long long x;
12 cin >> x;
13 g = __gcd(g, x);
14 if(over) continue;
15 long long t = l / __gcd(l, x);
16 if(t > LIM / x) over = true;
17 else l = t * x;
18 }
19 cout << g << "\n";
20 if(over) cout << -1; else cout << l;
21 return 0;
22}
Xem code C++ / Python: ƯCLN và BCNN của cả dãy, in -1 nếu BCNN vượt 10^18 (bấm để mở)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll LIM = 1000000000000000000LL;   // 10^18

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    ll g = 0, l = 1;
    bool over = false;                  // BCNN đã vượt 10^18 chưa?
    for(int i = 0; i < n; i++){
        ll x;
        cin >> x;
        g = __gcd(g, x);                // ƯCLN(0, x) = x
        if(over) continue;
        ll t = l / __gcd(l, x);         // CHIA TRƯỚC
        if(t > LIM / x) over = true;    // t * x > LIM  <=>  t > LIM / x
        else l = t * x;
    }
    cout << g << "\n";
    if(over) cout << -1; else cout << l;
    return 0;
}
import sys
from math import gcd

data = sys.stdin.buffer.read().split()
n = int(data[0])
LIM = 10 ** 18
g, l, over = 0, 1, False
for v in data[1:1 + n]:
    x = int(v)
    g = gcd(g, x)                      # gcd(0, x) = x
    if not over:
        l = l // gcd(l, x) * x
        if l > LIM:                    # dừng sớm, kẻo số càng ngày càng dài
            over = True
print(g)
print(-1 if over else l)
Hàm có sẵn
C++: __gcd(a, b) có ở mọi trình biên dịch GCC; từ C++17 có thêm gcd(a, b) và lcm(a, b) (nhưng lcm có sẵn vẫn tràn như thường). Python: math.gcd, và từ Python 3.9 có math.lcm(*a) nhận nhiều số. Số nguyên Python không tràn, nhưng BCNN của $10^5$ số có thể dài hàng trăm nghìn chữ số, phép tính trên số khổng lồ đó rất chậm. Vì vậy code Python ở trên cũng dừng sớm khi vượt $10^{18}$.

ƯCLN tiền tố – hậu tố: bỏ đi một phần tử

Dạng bài quen thuộc: bỏ đi đúng một phần tử của dãy để ƯCLN các phần tử còn lại lớn nhất. Thử bỏ từng phần tử rồi tính lại ƯCLN mất $O(n^2)$. Hãy làm giống mảng cộng dồn ở bài 13, nhưng với phép $\gcd$ thay cho phép cộng:

  • pre[i] = ƯCLN của $a_1..a_i$ (tiền tố), suf[i] = ƯCLN của $a_i..a_n$ (hậu tố), với pre[0] = suf[n+1] = 0;
  • bỏ $a_i$ thì ƯCLN phần còn lại là $\gcd(\text{pre}[i-1], \text{suf}[i+1])$.
i1234
$a_i$1215187
pre[i]12331
suf[i]1117
bỏ $a_i$: $\gcd(\text{pre}[i-1], \text{suf}[i+1])$$\gcd(0, 1) = 1$$\gcd(12, 1) = 1$$\gcd(3, 7) = 1$$\gcd(3, 0) = 3$

Bảng 5. Bỏ số 7 thì ƯCLN phần còn lại lớn nhất, bằng 3.

Hai mẹo ƯCLN hay gặp
(1) $\gcd(a, b) = \gcd(a, b - a)$. Vì vậy “tìm $d$ lớn nhất để $a, b, c$ chia $d$ có cùng số dư” chính là ƯCLN của các hiệu $|b - a|$, $|c - b|$. (2) ƯCLN của một đoạn bất kỳ $[l, r]$ không tính được bằng phép trừ như tổng (vì không có “phép trừ” cho $\gcd$). Với ít câu hỏi thì duyệt đoạn; với nhiều câu hỏi thì cần Sparse Table, em sẽ học ở bài 37.

Phần 5Chia hết với số rất lớn, dấu hiệu chia hết, tổng chữ số lặp

Khi đề cho một số có tới $10^5$ chữ số, em phải đọc nó vào xâu. Ở chuyên đề 3 em đã biết cách tính số dư của số lớn cho số nhỏ: đọc từng chữ số từ trái sang phải và chỉ giữ số dư r = (r * 10 + c) % k. Em xem lại cho thật rõ bằng mô phỏng:

MÔ PHỎNGChia lấy dư số rất lớn: đọc từng chữ sốNhập số (tối đa 30 chữ số) và k rồi bấm Tiếp.
chữ số đã đọcchữ số đang đọcsố dư r hiện tại

Bẫy khi $k$ lớn. Nếu $k$ chỉ tới $10^9$ thì $r \cdot 10 + 9 < 10^{10}$, long long chứa thoải mái. Nhưng nếu đề cho $k$ tới $10^{18}$ thì $r$ có thể gần $10^{18}$ và $r \cdot 10$ gần $10^{19}$, vượt giới hạn $9{,}2 \cdot 10^{18}$ của long long! Cách chữa: khai báo $r$ là __int128 (số nguyên 128 bit của GCC) hoặc unsigned long long (tới $1{,}8 \cdot 10^{19}$). Python thì không lo tràn, nhưng có một bẫy khác: từ Python 3.11, int(s) với xâu dài hơn 4300 chữ số sẽ báo lỗi (ValueError), nên cũng nên đọc từng chữ số.

Dấu hiệu chia hết: khi nào chỉ cần nhìn vài chữ số?

Chia hết choDấu hiệuVì saoVí dụ
2, 5, 10chữ số cuối$10$ chia hết cho 2, 512345 ⋮ 5
4, 25số tạo bởi 2 chữ số cuối$100$ chia hết cho 4, 25987652 ⋮ 4
8, 125số tạo bởi 3 chữ số cuối$1000$ chia hết cho 8, 12512344 ⋮ 8
3, 9tổng các chữ số$10 \equiv 1 \pmod 9$$1+2+3+4+5 = 15$ ⋮ 3
11tổng xen kẽ dấu từ chữ số cuối$10 \equiv -1 \pmod{11}$$918082$: $2 - 8 + 0 - 8 + 1 - 9 = -22$ ⋮ 11
6, 12, 15, 18…tách thành các số nguyên tố cùng nhau$12 = 3 \cdot 4$⋮ 12 ⟺ ⋮ 3 và ⋮ 4

Bảng 6. Các dấu hiệu chia hết. Với số dạng xâu, mỗi dấu hiệu chỉ cần vài phép tính trên chữ số.

Tất cả đều xuất phát từ một ý: viết số dưới dạng $\overline{d_1 d_2 \ldots d_m} = d_1 \cdot 10^{m-1} + \cdots + d_m$, rồi xem $10^j$ chia $k$ dư bao nhiêu. Với $k = 9$, mọi $10^j$ chia 9 dư 1, nên số chia 9 dư bằng tổng chữ số chia 9 dư. Cẩn thận với dòng cuối bảng: “chia hết cho 12 khi chia hết cho 2 và 6” là sai (thử số 6), vì 2 và 6 không nguyên tố cùng nhau; phải tách $12 = 3 \cdot 4$.

Tổng chữ số lặp

Đề kinh điển: cộng các chữ số của $n$, được số mới lại cộng chữ số tiếp, cho tới khi còn một chữ số. Ví dụ $98765 \to 35 \to 8$. Vì mỗi lần cộng chữ số thì số dư khi chia 9 không đổi, kết quả chính là $n \bmod 9$, chỉ khác là khi chia hết cho 9 thì được 9 (trừ $n = 0$). Công thức gọn:

$$\text{root}(n) = \begin{cases} 0 & n = 0 \\ 1 + (n - 1) \bmod 9 & n > 0 \end{cases}$$

Kiểm tra: $1 + 98764 \bmod 9 = 1 + 7 = 8$. Với số dạng xâu, ta lấy tổng chữ số (một số nhỏ) rồi áp dụng công thức. Code dưới đây đọc số $N$ (tới $10^5$ chữ số) và $k \le 10^{18}$, in $N \bmod k$ và tổng chữ số lặp của $N$:

Những lỗi số học hay gặp trong phòng thi
  • Đếm bội trong đoạn có số âm mà dùng / của C++ (phần 1).
  • Lấy giao của bội $x$ và bội $y$ bằng tích $x \cdot y$ thay vì BCNN.
  • Tính l * x / gcd (nhân trước) làm BCNN tràn số; hoặc kiểm tra tràn sau khi đã nhân.
  • Dùng (int)sqrt(n) với $n$ tới $10^{18}$ mà không sửa bằng while.
  • Mảng tổng ước khai báo int khi $N$ lớn, hay sàng tới $10^7$ mà quên giới hạn bộ nhớ.
  • Quên trường hợp $n = 0$, $n = 1$: số 0 chia hết cho mọi số, số 1 chỉ có 1 ước, tổng chữ số lặp của 0 là 0.
Xem code C++ / Python: số lớn chia lấy dư cho k tới 10^18 và tổng chữ số lặp (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    string s;
    long long k;
    cin >> s >> k;
    __int128 r = 0;                       // r * 10 có thể tới ~10^19: vượt long long!
    int sum = 0;
    for(char c : s){
        r = (r * 10 + (c - '0')) % k;
        sum += c - '0';
    }
    int root = (sum == 0) ? 0 : 1 + (sum - 1) % 9;   // tổng chữ số lặp
    cout << (long long)r << "\n" << root;
    return 0;
}
import sys

data = sys.stdin.read().split()
s, k = data[0], int(data[1])
r, total = 0, 0
for c in s:                            # không dùng int(s): Python 3.11+ giới hạn 4300 chữ số
    r = (r * 10 + ord(c) - 48) % k
    total += ord(c) - 48
root = 0 if total == 0 else 1 + (total - 1) % 9      # tổng chữ số lặp
print(r)
print(root)
Luyện tập trên web
Các bài dưới đây trên blog đều có lời giải chi tiết bằng C++ và Python, xếp từ dễ tới khó. Em hãy tự làm trước rồi mới xem lời giải nhé.Xem toàn bộ bài cùng chủ đề: nhãn Số học.

Bài tậpLuyện số học trong đề thi

Bài 1Đếm bội trong đoạnDễ

Cho ba số nguyên $a$, $b$, $k$ ($-10^{18} \le a \le b \le 10^{18}$, $1 \le k \le 10^{18}$). Đếm số các số nguyên $x$ thuộc đoạn $[a, b]$ chia hết cho $k$.

Input
-7 10 3
Output
6

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Công thức $\lfloor b / k \rfloor - \lfloor (a - 1) / k \rfloor$ ở phần 1, nhưng phép chia phải làm tròn xuống: trong C++ dùng hàm floorDiv, Python dùng //. Mẫu: $\lfloor 10/3 \rfloor = 3$, $\lfloor -8/3 \rfloor = -3$, đáp án $3 - (-3) = 6$, gồm $-6, -3, 0, 3, 6, 9$. Nếu viết (a - 1) / k trong C++ em sẽ được $-2$ và ra 5, sai.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll floorDiv(ll a, ll b){        // làm tròn xuống, b > 0
    ll q = a / b;
    if(a % b < 0) q--;
    return q;
}

int main(){
    ll a, b, k;
    cin >> a >> b >> k;
    cout << floorDiv(b, k) - floorDiv(a - 1, k);
    return 0;
}
a, b, k = map(int, input().split())
print(b // k - (a - 1) // k)
Bài 2Hành lang đènDễ

Hành lang có $n$ bóng đèn đánh số $1..n$, ban đầu đều tắt. Lần lượt $n$ bạn đi qua; bạn thứ $i$ bấm công tắc (tắt thành bật, bật thành tắt) của mọi đèn có số thứ tự chia hết cho $i$. Sau khi cả $n$ bạn đi qua, trong các đèn từ số $a$ tới số $b$ có bao nhiêu đèn đang sáng? ($1 \le a \le b \le n \le 10^{18}$.) Input: $n$, $a$, $b$.

Input
10 2 9
Output
2

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Đèn $j$ bị bấm đúng bằng số ước của $j$ lần, sáng khi số ước lẻ, tức là $j$ chính phương (phần 2). Đáp án là số chính phương trong $[a, b]$: $\lfloor \sqrt{b} \rfloor - \lfloor \sqrt{a - 1} \rfloor$, nhớ dùng căn nguyên chính xác. Mẫu: trong $[2, 9]$ có hai đèn sáng là 4 và 9. Để ý $n$ không ảnh hưởng tới đáp án!
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll isqrt_(ll n){
    ll r = sqrtl((long double)n);
    while(r * r > n) r--;
    while((r + 1) * (r + 1) <= n) r++;
    return r;
}

int main(){
    ll n, a, b;
    cin >> n >> a >> b;
    cout << isqrt_(b) - isqrt_(a - 1);
    return 0;
}
from math import isqrt

n, a, b = map(int, input().split())
print(isqrt(b) - isqrt(a - 1))
Bài 3BCNN của dãyDễ

Cho dãy $n$ số nguyên dương ($1 \le n \le 10^5$, $1 \le a_i \le 10^9$). In ra BCNN của cả dãy, hoặc in -1 nếu BCNN lớn hơn $10^{18}$.

Input
4
4 6 10 15
Output
60

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Gộp dần $l = l / \gcd(l, a_i) \cdot a_i$, bắt đầu từ $l = 1$. Trước khi nhân, kiểm tra $t = l / \gcd(l, a_i) > 10^{18} / a_i$ thì dừng và in -1 (phần 4). Mẫu: $l$ lần lượt là 4, 12, 60, 60. Trong Python cũng phải dừng sớm, nếu không số sẽ dài tới hàng trăm nghìn chữ số và chạy rất chậm.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll LIM = 1000000000000000000LL;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    ll l = 1;
    bool over = false;
    for(int i = 0; i < n; i++){
        ll x;
        cin >> x;
        if(over) continue;
        ll t = l / __gcd(l, x);
        if(t > LIM / x) over = true;
        else l = t * x;
    }
    if(over) cout << -1; else cout << l;
    return 0;
}
import sys
from math import gcd

data = sys.stdin.buffer.read().split()
n = int(data[0])
l, LIM = 1, 10 ** 18
for v in data[1:1 + n]:
    x = int(v)
    l = l // gcd(l, x) * x
    if l > LIM:
        l = -1
        break
print(l)
Bài 4Số may mắnTrung bình

Bạn Tý coi các số $x$, $y$, $z$ là số xui. Một số nguyên dương được gọi là may mắn nếu nó không chia hết cho số xui nào. Đếm số may mắn trong đoạn $[1, n]$. ($1 \le n \le 10^{18}$, $1 \le x, y, z \le 10^{18}$.) Input: $n$, $x$, $y$, $z$.

Input
30 2 3 5
Output
8

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Đáp án = $n$ trừ số các số chia hết cho ít nhất một trong $x, y, z$, đếm bằng bao hàm – loại trừ với BCNN (phần 1, hình 1). BCNN của hai số tới $10^{18}$ có thể tới $10^{36}$: nếu nó vượt $n$ thì không có bội nào trong $[1, n]$, nên chỉ cần “chặn” BCNN ở giá trị $n + 1$ là đủ. Mẫu: 30 − 22 = 8 số may mắn là 1, 7, 11, 13, 17, 19, 23, 29.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n;

ll lcmCap(ll x, ll y){              // BCNN, nhưng nếu > n thì trả về n + 1
    if(x > n || y > n) return n + 1;
    x /= __gcd(x, y);
    if(x > n / y) return n + 1;     // x * y > n
    return x * y;
}

int main(){
    ll x, y, z;
    cin >> n >> x >> y >> z;
    ll xy = lcmCap(x, y), xz = lcmCap(x, z), yz = lcmCap(y, z), xyz = lcmCap(xy, z);
    ll hop = n / x + n / y + n / z - n / xy - n / xz - n / yz + n / xyz;
    cout << n - hop;
    return 0;
}
from math import gcd

def lcm(u, v):
    return u // gcd(u, v) * v

n, x, y, z = map(int, input().split())
hop = (n // x + n // y + n // z - n // lcm(x, y) - n // lcm(x, z) - n // lcm(y, z)
       + n // lcm(lcm(x, y), z))
print(n - hop)
Bài 5Số dồi dàoTrung bình

Số nguyên dương $x$ gọi là dồi dào nếu tổng các ước của nó (kể cả chính nó) lớn hơn $2x$, ví dụ 12 có tổng ước $28 > 24$. Có $Q$ câu hỏi ($Q \le 10^5$), mỗi câu cho $l$, $r$ ($1 \le l \le r \le 10^6$): đếm số dồi dào trong $[l, r]$.

Input
3
1 30
13 20
1 11
Output
5
2
0

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Sàng tổng ước s[j] cho mọi $j \le 10^6$ (phần 3), đánh dấu số dồi dào, rồi dựng mảng cộng dồn c[j] = số dồi dào trong $[1, j]$ như bài 13; mỗi câu hỏi trả lời bằng c[r] - c[l-1]. Các số dồi dào không quá 30 là 12, 18, 20, 24, 30, nên đoạn $[13, 20]$ có 2 số. Tổng ước của số tới $10^6$ chưa vượt $5 \cdot 10^6$ nên int là đủ.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXV = 1000000;
int s[MAXV + 1], c[MAXV + 1];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    for(int i = 1; i <= MAXV; i++)
        for(int j = i; j <= MAXV; j += i) s[j] += i;
    for(int j = 1; j <= MAXV; j++)
        c[j] = c[j - 1] + (s[j] > 2 * j ? 1 : 0);
    int q;
    cin >> q;
    while(q--){
        int l, r;
        cin >> l >> r;
        cout << c[r] - c[l - 1] << "\n";
    }
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
q = int(data[0])
qs = [(int(data[1 + 2 * k]), int(data[2 + 2 * k])) for k in range(q)]
M = max(r for _, r in qs)
s = [0] * (M + 1)
for i in range(1, M + 1):
    for j in range(i, M + 1, i):
        s[j] += i
c = [0] * (M + 1)
for j in range(1, M + 1):
    c[j] = c[j - 1] + (s[j] > 2 * j)
print('\n'.join(str(c[r] - c[l - 1]) for l, r in qs))
Bài 6Bỏ một sốTrung bình

Cho dãy $n$ số nguyên dương ($2 \le n \le 10^5$, $a_i \le 10^9$). Hãy xoá đúng một phần tử sao cho ƯCLN của $n - 1$ phần tử còn lại lớn nhất. In ra ƯCLN lớn nhất đó.

Input
4
12 15 18 7
Output
3

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Dựng pre[i] = ƯCLN tiền tố và suf[i] = ƯCLN hậu tố như bảng 5, với pre[0] = suf[n+1] = 0. Bỏ $a_i$ được $\gcd(\text{pre}[i-1], \text{suf}[i+1])$; lấy max theo $i$. Độ phức tạp $O(n \log)$. Mẫu: bỏ số 7 được $\gcd(12, 15, 18) = 3$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int a[100005], pre[100005], suf[100005];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i];
    pre[0] = 0; suf[n + 1] = 0;
    for(int i = 1; i <= n; i++) pre[i] = __gcd(pre[i - 1], a[i]);
    for(int i = n; i >= 1; i--) suf[i] = __gcd(suf[i + 1], a[i]);
    int best = 0;
    for(int i = 1; i <= n; i++)
        best = max(best, __gcd(pre[i - 1], suf[i + 1]));
    cout << best;
    return 0;
}
import sys
from math import gcd

data = sys.stdin.buffer.read().split()
n = int(data[0])
a = [0] + [int(v) for v in data[1:1 + n]] + [0]
pre = [0] * (n + 2)
suf = [0] * (n + 2)
for i in range(1, n + 1):
    pre[i] = gcd(pre[i - 1], a[i])
for i in range(n, 0, -1):
    suf[i] = gcd(suf[i + 1], a[i])
print(max(gcd(pre[i - 1], suf[i + 1]) for i in range(1, n + 1)))
Bài 7Ước của tíchTrung bình

Cho $n$ số nguyên dương ($1 \le n \le 10^5$, $a_i \le 10^6$). Gọi $P = a_1 \cdot a_2 \cdots a_n$. In số ước của $P$ sau khi chia lấy dư cho $10^9 + 7$.

Input
3
4 6 9
Output
16

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Không nhân $P$ ra. Sàng spf[x] = ước nguyên tố nhỏ nhất của $x$ (giống sàng Eratosthenes), rồi tách mỗi $a_i$ bằng cách chia liên tục cho spf, cộng số mũ vào e[p]. Đáp án là tích các $(e[p] + 1)$ lấy dư $10^9 + 7$ (phần 3). Mẫu: $P = 4 \cdot 6 \cdot 9 = 216 = 2^3 \cdot 3^3$, số ước $4 \cdot 4 = 16$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXV = 1000000;
const long long MOD = 1000000007;
int spf[MAXV + 1];
long long e[MAXV + 1];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    for(int i = 2; i <= MAXV; i++)
        if(spf[i] == 0)                        // i nguyên tố
            for(int j = i; j <= MAXV; j += i)
                if(spf[j] == 0) spf[j] = i;
    int n;
    cin >> n;
    for(int k = 0; k < n; k++){
        int x;
        cin >> x;
        while(x > 1){                          // tách x bằng spf: O(log x)
            int p = spf[x];
            while(x % p == 0){ x /= p; e[p]++; }
        }
    }
    long long ans = 1;
    for(int p = 2; p <= MAXV; p++)
        if(e[p]) ans = ans * ((e[p] + 1) % MOD) % MOD;
    cout << ans;
    return 0;
}
import sys
from math import isqrt

data = sys.stdin.buffer.read().split()
n = int(data[0])
a = [int(v) for v in data[1:1 + n]]
M = max(a)
MOD = 10 ** 9 + 7
spf = list(range(M + 1))                   # spf[x] = ước nguyên tố nhỏ nhất của x
for i in range(isqrt(M), 1, -1):           # i giảm dần: số nguyên tố nhỏ ghi đè sau cùng
    if all(i % p for p in range(2, isqrt(i) + 1)):
        spf[i * i::i] = [i] * len(range(i * i, M + 1, i))
e = {}
for x in a:
    while x > 1:
        p = spf[x]
        while x % p == 0:
            x //= p
            e[p] = e.get(p, 0) + 1
ans = 1
for v in e.values():
    ans = ans * (v + 1) % MOD
print(ans)
Bài 8Đoạn chia hết cho 3Khá

Cho xâu $s$ gồm toàn chữ số, độ dài không quá $10^5$. Đếm số cặp $(l, r)$ với $l \le r$ sao cho số tạo bởi các chữ số $s_l s_{l+1} \ldots s_r$ chia hết cho 3 (số có thể bắt đầu bằng chữ số 0, ví dụ “03” vẫn tính là số 3).

Input
1236
Output
6

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Theo dấu hiệu chia hết cho 3, đoạn $[l, r]$ chia hết cho 3 khi tổng chữ số của nó chia hết cho 3. Gọi $P_i$ là tổng chữ số tiền tố (bài 13); tổng đoạn là $P_r - P_{l-1}$, chia hết cho 3 khi $P_r \equiv P_{l-1} \pmod 3$. Vậy duyệt $r$ từ trái sang phải, giữ cnt[0..2] = số tiền tố đã gặp theo số dư (ban đầu cnt[0] = 1 cho tiền tố rỗng), cộng cnt[P_r % 3] vào đáp án. Mẫu có 6 đoạn: 3, 6, 12, 36, 123, 1236. Đáp án có thể tới $5 \cdot 10^9$ nên dùng long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    string s;
    cin >> s;
    long long cnt[3] = {1, 0, 0};       // tiền tố rỗng có tổng 0
    long long ans = 0;
    int p = 0;                          // tổng chữ số tiền tố, chỉ giữ số dư cho 3
    for(char c : s){
        p = (p + (c - '0')) % 3;
        ans += cnt[p];                  // ghép với mọi tiền tố cùng số dư
        cnt[p]++;
    }
    cout << ans;
    return 0;
}
s = input().strip()
cnt = [1, 0, 0]                    # tiền tố rỗng có tổng 0
ans = p = 0
for c in s:
    p = (p + ord(c) - 48) % 3
    ans += cnt[p]                  # ghép với mọi tiền tố cùng số dư
    cnt[p] += 1
print(ans)
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 😇
Bản quyền thuộc về Thầy Nguyễn Việt Nam Sơn. Bài viết, lời giải, hình minh hoạ và mã nguồn trên trang là công sức biên soạn của thầy qua nhiều năm giảng dạy. Vui lòng không sao chép, đăng lại hay sử dụng dưới mọi hình thức khi chưa được sự đồng ý của tác giả. Nếu thấy hữu ích, hãy chia sẻ bằng đường link bài gốc nhé. Cảm ơn bạn!
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
  • Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
  • Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
  • Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
  • Dễ nản giữa chừng khi không có ai theo sát.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
  • Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
  • Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
  • Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.

Nhận xét