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

Chữ số (Tin học trẻ toàn quốc 2023 – Vòng khu vực miền Trung – Bảng B, C2)

Chữ số (Tin học trẻ toàn quốc 2023 – Vòng khu vực miền Trung – Bảng B, C2)
Nguồn: Tin học trẻ 2023 Độ khó: Dễ Fibonacci Chu kỳ Pisano Lũy thừa ma trận

Đề bài

Nguồn đề: Hội thi Tin học trẻ toàn quốc 2023 – Vòng khu vực miền Trung – Bảng B, C2 (bài Chữ số). Năm 2023 vòng khu vực có hai bộ đề: miền Bắc và miền Nam dùng chung một đề, miền Trung dùng một đề riêng – bài này thuộc đề miền Trung. Đề bên dưới do mình viết lại bằng lời của mình cho dễ hiểu (giữ đúng nghĩa, đúng số liệu và ví dụ của đề gốc).

Cho hai số nguyên dương $a, b$. Dãy $fib$ được xây dựng giống dãy Fibonacci nhưng hai số đầu tùy ý:

  • $fib(1) = a$, $fib(2) = b$;
  • $fib(n)$ $= fib(n - 1) + fib(n - 2)$ với mọi $n \ge 3$.

Yêu cầu: cho $a, b, L, R$ ($a, b \le 10^{18}$, $L \le R \le 10^{18}$), tìm chữ số hàng đơn vị của tổng

$S = fib(L) + fib(L + 1)$ $+ \dots + fib(R)$.

Dữ liệu vào:

  • Dòng đầu: số nguyên dương $T$ ($T \le 100$) – số bộ dữ liệu.
  • $T$ dòng sau, mỗi dòng bốn số nguyên dương $a, b, L, R$.

Kết quả: $T$ dòng, mỗi dòng một chữ số – chữ số cuối của $S$ ứng với bộ dữ liệu đó.

Subtask:

  • Subtask 1 (40%): $R \le 10^6$;
  • Subtask 2 (30%): $R - L \le 10^6$;
  • Subtask 3 (30%): không có ràng buộc gì thêm.

Giới hạn thời gian: 1 giây. Dữ liệu nhập từ bàn phím, in ra màn hình.

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

Giải thích: với $a = b = 1$ thì $fib(1) = 1$, $fib(2) = 1$, $fib(3) = 2$. Bộ thứ nhất $S = 1 + 1 + 2 = 4$; bộ thứ hai chỉ có một số hạng $S = fib(3) = 2$.

Lưu ý: đề gốc ghi $L < R$ nhưng chính ví dụ lại có bộ $L = R = 3$, nên đề trên CLUE ghi $L \le R$ (các lời giải bên dưới đều xử lý được $L = R$).

Lời mở đầu

Đây là bài "dễ mà dễ sai" của đề miền Trung: số hạng của dãy lớn khủng khiếp (vượt xa long long chỉ sau vài chục số hạng), $R$ tới $10^{18}$ nên không thể cộng từng số. Mấu chốt là đề chỉ hỏi chữ số cuối, tức là chỉ cần làm việc với các số mod 10. Mình đi 3 bước: cộng thẳng (40 điểm) → nhảy nhanh tới $L$ bằng lũy thừa ma trận (70 điểm) → phát hiện dãy mod 10 tuần hoàn chu kỳ 60 (100 điểm).

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 trên CLUE do mình tự sinh theo đúng giới hạn và tỉ lệ điểm subtask của đề: 20 test, mỗi test có tới $T = 100$ bộ dữ liệu (test 1 là ví dụ của đề). Có đủ các trường hợp $a, b = 10^{18}$, $L = 1$, $R = 10^{18}$, $L, R$ rơi đúng bội của 60 hoặc lệch 1 so với bội của 60 (để bắt lỗi chỉ số).

NhómTestSố bộ $T$Tổng số bộ$R$ lớn nhất$R - L$ lớn nhất$a, b$ lớn nhất
Subtask 11–82, 5, 10, 1005171.000.000999.9991018
Subtask 29–144, 10050410181.000.0001018
Subtask 315–205, 1005051018≈ 1,00·10181018
Đã nộp thử trên bộ chấm chính thức
Lời giải cuối cùng (cách 3) mình đã nộp lên hệ thống chấm chính thức của Hội thi (tinhoctre.vn) và được 100/100 điểm (AC).

Kết quả chạy thật các cách bên dưới theo từng subtask (số test đúng / số test của subtask):

