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

Biến đổi (Tin học trẻ toàn quốc 2025 – Vòng loại quốc gia đợt 1 – Bảng B, C2)

Biến đổi
Nguồn: Tin học trẻ 2025 – Vòng loại QG đợt 1 – Bảng B, C2 Độ khó: Khá BFS Tìm kiếm sâu dần (IDDFS) Cắt tỉa

Đề bài

Cho dãy số nguyên không âm $a_1, a_2, \dots, a_n$ $(4 \le n \le 8;\ a_i \le 10^9)$. Ta muốn biến đổi để mọi phần tử đều bằng 0. Mỗi bước, được chọn 4 phần tử liên tiếp $a, b, c, d$ trong dãy và thay chúng (giữ nguyên vị trí) lần lượt bằng

$$|a - b|,\quad |b - c|,\quad |c - d|,\quad |d - a|.$$

Ví dụ với dãy $0, 1, 3, 5, 9$ có thể biến đổi như sau (ô vàng là 4 phần tử được chọn ở bước đó):

Ban đầu
01359
Bước 1
02248
Bước 2
00246
Bước 3
02226
Bước 4
00044
Bước 5
00404
Bước 6
04444
Bước 7
00000

Hãy tính số bước ít nhất để tất cả các phần tử đều bằng 0.

Dữ liệu vào: gồm $n$ dòng, mỗi dòng một số nguyên $a_i$ – đề không cho trước $n$, số phần tử chính là số dòng nên phải đọc tới hết dữ liệu.

Kết quả: một số nguyên là số bước biến đổi ít nhất.

Subtask:

  • Subtask 1 (30% số điểm): $n = 4$.
  • Subtask 2 (30% số điểm): $n \le 6$.
  • Subtask 3 (40% số điểm): không có ràng buộc gì thêm.

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

Nguồn: Hội thi Tin học trẻ toàn quốc 2025 – Vòng loại quốc gia đợt 1 – Bảng B, C2. Đề được mình viết lại cho dễ đọc.

Ví dụ – input
0
1
3
5
9
Ví dụ – output
7

Giải thích: dãy biến đổi ở trên dùng 7 bước và không có cách nào ít hơn. Lưu ý 4 phần tử phải liên tiếp và không được "vòng" từ cuối dãy về đầu dãy (nếu cho vòng thì ví dụ chỉ cần 5 bước).

Lời mở đầu

Bài này là một bài tìm kiếm trên không gian trạng thái: mỗi trạng thái là cả dãy số, mỗi bước có $n - 3$ lựa chọn (vị trí bắt đầu của 4 phần tử). Với $n = 4$ thì chỉ có đúng 1 lựa chọn nên chỉ cần mô phỏng – đây là dãy Ducci nổi tiếng, luôn về 0. Với $n$ lớn hơn ta cần tìm đường đi ngắn nhất: BFS là lựa chọn tự nhiên, nhưng không gian trạng thái nở rất nhanh; cách trọn điểm là tìm kiếm sâu dần (IDDFS) kèm vài nhát cắt tỉa khéo ở các bước cuối.

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 gồm 20 test (mình tự sinh theo đúng ràng buộc của đề). Test 1 là ví dụ ($n = 5$, thuộc subtask 2). Các test khó của subtask 2, 3 là những dãy mình tìm bằng leo đồi (thay đổi dần các số để số bước càng lớn càng tốt) – đáp án 11–13 bước. Lời giải cách 4 mình đã nộp trên bộ chấm chính thức tinhoctre.vn và được 100 điểm.

NhómTestGiá trị $n$$a_i$ lớn nhấtĐáp án
Subtask 12–741.000.000.0000 – 52
Subtask 21, 8–125, 61.000.000.0004 – 13
Subtask 313–207, 8869.563.9762 – 13
  • Test 2 và 7 là bộ "tribonacci" $n = 4$ cần tới 52 bước – mô phỏng vẫn tức thì.
  • Test 19 ($n = 8$, đáp án 13) là test "xấu" nhất mình tìm được – cũng là test quyết định cách nào trọn điểm.
  • Đề không có chứng minh chặn trên cho số bước khi $n \ge 5$; thực nghiệm (ngẫu nhiên + leo đồi) chưa thấy đáp án nào vượt 13.

Kết quả chạy thật của các cách bên dưới theo từng subtask:

SubtaskĐiều kiệnTestCách 1Cách 2Cách 3Cách 4
Subtask 1 (30%)$n = 4$2–76/66/66/66/6
Subtask 2 (30%)$n \le 6$1, 8–120/66/66/66/6
Subtask 3 (40%)không ràng buộc thêm ($n \le 8$)13–200/81/86/88/8

Ý tưởngĐường đi ngắn nhất trên đồ thị trạng thái

