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

Tổng bình phương chữ số

Tổng bình phương chữ số
Nguồn: CLUE OJ Độ khó: Trung bình map Tìm chu trình

Đề bài

Tổng bình phương chữ số [DIGITSQR]

Cho số nguyên dương $x$. Lập dãy vô hạn $A = (a_1, a_2, \ldots)$ theo quy tắc:

  • $a_1 = x$;
  • $a_i$ bằng tổng bình phương các chữ số của $a_{i-1}$ với mọi $i > 1$.

Hãy xác định số lần xuất hiện của một số nguyên $v$ trong $n$ phần tử đầu tiên của dãy $A$.

Dữ liệu:

  • Dòng đầu tiên chứa số nguyên $T$ $(1 \le T \le 30)$ là số tests.
  • Tiếp theo là $T$ dòng, mỗi dòng gồm ba số nguyên $x, v, n$ $(1 \le x, v, n \le 10^9)$.

Kết quả: Dòng $1 \ldots T$: dòng $i$ ghi số nguyên kết quả của test $i$.

Giới hạn:

  • 50% điểm đầu tiên với $n \le 10^5$;
  • 50% điểm còn lại không có giới hạn gì thêm.

Giải thích ví dụ: với $x = 13$: dãy là $13, 10, 1, 1, 1, \ldots$ ($1^2 + 3^2 = 10$, $1^2 + 0^2 = 1$, $1^2 = 1$). Trong 10 phần tử đầu, số $1$ xuất hiện $8$ lần. Với $x = 4$: dãy là $4, 16, 37, 58, 89, 145, \ldots$ – trong 6 phần tử đầu không có số $8$.

Ví dụ – input
2
13 1 10
4 8 6
Ví dụ – output
8
0

Lời mở đầu

Đọc đề thấy $n$ tới $10^9$, nếu sinh đủ $n$ phần tử thì chắc chắn chậm. Mấu chốt của bài là nhận ra dãy này rất nhanh rơi vào một vòng lặp (chu trình), và ta có thể dùng map để "nhớ" mỗi giá trị đã xuất hiện ở vị trí nào – gặp lại giá trị cũ là biết ngay chu trình bắt đầu ở đâu, dài bao nhiêu. Sau đó chỉ cần một phép chia là đếm xong.

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 chính thức có gì?

Bộ test có 100 test, test nào cũng có đủ $T = 30$ truy vấn:

TestSố truy vấn Tx lớn nhấtv lớn nhấtn lớn nhấtĐáp án = 0Đáp án lớn nhất
1–30309999300/90030
31–5030970.087.17928899.7980/60096.543
51–7030931.038.3184939.813.0900/6009.813.090
71–10030987.349.0995601.000.000.0000/9001.000.000.000

Đối chiếu giới hạn của đề với test thật và số test đúng của từng cách:

PhầnĐề ghiTestThực tếCách 1
(mô phỏng)
Cách 2
(map + chu trình)
50% đầu$n \le 10^5$1–50test 1–30: $x, n < 100$; test 31–50: $n < 10^5$50/5050/50
50% còn lạikhông giới hạn thêm51–70$n \approx 10^7$20/2020/20
71–100$n$ tới $10^9$0/3030/30

Một con số thú vị mình đo được: trên toàn bộ 3000 truy vấn, dãy chỉ cần sinh tối đa 20 giá trị khác nhau là đã gặp lại một giá trị cũ. Đó là lý do cách 2 chạy gần như tức thì.

Ý tưởngDãy luôn rơi vào chu trình

Với $x \le 10^9$ thì $x$ có tối đa 10 chữ số, nên $a_2 \le 10 \cdot 9^2 = 810$. Từ đó trở đi mọi phần tử đều $\le 810$ (một số có 3 chữ số cho tổng bình phương không quá $3 \cdot 81 = 243$). Dãy chỉ nhận hữu hạn giá trị và mỗi phần tử chỉ phụ thuộc phần tử ngay trước nó, nên sớm muộn sẽ gặp lại một giá trị cũ – từ đó dãy lặp lại y hệt mãi mãi.

Ví dụ $x = 4$: $\underbrace{4 \to 16 \to 37 \to 58 \to 89 \to 145 \to 42 \to 20}_{\text{chu trình dài 8}} \to 4 \to \ldots$. Còn $x = 13$: $13 \to 10 \to \underbrace{1}_{\text{chu trình dài 1}} \to 1 \to \ldots$ ($13, 10$ là phần "đuôi" trước chu trình).