SubtaskĐiều kiệnTestCách 1
C++
Cách 1
Python
Cách 2
C++
Cách 2
Python
Cách 3
C++
Cách 3
Python
Subtask 1 (40%)$R \le 10^6$1–88/83/88/83/88/88/8
Subtask 2 (30%)$R - L \le 10^6$9–140/60/66/61/66/66/6
Subtask 3 (30%)không ràng buộc thêm15–200/60/60/60/66/66/6

Ý tưởngChỉ giữ chữ số cuối và dãy sẽ lặp lại

Nhận xét 1. Chữ số cuối của một tổng chỉ phụ thuộc vào chữ số cuối của các số hạng: $(x + y) \bmod 10$ $= \big((x \bmod 10) + (y \bmod 10)\big) \bmod 10$. Vậy thay vì $fib(n)$ khổng lồ, ta chỉ cần $f(n) = fib(n) \bmod 10$, và $f(n) = (f(n-1) + f(n-2)) \bmod 10$ – các số luôn từ 0 đến 9.

Nhận xét 2 (tuần hoàn). Số hạng tiếp theo chỉ phụ thuộc vào cặp hai số hạng liền trước $(f(n-1), f(n))$. Cặp này chỉ có $10 \times 10 = 100$ khả năng, nên chắc chắn có lúc cặp bị lặp lại, và từ đó dãy lặp lại y hệt. Hơn nữa dãy còn "chạy lùi" được ($f(n-2) = f(n) - f(n-1)$) nên phần lặp bắt đầu ngay từ đầu dãy. Người ta đã biết dãy Fibonacci mod 10 có chu kỳ 60 (gọi là chu kỳ Pisano), và điều này đúng với mọi cặp $a, b$ ban đầu vì $fib(n)$ luôn là tổ hợp $x \cdot a + y \cdot b$ của hai dãy Fibonacci chuẩn.

Nhận xét 3. Gọi $P(n) = f(1) + f(2) + \dots + f(n)$ (mod 10). Khi đó đáp án là $(P(R) - P(L - 1)) \bmod 10$, và nhờ chu kỳ 60:

$P(n) = \lfloor n / 60 \rfloor \cdot P(60)$ $+ P(n \bmod 60) \pmod{10}$.

Em nhập $a, b, L, R$ rồi bấm Tiếp để xem dãy $f$ được lập, chu kỳ 60 hiện ra, và cách tính đáp án chỉ bằng vài phép tra bảng:

Ví dụ nhanh:

Một điều thú vị em sẽ thấy trong widget: $P(60)$ luôn bằng 0 (tổng một chu kỳ chia hết cho 10). Lý do: với dãy kiểu Fibonacci có $fib(1) + \dots + fib(n)$ $= fib(n + 2) - fib(2)$, nên $P(60)$ $= f(62) - f(2)$ $= f(2) - f(2)$ $= 0$. Dù vậy lời giải cuối mình vẫn viết đủ công thức cho dễ hiểu.

Cách 1Cộng thẳng từ 1 tới R (Subtask 1)

Đi lần lượt $i = 1, 2, \dots, R$, giữ hai biến x = f(i), y = f(i + 1) (đã mod 10). Gặp $i \ge L$ thì cộng x vào tổng. Chú ý a % 10 ngay từ đầu vì $a, b$ tới $10^{18}$.

C++▶ Chạy tay từng dòng codeCách 1: trượt cặp (x, y) = (f(i), f(i+1)) từ 1 tới R, cộng các số hạng từ L.
Dữ liệu mẫu:
  • Không cần mảng: mỗi bước chỉ "trượt" cặp $(x, y) \to (y, x + y)$.
  • Mọi phép cộng đều % 10 nên không bao giờ tràn số.
Không gian$O(1)$
Thời gian$O(T \cdot R)$

Kết quả chạy thật: Bản C++ đúng 8/20 test, quá thời gian test 9–20 (các test đúng chạy lâu nhất 0.189s) → 40 điểm. Bản Python đúng 3/20 test, quá thời gian test 4–20 (các test đúng chạy lâu nhất 0.212s) – Python cộng tới $10^8$ lần (100 bộ × $R = 10^6$) thì quá chậm, chỉ qua được 3 test nhỏ.

Cách 2Nhảy tới L bằng lũy thừa ma trận (Subtask 1, 2)

