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

Đảo ngược đệ quy

Đảo ngược đệ quy
Nguồn: CLUE OJ – Chương 19: Đệ quy cơ bản Độ khó: Dễ Đệ quy Xâu

Đề bài

Cho một xâu kí tự $s$. Ta gọi xâu đảo ngược của $s$ là xâu có được khi viết $s$ theo thứ tự ngược lại. Ví dụ s = Huy thì xâu đảo ngược của nó là yuH.

Nhiệm vụ của bài tập này là xây dựng chương trình con đệ quy tìm và in ra màn hình xâu đảo ngược của một xâu kí tự được cho.

Dữ liệu vào:

  • Gồm một xâu kí tự có độ dài không quá $5000$.

Kết quả:

  • In ra xâu đảo ngược của xâu đã cho.
Ví dụ 1 – input
Huy
Ví dụ 1 – output
yuH
Ví dụ 2 – input
UYEN
Ví dụ 2 – output
NEYU

Lời mở đầu

Đảo ngược xâu thì dùng vòng lặp hay reverse là xong, nhưng bài này yêu cầu viết hàm đệ quy – một bài tập rất tốt để hiểu thứ tự "đi xuống – quay về" của đệ quy. Mình sẽ trình bày 2 cách đệ quy khác nhau, và chỉ ra một cái bẫy ở khâu đọc dữ liệu khiến rất nhiều bạn sai gần hết test. Bài thuộc chương Đệ quy cơ bản trên CLUE OJ (nguồn bài: VinhDinhCoder).

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, mỗi test là một dòng gồm chữ cái, chữ số và dấu cách:

TestĐộ dài xâuSố test có dấu cáchCó dấu cách ở đầu/cuốiTỉ lệ dấu cách
1–303 … 10241124%
31–1005.000703025%
  • Trừ 2 test của đề (Huy, UYEN), tất cả các test đều có dấu cách, nhiều test có dấu cách ngay ở đầu hoặc cuối xâu.
  • Test 31–100 có độ dài đúng $5000$ – đệ quy sâu 5000 tầng.
Cái bẫy: xâu có dấu cách
Nếu đọc bằng cin >> s thì máy chỉ lấy tới dấu cách đầu tiên (một "từ"). Phải đọc cả dòng bằng getline(cin, s). Mình đã thử bản dùng cin >> s: chỉ đúng 9/100 test.

Đề không chia subtask, mình chia 2 nhóm theo độ dài (kết quả chạy thật, giới hạn 1 giây):

NhómĐiều kiệnTestĐọc bằng cin >> s
(code sai)
Cách 1
(in khi quay về)
Cách 2
(đổi chỗ 2 đầu)
1độ dài $\le 10$1–309/3030/3030/30
2độ dài $= 5000$31–1000/7070/7070/70

Ý tưởngMuốn in ngược s[i..], hãy in ngược phần sau trước

Gọi rev(i) là hàm in ngược đoạn $s[i], s[i+1], \ldots, s[n-1]$ (với $n$ là độ dài xâu). Xâu ngược của đoạn này gồm: xâu ngược của đoạn $s[i+1..n-1]$, rồi tới ký tự $s[i]$ ở cuối cùng. Vậy:

Hàm đệ quy rev(i)
  1. Nếu $i = n$ (đoạn rỗng): không in gì, return. Đây là điểm dừng.
  2. Ngược lại: gọi rev(i + 1) trước, sau khi nó xong mới in $s[i]$.

Mấu chốt là dòng in nằm sau lời gọi đệ quy: lúc đi xuống ($rev(0) \to rev(1) \to \ldots$) chưa ai in gì; tới đáy rồi các lời gọi lần lượt quay về và in $s[n-1], s[n-2], \ldots, s[0]$ – đúng thứ tự ngược. Em nhập xâu rồi bấm Tiếp để xem:

Ví dụ nhanh:

Thử đổi chỗ 2 dòng (in $s[i]$ trước rồi mới gọi rev(i + 1)) thì xâu sẽ được in xuôi – các bạn thử nghĩ xem vì sao nhé.

Cách 1Gọi đệ quy trước, in sau – $O(n)$

C++▶ Chạy tay từng dòng codeCách 1: rev(i) gọi rev(i + 1) trước rồi mới in s[i] – in ngược khi các lời gọi quay về.
Dữ liệu mẫu:
  1. getline(cin, s) đọc cả dòng, giữ nguyên mọi dấu cách.
  2. Xâu s khai báo toàn cục để hàm rev dùng được mà không phải truyền xâu vào mỗi lần gọi (truyền string theo giá trị sẽ phải sao chép xâu ở mỗi tầng, rất chậm).
  3. Độ sâu đệ quy $n + 1 \le 5001$ tầng – C++ không vấn đề gì. Bản Python phải tăng giới hạn bằng sys.setrecursionlimit (mặc định chỉ 1000 tầng) và gom ký tự vào list rồi in một lần.
Không gian$O(n)$ (ngăn xếp)
Thời gian$O(n)$

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 chạy lâu nhất 0.02s.

Cách 2Đổi chỗ 2 đầu, đảo phần giữa – $O(n)$

Cách khác: muốn đảo đoạn $s[l..r]$ thì đổi chỗ $s[l]$ và $s[r]$, rồi đảo tiếp đoạn bên trong $s[l+1..r-1]$. Dừng khi $l \ge r$ (đoạn rỗng hoặc chỉ còn 1 ký tự). Cách này sửa trực tiếp xâu thành xâu ngược rồi mới in, rất tiện nếu sau đó còn cần dùng xâu đã đảo.

C++▶ Chạy tay từng dòng codeCách 2: đổi chỗ 2 đầu s[l], s[r] rồi đảo tiếp đoạn bên trong.
Dữ liệu mẫu:
  1. Gọi rev(0, s.size() - 1). Chú ý ép (int)s.size() trước khi trừ 1: nếu xâu rỗng, s.size() - 1 kiểu không dấu sẽ thành một số rất lớn.
  2. Độ sâu đệ quy chỉ khoảng $n / 2$ tầng.
Không gian$O(n)$ (ngăn xếp)
Thời gian$O(n)$

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 chạy lâu nhất 0.03s.

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

CáchNgôn ngữĐúngLâu nhấtGhi chú
Đọc bằng cin >> sC++9/1000.00sChỉ đọc được từ đầu tiên
1. Gọi đệ quy trước, in sauC++100/1000.00sFull điểm
Python100/1000.02sFull (nhớ tăng giới hạn đệ quy)
2. Đổi chỗ 2 đầu rồi đảo phần giữaC++100/1000.00sFull điểm
Python100/1000.03sFull (nhớ tăng giới hạn đệ quy)
Ghi chú của mình: cả 2 cách đệ quy đều qua 100/100 test, mỗi test vài mili giây. Lỗi chết người của bài là đọc xâu bằng cin >> s – xâu trong test có dấu cách nên phải dùng getline. Với Python nhớ sys.setrecursionlimit vì xâu dài 5000.
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