Thuật toán
Sinh lần lượt $a_1, a_2, \ldots$ và lưu pos[giá trị] = vị trí đầu tiên trong một map. Khi sinh ra giá trị $a$ đã có trong map ở vị trí $j$: chu trình bắt đầu ở $st$ = pos[a], dài $c = j - st$. Nếu $v$ không có trong map thì đáp án 0. Ngược lại gọi $k$ = pos[v]:
  • $k > n$: $v$ chưa kịp xuất hiện → 0.
  • $k < st$: $v$ nằm ở phần đuôi, xuất hiện đúng 1 lần.
  • $st \le k \le n$: $v$ xuất hiện ở các vị trí $k, k + c, k + 2c, \ldots \le n$ → $\lfloor (n - k) / c \rfloor + 1$ lần.

Em nhập $x, v, n$ rồi bấm Tiếp để xem dãy được sinh ra, map được điền dần và chu trình hiện ra:

Ví dụ nhanh:

Cách 1Mô phỏng đủ n phần tử – 50% điểm

Làm đúng như đề: sinh lần lượt $n$ phần tử, gặp $v$ thì đếm. Hàm f(x) tách từng chữ số bằng x % 10 và x /= 10.

C++▶ Chạy tay từng dòng codeCách 1: sinh lần lượt n phần tử của dãy, gặp v thì đếm.
Dữ liệu mẫu:
Mỗi lần gọi f, khung sẽ đi vào bên trong hàm để tách từng chữ số.
Không gian$O(1)$
Thời gian$O(T \cdot n \cdot \log_{10} x)$

Kết quả chạy thật: bản C++ đúng 70/100 test (quá thời gian ở test 71–100), test đúng chạy lâu nhất 0.88s; bản Python (chỉ chạy test 1–60) đúng 50/60 test (quá thời gian ở test 51–60), test đúng lâu nhất 0.49s. Ăn trọn 50% điểm đầu ($n \le 10^5$), và bất ngờ là còn "lì" qua được cả 20 test 51–70 ($n \approx 10^7$): 30 truy vấn là khoảng $3 \cdot 10^8$ lần gọi f, nhưng sau phần tử thứ hai các số đều nhỏ (tối đa 3 chữ số) nên mỗi lần gọi rất nhẹ – dù vậy nhiều test mất 0.6–0.9 giây, rất sát giới hạn. Với $n$ tới $10^9$ (test 71–100) thì không thể kịp.

Cách 2map nhớ vị trí, tìm chu trình – Full điểm

C++▶ Chạy tay từng dòng codeCách 2: map pos nhớ vị trí đầu tiên của mỗi giá trị, gặp lại là biết chu trình, rồi đếm bằng phép chia.
Dữ liệu mẫu:
Bấm ⏭ hoặc chọn tốc độ nhanh để bỏ qua các bước tách chữ số trong hàm f.
  1. while (pos.find(a) == pos.end()): còn chưa gặp lại giá trị cũ thì ghi vị trí của $a$ rồi sinh tiếp. Vòng này chỉ chạy vài chục lần.
  2. Khi thoát vòng, $a$ chính là giá trị bị lặp, $j$ là vị trí mà nó xuất hiện lần thứ hai, nên chu trình bắt đầu ở st = pos[a] và dài c = j - st.
  3. Mọi giá trị trong map đều khác nhau, nên $v$ (nếu có) chỉ có một vị trí đầu tiên $k$. Ba trường hợp đúng như phần ý tưởng.
  4. Kiểu long long cho $n, k$ không bắt buộc ($\le 10^9$ vừa int), mình dùng cho an toàn.
Không gian$O(K)$
Thời gian$O(T \cdot K \log K)$ với $K \le 20$

Kết quả chạy thật: bản C++ đúng 100/100 test, test đúng chạy lâu nhất 0.00s; bản Python đúng 100/100 test, test đúng lâu nhất 0.03s.

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

CáchNgôn ngữĐúngTest đúng lâu nhấtGhi chú
1. Mô phỏng $n$ phần tửC++70/1000.88sQuá thời gian test 71–100
Python50/600.49sChỉ chạy test 1–60; quá thời gian test 51–60
2. map tìm chu trìnhC++100/1000.00sFull điểm
Python100/1000.03sFull điểm (dict)
Ghi chú của mình: thời gian trong bảng là thời gian CPU đo trên máy của mình (giới hạn 1 giây). Mô phỏng thẳng chỉ được 50 điểm; dùng map để phát hiện chu trình thì mỗi truy vấn chỉ sinh không quá 20 phần tử, chạy tức thì với cả C++ lẫn Python.
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 😇

Nhận xét