Think Like a Programmer #2 — Pure Puzzles: Khi 'bài toán thuần tuý' mới thấy mình yếu ở đâu
Mở đầu
Hồi trước mình có viết về Chương 1 của cuốn Think Like a Programmer — bàn về chiến lược giải quyết vấn đề nói chung. Tới Chương 2 này, tác giả V. Anton Spraul mới bắt đầu cho "động tay động chân" vô code thiệt. Và ổng cũng nói trước luôn: cú pháp ở chương này là đơn giản nhất có thể — nếu mấy bạn thấy khó thì không phải tại C++ syntax đâu, tại cái cách suy nghĩ đó.
Nghe thì có vẻ "xạo" nhưng đọc xong mình mới thấm. Đúng là khi bỏ hết mấy thứ phức tạp như class, pointer, STL đi, chỉ còn vòng lặp với điều kiện, tự nhiên thấy cái đầu mình nó... trống rỗng thiệt. Ngồi trước bài toán sinh hình nửa cái tam giác mà nghĩ hoài không ra cách tối ưu.
Ảnh: ThisIsEngineering — Pexels
Chapter 2: Pure Puzzles — Nội dung chính
1. Output Patterns — Mấy bài in hình
Chương này mở đầu bằng một loạt bài tập in ký tự ra màn hình: in nửa hình vuông (half-square), in hình tam giác ngang (sideways triangle). Nghe tưởng dễ chứ không đâu. Cái khó là phải tìm ra công thức tổng quát cho số lượng ký tự trên mỗi dòng.
Spraul dùng mấy bài này để dạy phương pháp "reduce the problem" — bắt đầu từ cái đơn giản nhất (một dòng), rồi từ từ thêm vô. Thay vì lao vô giải nguyên cái hình tam giác, ổng bắt mình giải bài toán "in 5 dấu #" trước, rồi "in 4 dấu #", rồi mới ghép lại thành vòng lặp. Nghe có vẻ cơ bản, nhưng mà cái cách suy nghĩ đó áp dụng được cho mọi bài toán lớn — lúc nào cũng hỏi: "đâu là version đơn giản nhất của bài toán này?"
2. Luhn Checksum Validation — Thuật toán có thiệt
Bài toán thứ hai là kiểm tra số thẻ tín dụng bằng thuật toán Luhn. Đây là thuật toán có thật ngoài đời — mỗi lần mấy bạn nhập số thẻ online là nó chạy đó. Bài toán này dạy cách xử lý input từng ký tự một và chia bài toán thành nhiều bước nhỏ:
- Đọc từng chữ số từ chuỗi nhập vào
- Nhân đôi mỗi chữ số thứ hai (từ phải qua)
- Cộng tất cả lại
- Kiểm tra tổng có chia hết cho 10 không
Cái hay là bài toán tưởng dài mà khi phân rã ra từng bước nhỏ, mỗi bước chỉ cần 5-10 dòng code. Đúng cái tinh thần "chia để trị" mà Chương 1 đã nhắc.
Ảnh: Mikael Blomkvist — Pexels
3. Decode a Message — Tracking State
Bài cuối cùng trong chương là giải mã một thông điệp. Điểm mới ở đây là trạng thái (state) — chương trình phải nhớ mình đang ở đâu trong quá trình xử lý, gặp ký tự gì, cần làm gì tiếp theo. Ngày xưa mình cứ nghĩ biến chỉ để lưu số hay chữ thôi, chứ ít khi nghĩ tới việc dùng biến để "nhớ trạng thái" của chương trình.
Đây là bài học quan trọng: khi xử lý dữ liệu tuần tự (string, stream, file), chương trình của mấy bạn cần có một cái "bộ nhớ tạm" để biết đã làm gì rồi và sắp làm gì tiếp. Nó giống như đọc sách — nếu không nhớ trang trước đọc gì thì trang sau khó mà hiểu được.
Cảm nhận của mình
Phải công nhận là đọc Chương 2 này xong mình thấy hơi "nhục" tí. Tại vì mấy bài toán trong này dùng toàn kiến thức cơ bản nhất — vòng lặp for, while, câu lệnh if, biến đếm — mà có bài mình cũng phải ngồi nghĩ tới 10-15 phút mới ra. Nhất là cái bài Luhn checksum, đọc đề xong cứ tưởng đơn giản, viết xong mới thấy mình quên xử lý mấy edge case (số thẻ có độ dài khác nhau, ký tự lẫn chữ,...).
Điều mình thích nhất ở chương này là cách tác giả không cho đáp án ngay. Ổng dẫn dắt từng bước: đưa ra bài toán —> phân tích —> viết code từng phần —> rồi mới ráp lại. Cảm giác như đang ngồi pair programming với một senior vậy. Không phải kiểu "đây là code mẫu, chép đi" mà là "cùng suy nghĩ, cùng giải nào".
Riêng cái phương pháp "bắt đầu từ cái đơn giản nhất" ảnh hưởng tới mình khá nhiều. Hồi đó mỗi lần nhận task là mình cứ lao vô viết luôn cái solution hoàn chỉnh. Kết quả là code rối, bug đầy, debug mệt nghỉ. Từ hồi đọc sách này, mình tập cách: viết cái đơn giản trước, test, rồi mới thêm dần. Nghe có vẻ chậm hơn nhưng mà thực ra nhanh hơn nhiều.
Kết
Chương 2 — Pure Puzzles đúng như cái tên: nó là những bài toán "thuần tuý", không bị che lấp bởi syntax phức tạp hay thư viện khủng. Nó phơi bày điểm yếu trong tư duy của mỗi lập trình viên. Nếu mấy bạn học được cách "reduce the problem" và "break it down" từ chương này, thì những chương sau (array, pointer, recursion) sẽ dễ thở hơn nhiều.
Hẹn mấy bạn ở bài sau — Chương 3: Solving Problems with Arrays. Lúc đó mới bắt đầu vô mấy cấu trúc dữ liệu thiệt nè. Chắc cũng không dễ đâu 😄
📋 Phụ lục thuật ngữ
- Luhn Checksum — thuật toán kiểm tra số thẻ tín dụng dựa trên checksum, nhân đôi mỗi chữ số thứ hai và kiểm tra tổng chia hết cho 10
- State tracking — kỹ thuật dùng biến để lưu trạng thái hiện tại của chương trình trong quá trình xử lý dữ liệu tuần tự
- Reduce the problem — chiến lược bắt đầu từ phiên bản đơn giản nhất của bài toán, sau đó thêm dần độ phức tạp