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 đề cơ bản 4: Đếm và tìm quy luật trong C++ và Python: quy tắc cộng, quy tắc nhân, đếm bằng đóng góp, lập bảng đoán công thức, chu kỳ

Chuyên đề cơ bản 4: Đếm và tìm quy luật trong C++ và Python: quy tắc cộng, quy tắc nhân, đếm bằng đóng góp, lập bảng đoán công thức, chu kỳ
Series: Chuyên đề cơ bản Độ khó: Trung bình Chuyên đề cơ bản 4 / Thuật toán cơ bản Đếm Quy luật
// Chuyên đề cơ bản · Chuyên đề 4
Đếm và tìm quy luật: quy tắc cộng nhân, đếm bằng đóng góp, lập bảng, chu kỳ
Đếm hàng tỉ đoạn con mà không duyệt đoạn nào, tìm số hạng thứ $10^{18}$ của một dãy chỉ sau vài chục bước, đoán công thức từ một bảng số nhỏ: những kỹ năng biến bài “tưởng phải vét cạn” thành vài dòng code.

Lời mở đầu

Trong đề HSG có một nhóm bài rất đặc biệt: đề ngắn, dữ liệu vào chỉ một hai con số, nhưng $n$ lên tới $10^9$ hay $10^{18}$. Không có cấu trúc dữ liệu nào cứu được, chỉ có đếm khéo và tìm quy luật. Chuyên đề cuối của nhóm Thuật toán cơ bản gom lại những “mẹo nhà nghề” mà các bạn đội tuyển hay dùng: đếm phần bù, đếm bằng đóng góp, lập bảng vét cạn để đoán công thức, phát hiện chu kỳ và các công thức tổng quen thuộc.

Em sẽ dùng lại vòng lặp ở bài 4, ước lượng thời gian và bẫy tràn số ở bài 9, ƯCLN và chia lấy dư ở bài 12. Bao hàm – loại trừ đã có ở chuyên đề 6, cách viết vét cạn gọn và nhanh ở chuyên đề 10. Phần tổ hợp $C(n, k)$ và chia trong modulo để dành cho bài 26, ở đây mình chỉ dùng phép đếm “bằng tay”.

Trong bài này bạn sẽ học: quy tắc cộng, quy tắc nhân và mẹo đếm phần bù (“đếm cái không thỏa rồi trừ”); đếm bằng đóng góp: mỗi phần tử nằm trong $i \cdot (n - i + 1)$ đoạn con, tổng chênh lệch mọi cặp sau khi sắp xếp; quy trình lập bảng vét cạn, tính hiệu, đoán công thức rồi kiểm chứng; chu kỳ của chữ số tận cùng, của dãy lặp $x_{k+1} = f(x_k)$ và cách tìm chu kỳ bằng mảng đánh dấu để tính số hạng thứ $10^{18}$; các công thức cấp số cộng, cấp số nhân, tổng bình phương, đếm điểm nguyên và cách tránh tràn số. 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 1Quy tắc cộng, quy tắc nhân và đếm phần bù

Hai quy tắc dưới đây nghe rất “toán lớp 11”, nhưng hiểu chắc chúng thì em đếm được gần như mọi thứ trong đề HSG cơ bản:

  • Quy tắc cộng: công việc làm theo phương án A hoặc phương án B; A có $a$ cách, B có $b$ cách, và không cách nào thuộc cả hai. Khi đó có $a + b$ cách.
  • Quy tắc nhân: công việc gồm bước 1 rồi bước 2; bước 1 có $a$ cách, và ứng với mỗi cách đó bước 2 đều có $b$ cách. Khi đó có $a \cdot b$ cách.

Ví dụ: biển số gồm 2 chữ cái in hoa rồi 3 chữ số có $26 \cdot 26 \cdot 10 \cdot 10 \cdot 10 = 676\,000$ biển. Số có 3 chữ số đôi một khác nhau: chữ số hàng trăm có 9 cách (khác 0), hàng chục còn 9 cách (khác hàng trăm, được là 0), hàng đơn vị còn 8 cách, tổng $9 \cdot 9 \cdot 8 = 648$ số. Để ý: ở bước sau các chữ số được chọn thay đổi theo bước trước, nhưng số lượng cách thì không đổi, nên vẫn nhân được.

Đếm phần bù. Hỏi: có bao nhiêu số có $n$ chữ số chứa ít nhất một chữ số 7? Đếm trực tiếp phải chia trường hợp “có đúng một số 7 ở vị trí nào”, “có hai số 7”… rất rối. Đếm ngược lại dễ hơn nhiều: số có $n$ chữ số không có chữ số 7 nào thì chữ số đầu có 8 cách (bỏ 0 và 7), mỗi chữ số còn lại có 9 cách, tức $8 \cdot 9^{n-1}$ số. Lấy tất cả trừ đi:

$$\text{có ít nhất một số 7} = \underbrace{9 \cdot 10^{n-1}}_{\text{tất cả}} - \underbrace{8 \cdot 9^{n-1}}_{\text{không có số 7}}$$

nTất cảKhông có số 7Có ít nhất một số 7Vét cạn kiểm tra
19811 (chỉ số 7)
290721818
3900648252252
49000583231683168

Bảng 1. Đếm phần bù số có $n$ chữ số chứa chữ số 7. Cột cuối là kết quả của vòng for duyệt từng số, khớp hoàn toàn.

Cùng ý tưởng cho một bài rất hay gặp: cho dãy $n$ số, đếm số cặp $(i, j)$ với $i < j$ và $a_i \ne a_j$. Cặp “khác nhau” thì khó đếm, cặp “bằng nhau” lại rất dễ: nếu giá trị $v$ xuất hiện $c_v$ lần thì có $\frac{c_v(c_v - 1)}{2}$ cặp bằng $v$. Đáp án là $\frac{n(n-1)}{2} - \sum_v \frac{c_v(c_v-1)}{2}$. Với dãy 1 2 1 3 1: tất cả 10 cặp, số 1 xuất hiện 3 lần cho 3 cặp bằng nhau, đáp án 7. Đếm số lần xuất hiện bằng map như ở chuyên đề cơ bản 1:

Xem code C++ / Python: đếm cặp khác nhau bằng phần bù (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    map<long long, long long> cnt;
    for(int i = 0; i < n; i++){
        long long x;
        cin >> x;
        cnt[x]++;
    }
    long long total = 1LL * n * (n - 1) / 2;          // tất cả các cặp i < j
    long long same = 0;                               // các cặp KHÔNG thỏa: hai số bằng nhau
    for(auto &p : cnt) same += p.second * (p.second - 1) / 2;
    cout << total - same;                             // phần bù
    return 0;
}
import sys
from collections import Counter

data = sys.stdin.buffer.read().split()
n = int(data[0])
cnt = Counter(data[1:n + 1])
total = n * (n - 1) // 2                              # tất cả các cặp i < j
same = sum(c * (c - 1) // 2 for c in cnt.values())    # các cặp KHÔNG thỏa: hai số bằng nhau
print(total - same)                                   # phần bù
Khi nào nghĩ tới phần bù?
Khi đề có những chữ “ít nhất một”, “không phải tất cả”, “khác nhau”, “có trùng”. Phần bù của “ít nhất một” là “không có cái nào”, phần bù của “khác nhau” là “bằng nhau”, và phần bù gần như luôn dễ đếm hơn. Công thức chung: đáp án = tất cả − số cách KHÔNG thỏa.
Hai cái bẫy của quy tắc cộng và quy tắc nhân
1. Cộng khi hai nhóm chồng lên nhau. Số từ 1 tới 100 chia hết cho 2 hoặc 3 không phải $50 + 33$ vì các bội của 6 bị đếm hai lần; phải trừ đi phần chung (bao hàm – loại trừ, xem chuyên đề 6).
2. Nhân khi số cách của bước sau phụ thuộc bước trước. Ví dụ đếm số có 3 chữ số tăng dần: chọn chữ số đầu là 1 thì chữ số thứ hai có 8 cách, chọn 8 thì chỉ còn 1 cách. Lúc đó phải chia trường hợp (quy tắc cộng) hoặc dùng tổ hợp ở bài 26.

Phần 2Đếm bằng đóng góp: đổi vai “đối tượng” và “phần tử”

Bài toán: cho dãy $a_1, a_2, \ldots, a_n$, tính tổng các tổng của mọi đoạn con liên tiếp. Cách thẳng thắn là duyệt mọi đoạn $[l, r]$ rồi cộng tổng của nó (dùng cộng dồn ở bài 13 thì $O(n^2)$). Với $n = 2 \cdot 10^5$ có tới $2 \cdot 10^{10}$ đoạn, quá chậm.

Kỹ thuật đóng góp (contribution) đổi câu hỏi: thay vì “mỗi đoạn có tổng bao nhiêu”, ta hỏi “mỗi phần tử $a_i$ được cộng vào bao nhiêu đoạn?”. Đoạn $[l, r]$ chứa vị trí $i$ khi và chỉ khi $l \le i \le r$: đầu trái $l$ chọn trong $1..i$ ($i$ cách), đầu phải $r$ chọn trong $i..n$ ($n - i + 1$ cách), hai lựa chọn độc lập. Theo quy tắc nhân:

$$\text{số đoạn chứa } a_i = i \cdot (n - i + 1) \qquad\Rightarrow\qquad \sum_{\text{mọi đoạn}} \text{tổng đoạn} = \sum_{i=1}^{n} a_i \cdot i \cdot (n - i + 1)$$

a1a2a3a4a5[1, 3][1, 4][1, 5][2, 3][2, 4][2, 5][3, 3][3, 4][3, 5]l ∈ {1, 2, 3}r ∈ {3, 4, 5}3 · 3 = 9 đoạn

Hình 1. Dãy 5 phần tử: vị trí 3 nằm trong đúng $3 \cdot 3 = 9$ đoạn con. Màu của thanh cho biết đầu trái $l$, độ dài thanh cho biết đầu phải $r$.

Em bấm Tiếp để xem từng phần tử “đếm” số đoạn chứa nó, rồi thử dãy của riêng em. Bước cuối mô phỏng tự kiểm tra lại bằng vét cạn:

MÔ PHỎNGMỗi phần tử nằm trong bao nhiêu đoạn con?Nhập dãy (tối đa 8 số) rồi bấm Tiếp / Tự chạy. Đánh số vị trí từ 1.
phần tử đang xétcác cách chọn lcác cách chọn r / đã tính xongkhông được chọn
i$a_i$cách chọn lcách chọn rsố đoạn chứa $a_i$đóng góp
1314412
2−1236−6
3432624
424148
tổng38

Bảng 2. Dãy 3, −1, 4, 2 có 10 đoạn con, tổng các tổng là 38. Để ý số đoạn chứa $a_i$ đối xứng: phần tử ở giữa nằm trong nhiều đoạn nhất.

Xem code C++ / Python: tổng các tổng của mọi đoạn con bằng đóng góp (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    long long total = 0;
    for(long long i = 1; i <= n; i++){
        long long x;
        cin >> x;
        total += x * i * (n - i + 1);     // a[i] nằm trong i * (n - i + 1) đoạn
    }
    cout << total;
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n = int(data[0])
total = 0
for i in range(1, n + 1):
    total += int(data[i]) * i * (n - i + 1)   # a[i] nằm trong i * (n - i + 1) đoạn
print(total)

Chạy tay từng dòng để thấy chương trình không hề “nhìn” thấy đoạn con nào. Code dưới đánh số từ 0 nên công thức thành $(i + 1)(n - i)$:

C++▶ Chạy tay từng dòng codeMỗi phần tử a[i] được cộng vào (i + 1)(n − i) đoạn con: cộng thẳng đóng góp của nó, không liệt kê đoạn nào.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 int n;
6 cin >> n;
7 long long total = 0;
8 for(int i = 0; i < n; i++){
9 long long x;
10 cin >> x;
11 long long cnt = 1LL * (i + 1) * (n - i);
12 total += x * cnt;
13 }
14 cout << total;
15 return 0;
16}

Tổng chênh lệch của mọi cặp. Tính $\sum_{i < j} |a_i - a_j|$. Dấu giá trị tuyệt đối làm khó, nhưng nếu dãy đã sắp xếp tăng thì với $i < j$ luôn có $|a_i - a_j| = a_j - a_i$. Xét phần tử ở vị trí $i$ (đánh số từ 0) sau khi sắp xếp: nó là số lớn hơn trong $i$ cặp với các số đứng trước (được cộng $i$ lần) và là số nhỏ hơn trong $n - 1 - i$ cặp với các số đứng sau (bị trừ $n - 1 - i$ lần). Vậy:

$$\sum_{i<j} |a_i - a_j| = \sum_{i=0}^{n-1} a_i \cdot \big(i - (n - 1 - i)\big) = \sum_{i=0}^{n-1} a_i \cdot (2i - n + 1) \quad (\text{dãy đã sắp xếp})$$

Ví dụ dãy 3 1 4 2, sắp xếp thành 1 2 3 4: $1 \cdot (-3) + 2 \cdot (-1) + 3 \cdot 1 + 4 \cdot 3 = 10$, đúng bằng $1 + 2 + 3 + 1 + 2 + 1$ của 6 cặp. Thời gian $O(n \log n)$ cho bước sắp xếp.

Xem code C++ / Python: tổng chênh lệch mọi cặp sau khi sắp xếp (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    sort(a.begin(), a.end());
    long long total = 0;
    for(long long i = 0; i < n; i++)
        total += a[i] * (2 * i - n + 1);   // lớn hơn i số đứng trước, nhỏ hơn n-1-i số đứng sau
    cout << total;
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n = int(data[0])
a = sorted(map(int, data[1:n + 1]))
total = 0
for i in range(n):
    total += a[i] * (2 * i - n + 1)    # lớn hơn i số đứng trước, nhỏ hơn n-1-i số đứng sau
print(total)
Khuôn mẫu đếm bằng đóng góp
Bước 1. Viết đáp án dưới dạng tổng trên các “đối tượng lớn” (đoạn, cặp, tập con…).
Bước 2. Đổi thứ tự lấy tổng: $\sum_{\text{đối tượng}} \sum_{\text{phần tử trong đó}} = \sum_{\text{phần tử}} (\text{giá trị}) \times (\text{số đối tượng chứa nó})$.
Bước 3. Đếm “số đối tượng chứa phần tử” bằng quy tắc nhân. Các kết quả hay dùng: số đoạn chứa vị trí $i$ là $i(n - i + 1)$; số tập con chứa một phần tử cố định là $2^{n-1}$; số cặp mà phần tử thứ $i$ (sau sắp xếp) là số lớn hơn là $i$.
Tràn số khi đếm đóng góp
Với $n = 2 \cdot 10^5$, riêng $i \cdot (n - i + 1)$ đã tới $10^{10}$, vượt int. Phải tính bằng long long và viết 1LL * i * (n - i + 1) (hoặc khai báo i là long long), nếu không phép nhân int * int tràn trước khi được gán vào biến long long. Trước khi nộp, em nhân thử giá trị lớn nhất của từng thừa số xem đáp án có vượt $9 \cdot 10^{18}$ không.
Đóng góp theo từng bit
Tính tổng $a_i \oplus a_j$ (XOR) của mọi cặp: xét riêng từng bit $k$. Nếu có $c$ số có bit $k$ bằng 1 thì bit $k$ xuất hiện trong $c \cdot (n - c)$ cặp (một số có bit 1, số kia có bit 0), đóng góp $c(n - c) \cdot 2^k$. Từ $O(n^2)$ xuống $O(30n)$. Thao tác bit em đã học ở bài 20.

Phần 3Lập bảng nhỏ bằng vét cạn để tìm quy luật

Nhiều bài “tìm quy luật” có đáp án là một công thức đơn giản, nhưng nhìn đề thì không thể đoán ra ngay. Cách làm của dân thi đấu là để máy tính làm phần khó:

Quy trình 4 bước tìm quy luật
1. Vét cạn: viết chương trình chậm nhưng chắc chắn đúng cho $n$ nhỏ (xem cách viết vét cạn ở chuyên đề 10).
2. In bảng: in $f(1), f(2), \ldots, f(20)$, có thể thêm cột hiệu liên tiếp, tỉ số, $n \bmod 2$, $n \bmod 4$…
3. Đoán: nhìn bảng, đối chiếu với các dạng quen thuộc (đa thức, lũy thừa, chu kỳ, Fibonacci…).
4. Kiểm chứng: cài công thức, so với vét cạn cho mọi $n$ tới vài trăm. Chỉ nộp khi khớp hết.

Ví dụ. Trên bàn cờ $n \times n$ ô có bao nhiêu hình vuông (mọi kích thước, cạnh nằm trên đường kẻ)? Với $n = 2$ có 4 hình vuông nhỏ và 1 hình vuông lớn, tổng 5. Vét cạn: duyệt cạnh $k$, rồi duyệt góc trên trái $(i, j)$ sao cho hình vuông không lòi ra ngoài. In bảng và tính hiệu liên tiếp $\Delta_1(n) = f(n) - f(n-1)$, rồi hiệu của hiệu:

nf(n)$\Delta_1$$\Delta_2$$\Delta_3$
11
254
31495
4301672
5552592
69136112
714049132
820464152

Bảng 3. Số hình vuông trên bàn cờ $n \times n$. Hiệu bậc 1 là 4, 9, 16, 25… (số chính phương!), hiệu bậc 3 không đổi bằng 2.

Đọc bảng: hiệu bậc 1 là $n^2$, tức là $f(n) = f(n - 1) + n^2$, suy ra $f(n) = 1^2 + 2^2 + \cdots + n^2$. Hiệu bậc 3 là hằng số nên $f$ là đa thức bậc 3, khớp với công thức quen thuộc $f(n) = \frac{n(n+1)(2n+1)}{6}$. Lý giải lại cho chắc: hình vuông cạnh $k$ có $(n - k + 1)^2$ vị trí đặt góc, cộng lại theo $k$ đúng là tổng các bình phương. Chương trình dưới in bảng so sánh vét cạn với công thức, có dòng nào lệch sẽ báo ngay:

Xem code C++ / Python: in bảng vét cạn và so với công thức đoán được (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

long long brute(int n){                       // vét cạn: chắc chắn đúng, chậm cũng được
    long long cnt = 0;
    for(int k = 1; k <= n; k++)               // cạnh hình vuông
        for(int i = 0; i + k <= n; i++)       // hàng của góc trên trái
            for(int j = 0; j + k <= n; j++)   // cột của góc trên trái
                cnt++;
    return cnt;
}

long long formula(long long n){               // công thức đoán được
    return n * (n + 1) * (2 * n + 1) / 6;
}

int main(){
    int N;
    cin >> N;
    for(int n = 1; n <= N; n++){
        cout << n << " " << brute(n) << " " << formula(n);
        if(brute(n) != formula(n)) cout << "  <-- SAI";
        cout << "\n";
    }
    return 0;
}
def brute(n):                       # vét cạn: chắc chắn đúng, chậm cũng được
    cnt = 0
    for k in range(1, n + 1):       # cạnh hình vuông
        for i in range(n - k + 1):  # hàng của góc trên trái
            for j in range(n - k + 1):
                cnt += 1
    return cnt

def formula(n):                     # công thức đoán được
    return n * (n + 1) * (2 * n + 1) // 6

N = int(input())
for n in range(1, N + 1):
    line = f"{n} {brute(n)} {formula(n)}"
    if brute(n) != formula(n):
        line += "  <-- SAI"
    print(line)

Mô phỏng dưới đây làm đúng 4 bước trên cho ba bài khác nhau. Bài “XOR 1..n” không phải đa thức: hiệu lộn xộn, nhưng thêm cột $n \bmod 4$ thì quy luật hiện ra ngay. Em đổi bài và đổi $N$ để tự quan sát:

MÔ PHỎNGLập bảng vét cạn, tìm hiệu, đoán công thức rồi kiểm chứngChọn bài toán và N (5 … 12), bấm Tiếp để xem từng dòng của bảng hiện ra.
dòng vừa tínhcột đang xétkhớp với vét cạn
Dấu hiệu trong bảngDạng quy luậtVí dụ
Hiệu bậc $k$ không đổiđa thức bậc $k$ của $n$$\Delta_2$ hằng số → $an^2 + bn + c$, giải hệ từ 3 dòng đầu
Tỉ số $f(n)/f(n-1)$ không đổilũy thừa (cấp số nhân)1, 3, 9, 27 → $3^{n-1}$
Mỗi số bằng tổng hai số trướcFibonaccisố cách lát hành lang $2 \times n$ bằng gạch $1 \times 2$
Lặp lại sau $p$ dòngchu kỳ, dùng $n \bmod p$XOR $1..n$, chữ số tận cùng của $2^n$
Khác nhau theo chẵn lẻchia hai trường hợp$n$ chẵn: $n/2$, $n$ lẻ: $0$

Bảng 4. Những dạng quy luật hay gặp nhất khi nhìn bảng số.

Đoán xong phải kiểm chứng: dãy 1, 2, 4, 8, 16, …, 31!
Đặt $n$ điểm trên đường tròn rồi nối mọi cặp điểm, các dây cung chia hình tròn thành tối đa bao nhiêu miền? Với $n = 1, 2, 3, 4, 5$ đáp án là 1, 2, 4, 8, 16, ai nhìn cũng đoán $2^{n-1}$. Nhưng $n = 6$ cho 31 chứ không phải 32 (công thức đúng là $C(n, 4) + C(n, 2) + 1$). Bài học: quy luật đoán từ 5 dòng có thể sai, hãy so với vét cạn cho thật nhiều $n$, và nếu được, tìm một lời giải thích như ở ví dụ hình vuông.

Phần 4Chu kỳ: khi dãy số lặp lại

Chữ số tận cùng của $a^n$. Chữ số tận cùng của tích chỉ phụ thuộc chữ số tận cùng của các thừa số, nên chữ số tận cùng của $2^n$ là: 2, 4, 8, 6, 2, 4, 8, 6, … lặp lại sau 4 bước. Mọi chữ số đều có chu kỳ 1, 2 hoặc 4:

Chữ số cuối của aChữ số cuối của $a^1, a^2, a^3, a^4, a^5$Chu kỳ
00, 0, 0, 0, 0, …1
11, 1, 1, 1, 1, …1
22, 4, 8, 6, 2, …4
33, 9, 7, 1, 3, …4
44, 6, 4, 6, 4, …2
55, 5, 5, 5, 5, …1
66, 6, 6, 6, 6, …1
77, 9, 3, 1, 7, …4
88, 4, 2, 6, 8, …4
99, 1, 9, 1, 9, …2

Bảng 5. Chữ số tận cùng của lũy thừa. Chu kỳ luôn chia hết 4, nên chữ số cuối của $a^n$ (với $n \ge 1$) bằng chữ số cuối của $a^{((n-1) \bmod 4) + 1}$.

Vì thế dù $n = 10^{18}$, chữ số tận cùng của $7^n$ chỉ cần $(n - 1) \bmod 4 + 1 = 4$, tức là bằng chữ số cuối của $7^4 = 2401$, là 1. Nhớ trường hợp đặc biệt $n = 0$: $a^0 = 1$.

Vì sao dãy nào cũng phải lặp? Xét dãy $x_{k+1} = f(x_k)$ với mọi giá trị nằm trong $0..m-1$ (ví dụ $f(x) = (x^2 + c) \bmod m$). Chỉ có $m$ giá trị khác nhau, nên trong $m + 1$ số hạng đầu chắc chắn có hai số bằng nhau (nguyên lý Dirichlet: $m + 1$ chú chim, $m$ cái chuồng). Và vì số hạng sau chỉ phụ thuộc số hạng trước, một khi $x_t = x_\mu$ thì $x_{t+1} = x_{\mu+1}$, $x_{t+2} = x_{\mu+2}$… dãy đi lại đúng con đường cũ mãi mãi. Hình dạng của dãy giống chữ ρ: một đuôi dài $\mu$ rồi một vòng dài $\lambda = t - \mu$.

3x00x1, x71x22x35x46x57x6đuôi: μ = 1chu kỳ: λ = 6x(k+1) = (x(k)² + 1) mod 10x0 = 33 0 1 2 5 6 7 0 1 …

Hình 2. Dãy $x_{k+1} = (x_k^2 + 1) \bmod 10$ từ $x_0 = 3$: số 3 chỉ xuất hiện một lần (đuôi), sau đó 0, 1, 2, 5, 6, 7 lặp mãi (chu kỳ dài 6).

Tìm chu kỳ bằng mảng đánh dấu. Dùng mảng first[v] = bước đầu tiên gặp giá trị $v$ (khởi tạo $-1$). Sinh lần lượt $x_0, x_1, \ldots$; với mỗi $x_t$:

  • nếu first[x_t] = -1: giá trị mới, ghi first[x_t] = t và lưu $x_t$ vào mảng xs;
  • nếu đã có first[x_t] = μ: tìm được chu kỳ, $\lambda = t - \mu$. Khi đó số hạng thứ $N \ge \mu$ là $x_{\mu + (N - \mu) \bmod \lambda}$, còn $N < t$ thì có sẵn trong xs.

Chỉ sinh tối đa $m + 1$ số hạng, bộ nhớ $O(m)$, rồi trả lời cho $N$ tới $10^{18}$ bằng một phép chia lấy dư. Em xem mảng first được lấp dần và đúng lúc gặp lại thì chu kỳ hiện ra:

MÔ PHỎNGPhát hiện chu kỳ bằng mảng đánh dấu first[]Dãy x(k+1) = (x(k)² + c) mod m. Nhập x0, c, m (≤ 30) và N rồi bấm Tiếp.
số vừa sinh / phần đuôichu kỳgiá trị lặp lạixNchưa gặp
C++▶ Chạy tay từng dòng codeSinh x = (x² + c) % m, đánh dấu first[x]; gặp giá trị đã đánh dấu thì có mu, lam và nhảy thẳng tới x_N.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int first[100];
5long long xs[100];
6
7int main(){
8 long long x, c, m, N;
9 cin >> x >> c >> m >> N;
10 for(int v = 0; v < m; v++) first[v] = -1;
11 for(int t = 0; ; t++){
12 if(first[x] != -1){
13 long long mu = first[x], lam = t - mu;
14 long long idx = (N < t) ? N : mu + (N - mu) % lam;
15 cout << xs[idx];
16 return 0;
17 }
18 first[x] = t; xs[t] = x;
19 x = (x * x + c) % m;
20 }
21}
Xem code C++ / Python: tìm chu kỳ bằng mảng đánh dấu, tính số hạng thứ N (N tới 10^18) (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long x, c, m, N;
    cin >> x >> c >> m >> N;
    vector<int> first(m, -1);              // first[v]: bước đầu tiên gặp giá trị v
    vector<long long> xs;                  // các số hạng đã sinh
    for(int t = 0; ; t++){
        if(first[x] != -1){                // gặp lại: chu kỳ bắt đầu từ first[x]
            long long mu = first[x], lam = t - mu;
            long long idx = (N < t) ? N : mu + (N - mu) % lam;
            cout << xs[idx];
            return 0;
        }
        first[x] = t;
        xs.push_back(x);
        x = (x * x + c) % m;               // sinh số hạng tiếp theo
    }
}
x, c, m, N = map(int, input().split())
first = [-1] * m                 # first[v]: bước đầu tiên gặp giá trị v
xs = []                          # các số hạng đã sinh
t = 0
while first[x] == -1:
    first[x] = t
    xs.append(x)
    x = (x * x + c) % m          # sinh số hạng tiếp theo
    t += 1
mu = first[x]                    # gặp lại: chu kỳ bắt đầu từ first[x]
lam = t - mu
idx = N if N < t else mu + (N - mu) % lam
print(xs[idx])
Chu kỳ không phải lúc nào cũng bắt đầu từ đầu
Dãy $2^n \bmod 12$ là 1, 2, 4, 8, 4, 8, 4, 8, …: hai số đầu 1, 2 là đuôi, chỉ 4, 8 mới lặp. Nếu em vội lấy $n \bmod$ (độ dài chu kỳ) tính từ đầu dãy là sai. Mảng first trả lời đúng cả hai câu hỏi “bắt đầu lặp từ đâu” ($\mu$) và “lặp sau bao lâu” ($\lambda$). Cũng nhớ kiểm tra riêng các trường hợp $N = 0$ và $N < \mu$.
Trạng thái có thể là một cặp số
Dãy Fibonacci theo modulo $m$: số hạng sau phụ thuộc hai số hạng trước, nên “trạng thái” là cặp $(F_k, F_{k+1})$, có $m^2$ khả năng. Đánh dấu bằng mảng hai chiều first[a][b] hoặc map. Tương tự với robot trên lưới (trạng thái = ô + hướng) hay lịch (chu kỳ 400 năm ở chuyên đề 4). Bài tập 6 và 8 luyện đúng kỹ thuật này.

Phần 5Những công thức hay dùng và bẫy tràn số

Đại lượngCông thứcGhi chú
$1 + 2 + \cdots + n$$\frac{n(n+1)}{2}$cũng là số đoạn con của dãy $n$ phần tử
$1^2 + 2^2 + \cdots + n^2$$\frac{n(n+1)(2n+1)}{6}$số hình vuông trên bàn cờ $n \times n$
$1^3 + 2^3 + \cdots + n^3$$\left(\frac{n(n+1)}{2}\right)^2$số hình chữ nhật trên bàn cờ $n \times n$
Số cặp $i < j$ trong $n$ phần tử$\frac{n(n-1)}{2}$số cái bắt tay, số đoạn thẳng nối $n$ điểm
Cấp số cộng: số số hạng từ $u$ tới $v$, công sai $d$$\frac{v - u}{d} + 1$tổng = (đầu + cuối) × số số hạng / 2
Cấp số nhân: $1 + q + \cdots + q^{n-1}$$\frac{q^n - 1}{q - 1}$$q \ne 1$; có modulo thì xem bài 26
Số điểm nguyên trên đoạn nối $(x_1, y_1)$, $(x_2, y_2)$$\gcd(|x_2 - x_1|, |y_2 - y_1|) + 1$tính cả hai đầu mút

Bảng 6. Bảng công thức nên thuộc. Mọi công thức đều kiểm chứng được bằng một vòng for nhỏ, đúng như quy trình ở phần 3.

Cấp số cộng trong đề thi. Tính tổng các số chia hết cho $k$ trong đoạn $[L, R]$ (với $L, R \ge 1$): số đầu tiên là $u = \lceil L / k \rceil \cdot k$, số cuối là $v = \lfloor R / k \rfloor \cdot k$; nếu $u \le v$ thì có $c = (v - u)/k + 1$ số, tổng là $(u + v) \cdot c / 2$. Ví dụ $k = 3$, đoạn $[10, 25]$: $u = 12$, $v = 24$, $c = 5$, tổng $= 36 \cdot 5 / 2 = 90$ (12 + 15 + 18 + 21 + 24). Đếm bội trong đoạn đã học kỹ ở chuyên đề 6.

Điểm nguyên trên đoạn thẳng. Đoạn nối $(0, 0)$ với $(6, 4)$: bước nhỏ nhất để từ một điểm nguyên sang điểm nguyên kế tiếp là $(6/g, 4/g)$ với $g = \gcd(6, 4) = 2$, tức $(3, 2)$. Có $g$ bước nên có $g + 1 = 3$ điểm nguyên: $(0, 0)$, $(3, 2)$, $(6, 4)$. ƯCLN bằng thuật toán Euclid ở bài 12.

Bẫy tràn số. Công thức thì ngắn nhưng các thừa số rất to. $\frac{n(n+1)}{2}$ với int n = 100000 đã tràn ($n(n+1) \approx 10^{10}$). $n(n+1)(2n+1)$ với $n = 3 \cdot 10^6$ khoảng $5{,}4 \cdot 10^{19}$, tràn cả long long dù kết quả cuối $\approx 9 \cdot 10^{18}$ vẫn vừa. Hai cách chữa:

  • Chia trước, nhân sau: trong $n$ và $n + 1$ có đúng một số chẵn, chia số đó cho 2 trước; trong $n$, $n + 1$, $2n + 1$ luôn có một số chia hết cho 3, chia số đó cho 3 trước. Sau đó các thừa số nhỏ hơn hẳn.
  • Lấy modulo từng thừa số khi đề yêu cầu in kết quả $\bmod (10^9 + 7)$, nhưng chỉ sau khi đã chia xong (không được chia cho 6 sau khi lấy modulo, phải dùng nghịch đảo ở bài 26).
Xem code C++ / Python: tổng bình phương 1..n theo modulo với n tới 10^18 (chia trước rồi mới nhân) (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1000000007;

int main(){
    long long n;
    cin >> n;
    long long a = n, b = n + 1, c = 2 * n + 1;   // 2n + 1 <= 2*10^18 + 1 vẫn vừa long long
    if(a % 2 == 0) a /= 2; else b /= 2;          // chia 2 TRƯỚC khi nhân
    if(a % 3 == 0) a /= 3;                       // một trong ba thừa số chia hết cho 3
    else if(b % 3 == 0) b /= 3;
    else c /= 3;
    cout << (a % MOD) * (b % MOD) % MOD * (c % MOD) % MOD;
    return 0;
}
MOD = 10**9 + 7
n = int(input())
# Python không tràn số, nhưng phải dùng // (chia nguyên), KHÔNG dùng /
print(n * (n + 1) * (2 * n + 1) // 6 % MOD)
Python không tràn số nhưng vẫn có bẫy
Viết n * (n + 1) / 2 trong Python cho ra số thực (float). Với $n = 10^{17}$, float chỉ giữ khoảng 16 chữ số có nghĩa nên kết quả bị làm tròn sai. Luôn dùng phép chia nguyên // và chia sau cùng khi tích chắc chắn chia hết. Trong C++, kiểm tra nhanh: giá trị lớn nhất của mỗi thừa số nhân với nhau có vượt $9{,}2 \cdot 10^{18}$ không? Nếu có, chia trước hoặc dùng __int128.
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 Tìm quy luật.

Bài tậpLuyện đếm và tìm quy luật

Bài 1Mật khẩu có chữ số 7Dễ

Mật khẩu là một xâu gồm đúng $n$ chữ số (được bắt đầu bằng 0). Đếm số mật khẩu chứa ít nhất một chữ số 7. Vì đáp án rất lớn, in phần dư khi chia cho $10^9 + 7$. Giới hạn: $1 \le n \le 10^6$.

Input
2
Output
19

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

💡 Gợi ý
Phần bù: có $10^n$ mật khẩu, trong đó $9^n$ mật khẩu không có số 7 nào. Đáp án $(10^n - 9^n) \bmod (10^9 + 7)$, tính hai lũy thừa bằng một vòng lặp nhân dần và lấy dư sau mỗi bước (Python có sẵn pow(10, n, MOD)). Cẩn thận: sau khi lấy dư, $10^n \bmod p$ có thể nhỏ hơn $9^n \bmod p$, nên phải cộng thêm $p$ trước khi lấy dư lần cuối. Với $n = 2$: $100 - 81 = 19$ (07, 17, …, 97, 70, 71, …, 79, trừ 77 bị đếm hai lần).
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1000000007;

int main(){
    int n;
    cin >> n;
    long long p10 = 1, p9 = 1;
    for(int i = 0; i < n; i++){
        p10 = p10 * 10 % MOD;
        p9 = p9 * 9 % MOD;
    }
    cout << ((p10 - p9) % MOD + MOD) % MOD;   // tất cả - không có số 7
    return 0;
}
MOD = 10**9 + 7
n = int(input())
p10 = pow(10, n, MOD)          # hàm pow có sẵn: 10^n mod MOD
p9 = pow(9, n, MOD)
print((p10 - p9) % MOD)        # tất cả - không có số 7 (Python tự cho số dư không âm)
Bài 2Tổng mọi đoạn conDễ

Cho dãy $n$ số nguyên $a_1, \ldots, a_n$ ($1 \le n \le 2 \cdot 10^5$, $|a_i| \le 1000$). Với mỗi đoạn con liên tiếp $[l, r]$ tính tổng các phần tử trong đoạn, rồi in tổng của tất cả các tổng đó.

Input
3
1 2 3
Output
20

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

💡 Gợi ý
Đúng phần 2: $a_i$ nằm trong $i \cdot (n - i + 1)$ đoạn. Với mẫu: $1 \cdot 3 + 2 \cdot 4 + 3 \cdot 3 = 20$, khớp với 6 đoạn có tổng 1, 2, 3, 3, 5, 6. Đáp án tới cỡ $1000 \cdot 10^{10} \cdot 2 \cdot 10^5 / 5 \approx 10^{18}$ nên phải dùng long long cho mọi phép nhân.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    long long total = 0;
    for(long long i = 1; i <= n; i++){
        long long x;
        cin >> x;
        total += x * i * (n - i + 1);     // a[i] nằm trong i * (n - i + 1) đoạn
    }
    cout << total;
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n = int(data[0])
total = 0
for i in range(1, n + 1):
    total += int(data[i]) * i * (n - i + 1)   # a[i] nằm trong i * (n - i + 1) đoạn
print(total)
Bài 3Chữ số tận cùngDễ

Có $T$ câu hỏi ($T \le 10^5$), mỗi câu cho hai số nguyên $a, n$ ($1 \le a \le 10^{18}$, $0 \le n \le 10^{18}$). In chữ số tận cùng của $a^n$, mỗi đáp án một dòng.

Input
4
2 10
7 3
12 0
1000000000000000000 1000000000000000000
Output
4
3
1
0

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

💡 Gợi ý
Chỉ chữ số cuối $d = a \bmod 10$ quan trọng, và theo Bảng 5 mọi chu kỳ đều chia hết 4. Nếu $n = 0$ thì đáp án là 1; ngược lại tính $e = (n - 1) \bmod 4 + 1$ (một số từ 1 đến 4) và in $d^e \bmod 10$. Mẫu: $2^{10}$ có $e = 2$, $2^2 = 4$; $7^3 = 343$; $12^0 = 1$; $a$ tận cùng 0 thì mọi lũy thừa dương tận cùng 0.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int T;
    cin >> T;
    while(T--){
        long long a, n;
        cin >> a >> n;
        if(n == 0){ cout << 1 << "\n"; continue; }
        int d = a % 10, e = (n - 1) % 4 + 1;     // chu kỳ chia hết 4
        int r = 1;
        for(int i = 0; i < e; i++) r = r * d % 10;
        cout << r << "\n";
    }
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
T = int(data[0])
out = []
for k in range(T):
    a, n = int(data[1 + 2 * k]), int(data[2 + 2 * k])
    if n == 0:
        out.append(1)
        continue
    e = (n - 1) % 4 + 1                     # chu kỳ chia hết 4
    out.append(pow(a % 10, e) % 10)
print('\n'.join(map(str, out)))
Bài 4Tổng khoảng cách ManhattanTrung bình

Cho $n$ điểm trên mặt phẳng ($2 \le n \le 2 \cdot 10^5$, toạ độ nguyên, $|x_i|, |y_i| \le 10^8$). Khoảng cách Manhattan giữa hai điểm là $|x_i - x_j| + |y_i - y_j|$. Tính tổng khoảng cách của mọi cặp điểm.

Input
3
0 0
1 2
3 1
Output
10

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

💡 Gợi ý
Tổng tách được thành hai phần độc lập: $\sum |x_i - x_j|$ và $\sum |y_i - y_j|$. Mỗi phần là bài “tổng chênh lệch mọi cặp” ở phần 2: sắp xếp riêng mảng $x$, riêng mảng $y$, rồi cộng $a_i(2i - n + 1)$. Mẫu: theo $x$: 0, 1, 3 cho $1 + 3 + 2 = 6$; theo $y$: 0, 1, 2 cho $2 + 1 + 1 = 4$; tổng 10. Đáp án tới cỡ $4 \cdot 10^{18}$, vẫn vừa long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

long long pairSum(vector<long long> a){          // tổng |a_i - a_j| mọi cặp
    sort(a.begin(), a.end());
    long long n = a.size(), s = 0;
    for(long long i = 0; i < n; i++) s += a[i] * (2 * i - n + 1);
    return s;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<long long> x(n), y(n);
    for(int i = 0; i < n; i++) cin >> x[i] >> y[i];
    cout << pairSum(x) + pairSum(y);             // hai trục độc lập
    return 0;
}
import sys

def pair_sum(a):                    # tổng |a_i - a_j| mọi cặp
    a.sort()
    n = len(a)
    return sum(v * (2 * i - n + 1) for i, v in enumerate(a))

data = sys.stdin.buffer.read().split()
n = int(data[0])
x = [int(data[1 + 2 * i]) for i in range(n)]
y = [int(data[2 + 2 * i]) for i in range(n)]
print(pair_sum(x) + pair_sum(y))    # hai trục độc lập
Bài 5Hình chữ nhật và hình vuôngTrung bình

Một tấm lưới gồm $n$ hàng, $m$ cột ô vuông đơn vị ($1 \le n, m \le 5 \cdot 10^4$). Đếm số hình chữ nhật (mọi kích thước, kể cả hình vuông) có cạnh nằm trên đường kẻ, và số hình vuông. In hai số trên một dòng.

Input
2 3
Output
18 8

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

💡 Gợi ý
Một hình chữ nhật được xác định bởi 2 đường kẻ ngang (trong $n + 1$ đường) và 2 đường kẻ dọc (trong $m + 1$ đường). Theo quy tắc nhân: $\frac{(n+1)n}{2} \cdot \frac{(m+1)m}{2}$. Hình vuông cạnh $k$ có $(n - k + 1)(m - k + 1)$ vị trí, cộng với $k = 1..\min(n, m)$. Mẫu: $3 \cdot 6 = 18$ hình chữ nhật; hình vuông: $2 \cdot 3 + 1 \cdot 2 = 8$. Em có thể lập bảng vét cạn để tự kiểm tra công thức.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long n, m;
    cin >> n >> m;
    long long rect = (n * (n + 1) / 2) * (m * (m + 1) / 2);   // chọn 2 đường ngang, 2 đường dọc
    long long sq = 0;
    for(long long k = 1; k <= min(n, m); k++) sq += (n - k + 1) * (m - k + 1);
    cout << rect << " " << sq;
    return 0;
}
n, m = map(int, input().split())
rect = (n * (n + 1) // 2) * (m * (m + 1) // 2)    # chọn 2 đường ngang, 2 đường dọc
sq = sum((n - k + 1) * (m - k + 1) for k in range(1, min(n, m) + 1))
print(rect, sq)
Bài 6Fibonacci theo moduloTrung bình

Dãy Fibonacci: $F_0 = 0$, $F_1 = 1$, $F_k = F_{k-1} + F_{k-2}$. Cho $n$ và $m$ ($0 \le n \le 10^{18}$, $2 \le m \le 1000$), in $F_n \bmod m$.

Input
100 10
Output
5

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

💡 Gợi ý
Trạng thái là cặp $(F_k \bmod m, F_{k+1} \bmod m)$, có không quá $m^2$ khả năng nên dãy phải lặp. Hơn nữa từ cặp sau suy ngược được cặp trước ($F_{k-1} = F_{k+1} - F_k$), nên chu kỳ không có đuôi: dãy luôn quay lại đúng cặp $(0, 1)$ ban đầu. Sinh tới khi gặp lại $(0, 1)$ được độ dài chu kỳ $p$ (người ta chứng minh $p \le 6m$), rồi tính $F_{n \bmod p}$. Với $m = 10$: $p = 60$, $100 \bmod 60 = 40$, $F_{40} = 102334155$ tận cùng 5.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long n, m;
    cin >> n >> m;
    long long a = 0, b = 1, p = 0;            // cặp (F_k, F_k+1) mod m
    do {
        long long c = (a + b) % m;
        a = b; b = c; p++;
    } while(!(a == 0 && b == 1));             // quay lại cặp đầu tiên -> chu kỳ p
    long long r = n % p;
    a = 0; b = 1;
    for(long long i = 0; i < r; i++){ long long c = (a + b) % m; a = b; b = c; }
    cout << a;
    return 0;
}
n, m = map(int, input().split())
a, b, p = 0, 1, 0                  # cặp (F_k, F_k+1) mod m
while True:
    a, b = b, (a + b) % m
    p += 1
    if a == 0 and b == 1:          # quay lại cặp đầu tiên -> chu kỳ p
        break
a, b = 0, 1
for _ in range(n % p):
    a, b = b, (a + b) % m
print(a)
Bài 7Điểm nguyên trong hình trònTrung bình

Cho số nguyên $r$ ($0 \le r \le 10^6$). Đếm số điểm $(x, y)$ có toạ độ nguyên nằm trong hoặc trên đường tròn tâm $O$ bán kính $r$, tức là $x^2 + y^2 \le r^2$.

Input
2
Output
13

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

💡 Gợi ý
Đếm theo từng cột $x$ từ $-r$ tới $r$: với mỗi $x$, $y$ chạy từ $-h$ tới $h$ với $h$ lớn nhất thỏa $h^2 \le r^2 - x^2$, cột đó có $2h + 1$ điểm. Khi $x$ tăng từ 0 lên $r$ thì $h$ chỉ giảm, nên giữ một biến $h$ và hạ dần (hai con trỏ), không cần căn bậc hai, tổng $O(r)$ (bản Python dùng math.isqrt cho nhanh). Nhờ đối xứng, cột $x$ và cột $-x$ giống nhau. Mẫu: cột $x = 0$ có 5 điểm, $x = \pm 1$ mỗi cột 3 điểm, $x = \pm 2$ mỗi cột 1 điểm, tổng 13. Dùng long long cho $r^2 = 10^{12}$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long r;
    cin >> r;
    long long h = r, total = 0;
    for(long long x = 0; x <= r; x++){
        while(x * x + h * h > r * r) h--;     // h chỉ giảm khi x tăng
        long long col = 2 * h + 1;            // số điểm trên cột x
        total += (x == 0) ? col : 2 * col;    // cột x và cột -x
    }
    cout << total;
    return 0;
}
from math import isqrt

r = int(input())
rr = r * r
# cột x có 2h + 1 điểm với h = isqrt(r² - x²); cột x và cột -x giống nhau
total = (2 * r + 1) + 2 * sum(2 * isqrt(rr - x * x) + 1 for x in range(1, r + 1))
print(total)
Bài 8Tổng dãy lặpKhá

Cho dãy $x_0$ cho trước, $x_{k+1} = (x_k^2 + c) \bmod m$ ($2 \le m \le 10^6$, $0 \le x_0 < m$, $0 \le c \le 10^9$). Tính $S = x_0 + x_1 + \cdots + x_{N-1}$ với $1 \le N \le 10^{12}$.

Input
3 1 10 10
Output
27

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

💡 Gợi ý
Tìm đuôi $\mu$ và chu kỳ $\lambda$ bằng mảng first như phần 4, đồng thời lưu tổng tiền tố $P_k = x_0 + \cdots + x_{k-1}$ của các số hạng đã sinh (gọi $t$ là số số hạng đã sinh). Nếu $N \le t$ thì đáp án là $P_N$. Ngược lại, lấy hết đuôi $P_\mu$, còn lại $N - \mu$ số hạng gồm $q = (N - \mu) / \lambda$ vòng trọn, mỗi vòng có tổng $P_t - P_\mu$, cộng phần lẻ $r = (N - \mu) \bmod \lambda$ số hạng đầu chu kỳ là $P_{\mu + r} - P_\mu$. Mẫu: dãy 3, 0, 1, 2, 5, 6, 7, 0, …: $\mu = 1$, $\lambda = 6$, tổng một vòng 21, $q = 1$, $r = 3$: $S = 3 + 21 + (0 + 1 + 2) = 27$. Chú ý $x^2$ tới $10^{12}$ và $c$ tới $10^9$: dùng long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long x, c, m, N;
    cin >> x >> c >> m >> N;
    vector<int> first(m, -1);
    vector<long long> P(1, 0);                 // P[k] = x0 + ... + x(k-1)
    long long t = 0;
    while(first[x] == -1){
        first[x] = t;
        P.push_back(P.back() + x);
        x = (x * x + c) % m;
        t++;
    }
    long long mu = first[x], lam = t - mu;
    if(N <= t){ cout << P[N]; return 0; }
    long long q = (N - mu) / lam, r = (N - mu) % lam;
    cout << P[mu] + q * (P[t] - P[mu]) + (P[mu + r] - P[mu]);   // đuôi + q vòng + phần lẻ
    return 0;
}
x, c, m, N = map(int, input().split())
first = [-1] * m
P = [0]                            # P[k] = x0 + ... + x(k-1)
t = 0
while first[x] == -1:
    first[x] = t
    P.append(P[-1] + x)
    x = (x * x + c) % m
    t += 1
mu = first[x]
lam = t - mu
if N <= t:
    print(P[N])
else:
    q, r = divmod(N - mu, lam)
    print(P[mu] + q * (P[t] - P[mu]) + (P[mu + r] - P[mu]))   # đuôi + q vòng + phần lẻ
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