Ở subtask 2, $L$ rất lớn nhưng đoạn $[L, R]$ ngắn ($\le 10^6$ số). Phần tốn thời gian của cách 1 là "đi bộ" từ 1 tới $L$. Ta "nhảy" thẳng tới $L$ bằng công thức ma trận quen thuộc của Fibonacci:

$\begin{pmatrix} fib(i+1) \\ fib(i) \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}^{i-1} \begin{pmatrix} fib(2) \\ fib(1) \end{pmatrix}$

Lũy thừa ma trận $2 \times 2$ tính bằng lũy thừa nhanh trong $O(\log L)$ phép nhân (mọi phần tử mod 10). Có $f(L)$ và $f(L+1)$ rồi thì cộng tiếp từ $L$ tới $R$ như cách 1.

C++▶ Chạy tay từng dòng codeCách 2: lũy thừa nhanh ma trận [[1,1],[1,0]] (mod 10) để có ngay f(L), f(L+1), rồi cộng từ L tới R.
Dữ liệu mẫu:
  • luyThua(M, p): nếu bit thấp nhất của p là 1 thì nhân kết quả với M; sau đó bình phương M và dịch p sang phải.
  • Từ $P = M^{L-1}$: $f(L+1)$ $= P_{00} \cdot f(2) + P_{01} \cdot f(1)$ và $f(L) = P_{10} \cdot f(2) + P_{11} \cdot f(1)$.
Không gian$O(1)$
Thời gian$O(T \cdot (\log L + R - L))$

Kết quả chạy thật: Bản C++ đúng 14/20 test, quá thời gian test 15–20 (các test đúng chạy lâu nhất 0.243s) → 70 điểm. Subtask 3 có $R - L$ gần $10^{18}$ nên vòng cộng vẫn quá chậm. Bản Python đúng 4/20 test, quá thời gian test 4–8, 10–20 (các test đúng chạy lâu nhất 0.143s).

Cách 3Chu kỳ 60 – tổng tiền tố (full)

Làm đúng như phần ý tưởng: lập $f(1..60)$ và tổng tiền tố pre[0..60] (mod 10), rồi $P(n)$ = (số chu kỳ đầy đủ) × pre[60] + pre[n % 60].

C++▶ Chạy tay từng dòng codeCách 3: lập f[1..60] và tổng tiền tố pre (mod 10), mỗi P(n) chỉ là một phép tra bảng nhờ chu kỳ 60.
Dữ liệu mẫu:
  1. du % 10: số chu kỳ $\lfloor n / 60 \rfloor$ có thể tới $1{,}6 \cdot 10^{16}$; lấy mod 10 trước khi nhân cho chắc chắn không tràn.
  2. (P(R) - P(L - 1) + 10) % 10: hiệu có thể âm, cộng thêm 10 để về số không âm (trong C++, % của số âm cho kết quả âm).
  3. Khi $L = 1$ thì P(0) = pre[0] = 0 – đúng.
Không gian$O(60)$
Thời gian$O(T \cdot 60)$

Kết quả chạy thật: Bản C++ đúng 20/20 test, chạy lâu nhất 0.004s. Full điểm. Bản Python đúng 20/20 test, chạy lâu nhất 0.016s – Python cũng nhẹ nhàng.

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

CáchNgôn ngữĐúngLâu nhất
(test đúng)
Điểm theo subtask
1. Cộng thẳng từ 1 tới RC++8/200.189s40%
Python3/200.212s0%
2. Nhảy tới L bằng lũy thừa ma trận, cộng L..RC++14/200.243s70%
Python4/200.143s0%
3. Chu kỳ 60 (chu kỳ Pisano)C++20/200.004s100%
Python20/200.016s100%
Ghi chú của mình: thời gian là thời gian CPU đo trên máy mình (giới hạn 1 giây, các test quá giờ bị dừng ở khoảng 4 giây). Bài học rút ra: khi đề chỉ hỏi "chữ số cuối" hay "phần dư khi chia cho $m$" của một dãy truy hồi, hãy nghĩ ngay tới tính tuần hoàn – dãy truy hồi bậc 2 mod $m$ chỉ có $m^2$ trạng thái nên chắc chắn lặp. Với mod 10 chu kỳ là 60, mod 100 là 300, mod 1000 là 1500. Nếu không nhớ chu kỳ, em có thể tự tìm bằng cách chạy tới khi cặp $(f(n), f(n+1))$ quay lại $(f(1), f(2))$.
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 😇