Xem mỗi dãy số là một đỉnh; từ một dãy, chọn cửa sổ 4 phần tử bắt đầu ở vị trí $w$ ($w = 1..n-3$) cho ta một cạnh tới dãy mới. Cần tìm đường đi ngắn nhất từ dãy ban đầu tới dãy toàn 0.

  • $n = 4$: mỗi đỉnh chỉ có 1 cạnh đi ra → đi thẳng một mạch (mô phỏng).
  • $n \ge 5$: mỗi đỉnh có $n - 3$ cạnh (2 tới 5), độ sâu đáp án tới 13 → cây tìm kiếm có cỡ $5^{13} \approx 10^9$ nút nếu duyệt thô. Phải cắt tỉa.

Em nhập dãy rồi bấm Tiếp để xem một cách biến đổi ít bước nhất (máy tìm bằng BFS) – ô vàng là cửa sổ được chọn ở bước đó:

Ví dụ nhanh:

Cách 1Subtask 1: n = 4 – mô phỏng

Khi $n = 4$ chỉ có đúng một cách chọn 4 phần tử liên tiếp (cả dãy), nên quá trình là duy nhất: cứ biến đổi tới khi dãy toàn 0 và đếm số bước. Người ta đã chứng minh dãy Ducci 4 phần tử luôn về 0, và với $a_i \le 10^9$ thì không quá vài chục bước.

C++▶ Chạy tay từng dòng codeCách 1: n = 4 – chỉ một cách biến đổi, mô phỏng tới khi toàn 0.
Dữ liệu mẫu:
  1. while(cin >> v) đọc tới hết dữ liệu vì đề không cho $n$.
  2. Phải tính đủ 4 giá trị mới b0..b3 rồi mới gán lại – nếu gán a[0] trước thì |d - a| sẽ dùng nhầm giá trị mới của a[0].
Không gian$O(1)$
Thời gian$O(\text{số bước})$

Kết quả chạy thật: bản C++ đúng 6/20 test, sai test 1, 8–20 (các test đúng chạy lâu nhất 0.003s) – đúng trọn 6 test $n = 4$ của subtask 1, các test khác sai vì code chỉ xử lý 4 phần tử đầu. Bản Python đúng 6/20 test, sai test 1, 8–20 (các test đúng chạy lâu nhất 0.018s).

Cách 2BFS trên các trạng thái

Duyệt theo chiều rộng: hàng đợi chứa các dãy, map dist lưu số bước tới từng dãy đã gặp. Dãy toàn 0 đầu tiên lấy ra khỏi hàng đợi cho đáp án nhỏ nhất. Chính xác với mọi $n$, nhưng số trạng thái phải lưu tăng theo cấp số nhân với độ sâu.

C++▶ Chạy tay từng dòng codeCách 2: BFS – hàng đợi các dãy, dist lưu số bước.
Dữ liệu mẫu:
Với dữ liệu cần nhiều lớp BFS (như test đề), khung chạy tay tua nhanh các dãy ở giữa, chỉ hiện vài dãy đầu và lúc tìm thấy đích.
  1. Dùng map<vector<long long>, int> để đánh dấu dãy đã thăm – không cần biết trước có bao nhiêu trạng thái.
  2. u[w + (k + 1) % 4]: với $k = 3$ phần tử "kế tiếp" là phần tử đầu cửa sổ u[w] – đúng công thức $|d - a|$.
Không gian$O(\text{số trạng thái} \cdot n)$
Thời gian$O((n-3)^{d} \cdot n \log)$

Kết quả chạy thật: bản C++ đúng 13/20 test, quá thời gian test 13–17, 19–20 (các test đúng chạy lâu nhất 0.340s). Qua subtask 1, 2 nhưng ở $n = 7, 8$ với đáp án 9–13 bước số trạng thái lên hàng chục triệu (map rất chậm và tốn bộ nhớ) nên quá thời gian. Bản Python đúng 13/20 test, quá thời gian test 13–17, 19–20 (các test đúng chạy lâu nhất 0.702s).

Cách 3Tìm kiếm sâu dần (IDDFS)

Thay vì lưu cả "biên" BFS, ta hỏi lần lượt: có về 0 được trong đúng $d$ bước không? với $d = 0, 1, 2, \dots$ Mỗi câu hỏi trả lời bằng một hàm đệ quy quay lui (thử từng cửa sổ, đi sâu $d$ tầng). $d$ đầu tiên trả lời "có" chính là đáp án. Bộ nhớ chỉ $O(d \cdot n)$, và tổng thời gian chủ yếu nằm ở tầng cuối cùng (các tầng trước nhỏ hơn nhiều).

Cắt tỉa duy nhất ở cách này: bỏ qua cửa sổ toàn 0 (biến đổi không làm thay đổi gì, chỉ phí 1 bước).

C++▶ Chạy tay từng dòng codeCách 3: tìm kiếm sâu dần – thử d = 0, 1, 2, ... bằng quay lui.
Dữ liệu mẫu:
Các lần thử d nhỏ hơn đáp án và các nhánh con thất bại được tua nhanh (vẫn chạy đúng code); nhánh tìm ra đáp án hiện đầy đủ ngăn xếp lời gọi.
Không gian$O(d \cdot n)$
Thời gian$O((n-3)^{d} \cdot n)$

Kết quả chạy thật: bản C++ đúng 18/20 test, quá thời gian test 14, 19 (các test đúng chạy lâu nhất 0.522s). Gần trọn, chỉ trượt 2 test $n = 8$ có đáp án 12–13.

Cách 4IDDFS + cắt tỉa 2 bước cuối

Ở tầng cuối của cây tìm kiếm có rất nhiều nút; ta "chặn" chúng bằng cách trả lời ngay các câu hỏi nhỏ:

  1. Còn đúng 1 bước (oneStep): dãy về 0 sau 1 bước ⇔ mọi số khác 0 nằm gọn trong một cửa sổ 4 ô và 4 số trong cửa sổ đó bằng nhau (vì $|a-b|$ $= |b-c|$ $= |c-d|$ $= |d-a|$ $= 0$ ⇔ $a = b = c = d$). Kiểm tra $O(n)$ thay vì thử $n - 3$ nhánh.
  2. Còn đúng 2 bước (twoSteps): thử cửa sổ cho bước kế tiếp, nhưng trước khi dựng dãy mới, dùng mặt nạ bit các vị trí khác 0 để loại nhanh: nếu các vị trí khác 0 trải dài hơn 4 ô thì 1 bước nữa chắc chắn không đủ.
  3. Hai cửa sổ rời nhau giao hoán: với $n = 8$, cửa sổ $[1..4]$ và $[5..8]$ không chung phần tử nên làm $A$ rồi $B$ hay $B$ rồi $A$ cho cùng kết quả → chỉ xét một thứ tự.

Mình dùng unsigned int (vì $a_i \le 10^9 < 2^{32}$) và mảng tĩnh thay cho vector để mỗi nút rẻ hơn.

C++▶ Chạy tay từng dòng codeCách 4: IDDFS + trả lời ngay khi còn 1 hoặc 2 bước + luật giao hoán.
Dữ liệu mẫu:
Các lần thử d nhỏ hơn đáp án và các nhánh con thất bại được tua nhanh. Hàm oneStep / twoSteps được gộp thành một bước (kèm lý do Đúng/Sai).
  1. Tham số last là cửa sổ vừa dùng ở bước trước, để áp dụng luật giao hoán: nếu bước trước dùng cửa sổ 4 (tức $[5..8]$) thì bước này không dùng cửa sổ 0 nữa (thứ tự "0 rồi 4" đã được xét ở nhánh khác).
  2. __builtin_ctz(m), 31 - __builtin_clz(m): vị trí bit 1 thấp nhất / cao nhất của mặt nạ, tức vị trí khác 0 đầu tiên / cuối cùng.
Không gian$O(d \cdot n)$
Thời gian$O((n-3)^{d-2} \cdot n)$ (thực tế nhỏ hơn nhiều)

Kết quả chạy thật: bản C++ đúng 20/20 test, chạy lâu nhất 0.416s. Full điểm – đây là lời giải mình đã nộp trên tinhoctre.vn (100 điểm). Mình có thử bản trung gian chỉ có cắt tỉa "1 bước cuối" + giao hoán: bản đó đúng 19/20 test, quá thời gian test 19 (các test đúng chạy lâu nhất 0.319s) – nghĩa là nhát cắt "2 bước cuối" giúp nhanh hơn khoảng 2,5 lần ở test khó nhất.

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

CáchNgôn ngữĐúngLâu nhấtGhi chú
1. Mô phỏng ($n = 4$)C++6/200.003ssai 1, 8–20 – chỉ subtask 1
Python6/200.018ssai 1, 8–20
2. BFS trên trạng tháiC++13/200.340squá giờ 13–17, 19–20 – subtask 1, 2
Python13/200.702squá giờ 13–17, 19–20
3. Tìm kiếm sâu dần (IDDFS)C++18/200.522squá giờ 14, 19
3b. IDDFS + cắt tỉa 1 bước cuốiC++19/200.319squá giờ 19 – không in code
4. IDDFS + cắt tỉa 2 bước cuốiC++20/200.416sĐúng hết
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. Python chỉ làm cách 1, 2 (IDDFS hàng triệu nút thì Python không kịp). Đây là bài mà lời giải "chính thức" cũng dựa vào thực nghiệm: không có công thức chặn số bước khi $n \ge 5$; cách 4 chạy test xấu nhất mình tìm được (đáp án 13) trong khoảng 0,5 giây. Bài học: khi BFS hết bộ nhớ, hãy thử IDDFS, và tập trung cắt tỉa ở những tầng cuối – nơi có nhiều nút nhất.
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