Những lỗi “nhỏ nhưng đau” khi dùng C++ trong competitive programming
Đến với competitive programming từ nền tảng Python và JavaScript, tôi từng nghĩ rằng logic đúng là đủ. Nhưng C++ lại là một “thế giới khác” – nơi những chi tiết nhỏ như kiểu dữ liệu, thứ tự toán tử hay cách đọc input có thể quyết định AC hay WA. Trong quá trình làm quen, tôi liên tục mắc những lỗi rất cơ bản nhưng lại tốn hàng giờ để debug. Bài viết này là tổng hợp những “cú vấp” đó, cùng với những bài học giúp tôi dần quen và tự tin hơn khi dùng C++ trong thi đấu.
- Hành trình đến với C++
- Kiểu dữ liệu & Overflow – nhóm lỗi nguy hiểm nhất
- Input / Output – nguồn gốc của TLE và WA “ngớ ngẩn”
- Những lỗi “chạy được nhưng sai”
- STL & hiệu năng – thủ phạm của TLE “khó hiểu”
- Lỗi về trạng thái & cấu trúc chương trình
- Debug trong C++ – “khó hơn nhưng không phải không làm được”
- Kết luận
Hành trình đến với C++
Tôi không phải là người bắt đầu hành trình lập trình thi đấu bằng C++. Tôi không học C++ một cách “chuẩn bài”. Ở trường tôi học C, sau đó học Python, đi làm lại học thêm Ruby, JavaScript, v.v… toàn là ngôn ngữ script – những ngôn ngữ giúp triển khai nhanh, ít phải bận tâm đến overflow hay quản lý bộ nhớ. Thực ra, trong một khoảng thời gian khá dài, tôi gần như không quan tâm gì đến C++.
Những ngày đầu tham gia các cuộc thi lập trình, tôi sử dụng GNU C. Đây là ngôn ngữ tôi được học ở trường nên cảm thấy quen thuộc hơn. Tuy nhiên, càng giải nhiều bài toán, tôi càng nhận thấy việc lập trình bằng C thuần túy tiêu tốn khá nhiều thời gian và công sức.
Có thể một phần nguyên nhân nằm ở việc tôi chưa biết hết các thư viện hay hàm dựng sẵn mà C cung cấp. Nhưng cảm nhận chung của tôi khi đó là mọi thứ đều phải tự xây dựng khá nhiều. Chỉ những thao tác tưởng như đơn giản như xử lý chuỗi, quản lý tập hợp dữ liệu hay sắp xếp theo các điều kiện phức tạp cũng đòi hỏi khá nhiều mã nguồn. Trong môi trường competitive programming, nơi thời gian là yếu tố vô cùng quan trọng, điều này khiến tôi cảm thấy không thật sự thoải mái.
Sau đó tôi chuyển sang Python. Đó là một trải nghiệm rất thú vị.
Python là ngôn ngữ dynamic typing, cho phép tôi viết mã rất nhanh mà gần như không cần suy nghĩ quá nhiều về kiểu dữ liệu hay khai báo biến. Tôi có thể tập trung gần như hoàn toàn vào ý tưởng giải thuật thay vì những chi tiết kỹ thuật.
Ví dụ, thay vì phải khai báo mảng, cấp phát bộ nhớ hoặc xử lý chuỗi một cách thủ công như trong C, tôi có thể viết những đoạn mã rất ngắn gọn:
s = input()
cnt = Counter(s)
hoặc:
a.sort(key=lambda x: x[1])
Ngoài ra, thư viện chuẩn của Python cũng là thứ khiến tôi có ấn tượng rất tốt. Từ các cấu trúc dữ liệu như deque, heapq, Counter, defaultdict cho đến các công cụ xử lý chuỗi, tất cả gần như đều có sẵn. Trong rất nhiều bài thi, tôi cảm thấy Python cung cấp hầu hết những gì mình cần.
Có một trải nghiệm còn khiến tôi ngạc nhiên hơn.
Trong một số bài toán liên quan đến xử lý chuỗi, tôi từng nhận kết quả TLE (Time Limit Exceeded) khi sử dụng GNU C nhưng lại (AC) Accepted khi chuyển sang Python. Điều này ban đầu khiến tôi rất khó hiểu. Trong suy nghĩ của tôi lúc đó, C là ngôn ngữ biên dịch còn Python là ngôn ngữ thông dịch, vậy thì đáng lẽ C phải nhanh hơn mới đúng.
Sau này tôi mới nhận ra rằng hiệu năng thực tế không chỉ phụ thuộc vào bản thân ngôn ngữ. Nó còn phụ thuộc vào việc thư viện chuẩn được tối ưu đến mức nào và cách tôi sử dụng ngôn ngữ đó ra sao. Nhiều thao tác xử lý chuỗi trong Python được hiện thực bằng mã nguồn C tối ưu ở bên dưới, trong khi phiên bản mà tôi tự viết bằng C lại chưa đủ hiệu quả.
Tuy nhiên, khi bắt đầu học các thuật toán và kỹ thuật nâng cao hơn trong competitive programming, tôi lại liên tục bắt gặp C++. C++ gần như là ngôn ngữ mặc định dùng trong thi đấu.
Hầu hết các editorial, các bài giải chất lượng cao, các thư viện thuật toán nổi tiếng hay những lập trình viên thi đấu hàng đầu đều sử dụng C++. Rất nhiều cấu trúc dữ liệu và kỹ thuật được minh họa bằng C++ trước tiên.
Ban đầu tôi nghĩ rằng C++ chỉ đơn giản là “C có thêm tính năng”. Nhưng càng tìm hiểu, tôi càng nhận ra điều đó chưa đúng.
C++ mang đến rất nhiều thứ mà trước đây tôi yêu thích ở Python:
- Thư viện chuẩn rất phong phú.
- Có sẵn nhiều cấu trúc dữ liệu mạnh như
vector,set,map,priority_queue. - Có nhiều thuật toán dựng sẵn như
sort,lower_bound,upper_bound,next_permutation. - Hỗ trợ string tốt hơn rất nhiều so với C.
- Có thể viết mã ở mức trừu tượng cao hơn rất nhiều so với C thuần.
Tôi nhận ra rằng rất nhiều điều khiến tôi yêu thích Python thực ra cũng tồn tại trong C++, chỉ khác ở cách sử dụng.
Tuy nhiên, C++ cũng mang theo một thử thách mới.
Khác với Python, đây là ngôn ngữ yêu cầu khai báo kiểu dữ liệu một cách tường minh. Tôi phải suy nghĩ liên tục:
- Dùng
inthaylong long? - Truyền tham chiếu hay truyền giá trị?
- Kiểu trả về của hàm là gì?
- Có bị overflow hay không?
Ban đầu điều này tạo cho tôi khá nhiều áp lực. Tôi đã quen với việc Python tự xử lý nhiều vấn đề ở phía sau. Khi chuyển sang C++, tôi phải tự chịu trách nhiệm cho những quyết định đó.
Và cũng chính từ đây, tôi bắt đầu gặp hàng loạt lỗi đặc trưng của C++: overflow, ép kiểu ngầm, sai thứ tự ưu tiên của toán tử, lỗi với kiểu unsigned, quên 1LL, hay những bug rất khó phát hiện khác.
Nhìn lại chặng đường đã qua, tôi cảm thấy việc học C++ trong competitive programming giống như quay lại học đi xe đạp sau một thời gian dài chỉ đi xe máy. Mọi thứ đều đòi hỏi nhiều sự chú ý hơn. Ban đầu khá mệt, nhưng dần dần những chi tiết đó trở thành phản xạ.
Tôi rút ra một bài học thế này:
Viết C++ trong thi đấu không chỉ là biết syntax, mà là hiểu “hành vi thật” của ngôn ngữ. Không chỉ C++ mà bất kỳ ngôn ngữ lập trình nào cũng như vậy.
Và chính những va vấp đó là lý do tôi muốn chia sẻ các lỗi thường gặp trong phần tiếp theo của bài viết. Có nhiều lỗi nghe qua tưởng rất nhỏ, nhưng tôi đã từng mất hàng giờ, thậm chí cả một buổi contest, chỉ để tìm ra nguyên nhân của chúng.
Kiểu dữ liệu & Overflow – nhóm lỗi nguy hiểm nhất
Tin tưởng int quá mức
Một ví dụ kinh điển:
int a = 1e9, b = 1e9;
int c = a * b;
Nếu đến từ Python, rất dễ nghĩ rằng đây chỉ là một phép nhân bình thường.
Nhưng:
1e9 * 1e9 = 1e18- Trong khi
intchỉ chứa được khoảng2 * 10^9
Giá trị bị overflow ngay lập tức.
Điều thú vị là sau lần gặp lỗi đầu tiên, tôi nghĩ mình đã tìm được cách sửa:
int a = 1e9, b = 1e9;
long long c = a * b;
Nhìn qua thì rất hợp lý. Biến kết quả đã là long long, vậy còn gì có thể sai nữa?
Nhưng kết quả vẫn sai.
Tôi đã mất khá lâu để nhận ra rằng lỗi không nằm ở biến c, mà nằm ở chính phép tính a * b. Vì cả hai toán hạng đều là int, phép nhân được thực hiện bằng kiểu int trước. Giá trị đã overflow từ trước khi được gán vào c.
Cách đúng:
long long c = 1LL * a * b;
Điều khiến lỗi này nguy hiểm là chương trình vẫn biên dịch, vẫn chạy, không hề báo lỗi. Chỉ có kết quả là sai.
Bài học: Đừng chỉ nhìn vào kiểu dữ liệu của biến kết quả. Hãy nhìn vào kiểu dữ liệu được dùng trong phép tính.
“long long cho tất cả” – phản xạ sai thì cũng mệt
Sau vài lần bị overflow hành hạ, tôi đi sang cực đoan còn lại.
Tôi gần như đổi toàn bộ int thành long long.
Lúc đó tôi thấy mình rất thông minh. Khỏi cần tính toán giới hạn giá trị. Khỏi cần lo overflow.
Nhưng rồi tôi bắt đầu gặp những bài có dữ liệu rất lớn. Mảng vài triệu phần tử. Những cấu trúc dữ liệu cần lưu nhiều trạng thái. Khi đó việc mọi thứ đều là long long bắt đầu trở thành gánh nặng.
Nó:
- Dùng nhiều bộ nhớ hơn
- Làm cache hoạt động kém hiệu quả hơn
- Có thể khiến chương trình chậm hơn
Tôi thậm chí từng gặp trường hợp thay long long bằng int thì AC, còn giữ nguyên thì TLE.
Đó là lúc tôi nhận ra mục tiêu không phải dùng kiểu dữ liệu lớn nhất, mà là dùng kiểu dữ liệu phù hợp nhất.
Gợi ý thực tế:
int→ giá trị khoảng10^9long long→ tổng lớn, phép nhân lớn hoặc đề cho tới10^18
Lỗi với modulo
Đây là nhóm lỗi mà tôi cứ nghĩ mình đã hiểu, nhưng rồi vẫn mắc lại sau vài tháng.
Kết quả âm
Ví dụ:
int x = (a - b) % mod;
Trong đầu tôi lúc đó, đã modulo thì kết quả phải nằm trong khoảng từ 0 đến mod - 1.
Nhưng không.
Nếu a < b, C++ có thể trả về kết quả âm.
Điều khó chịu là lỗi này thường không xuất hiện ngay. Giá trị âm đó tiếp tục được đem đi cộng, nhân hoặc lưu vào DP, khiến đáp án cuối cùng sai hoàn toàn.
Có lần tôi ngồi debug cả thuật toán vì nghĩ công thức sai. Cuối cùng nguyên nhân lại chỉ là một số âm xuất hiện từ rất sớm trong quá trình tính toán.
Tôi thường dùng:
int x = ((a - b) % mod + mod) % mod;
để bảo đảm kết quả luôn không âm.
Overflow trước khi mod
Một lỗi khác:
z = (z + x * y) % mod;
Ban đầu nhìn không có gì bất thường.
Nhưng nếu x và y đủ lớn thì x * y có thể overflow trước khi phép modulo được thực hiện.
Điều này rất dễ bị bỏ qua vì chúng ta thường có cảm giác rằng “đằng nào cũng mod mà”.
Thực tế máy tính không suy nghĩ như vậy. Nó vẫn thực hiện phép nhân trước.
Vì vậy, khi nghi ngờ overflow, tôi luôn kiểm tra kỹ kiểu dữ liệu hoặc ép kiểu rõ ràng:
z = (z + 1LL * x * y) % mod;
Bài học: Chỉ vì có % mod ở cuối không có nghĩa là mọi thứ phía trước đều an toàn.
Input / Output – nguồn gốc của TLE và WA “ngớ ngẩn”
Không dùng fast I/O
ios::sync_with_stdio(false);
cin.tie(nullptr);
Hồi đầu tôi thường copy code của người khác mà không hiểu hai dòng này làm gì.
Thậm chí có thời gian tôi còn xóa chúng đi vì nghĩ rằng đó là “template thừa”.
Rồi đến một ngày, tôi gặp bài toán đọc vài trăm nghìn hoặc vài triệu số.
Thuật toán đúng.
Độ phức tạp đúng.
Nhưng vẫn TLE.
Sau rất nhiều lần thử nghiệm, tôi nhận ra nguyên nhân không nằm ở giải thuật mà nằm ở việc nhập xuất dữ liệu.
Từ đó về sau, hai dòng trên gần như trở thành phản xạ khi tôi bắt đầu một bài C++ mới.
Đọc thiếu input
Đây có lẽ là loại lỗi khiến tôi cảm thấy xấu hổ nhất.
Ví dụ đề bài cho:
n- một mảng
a
Có những bài toán, chỉ cần biết n là có câu trả lời, và tôi “quên” luôn mảng a phía sau.
Chương trình vẫn chạy.
Không compile error.
Không runtime error.
Nhưng toàn bộ dữ liệu phía sau bị lệch.
Điều nguy hiểm là khi WA, tôi thường nhìn vào thuật toán đầu tiên, trong khi lỗi thực sự nằm ở phần đọc dữ liệu.
Từ đó tôi tập cho mình thói quen đọc kỹ format input trước khi viết code.
Những lỗi “chạy được nhưng sai”
Lệch index
Một lỗi kinh điển:
for (int i = 0; i <= n; i++)
Khi đáng lẽ phải là:
for (int i = 0; i < n; i++)
Nghe rất nhỏ.
Nhưng hậu quả có thể là:
- Bỏ sót một phần tử
- Đếm thừa một trường hợp
- Truy cập ra ngoài mảng
Điều làm tôi khó chịu nhất là nhiều test nhỏ vẫn chạy đúng.
Chỉ đến khi gặp đúng một test biên nào đó thì chương trình mới WA.
Sau khá nhiều lần như vậy, tôi cố gắng nhất quán ngay từ đầu:
- hoặc
[0, n-1] - hoặc
[1, n]
Hạn chế tối đa việc chuyển đổi qua lại.
Undefined Behavior (UB)
Nếu phải chọn một thứ khiến tôi sợ nhất trong C++, tôi sẽ chọn UB.
Ví dụ:
int a[10];
cout << a[100];
Lần đầu gặp tình huống tương tự, tôi nghĩ chương trình sẽ crash.
Nhưng không.
Nó vẫn chạy.
Nó vẫn in ra một con số.
Thậm chí đôi khi con số đó còn giống như hợp lý.
Tôi đã từng rơi vào tình huống chạy đúng trên máy cá nhân nhưng nộp lên hệ thống chấm bài lại WA.
Cuối cùng mới phát hiện mình đang truy cập ngoài phạm vi mảng.
Điều đáng sợ của UB là bạn không thể dự đoán điều gì sẽ xảy ra.
Hôm nay chương trình chạy đúng.
Ngày mai vẫn đoạn code đó có thể cho kết quả khác.
Vì vậy, mỗi khi gặp lỗi quá khó hiểu, tôi luôn nghĩ tới khả năng mình đã tạo ra UB ở đâu đó.
Bẫy precedence (thứ tự ưu tiên phép tính)
Tôi từng quen với Python đến mức vô thức cho rằng mọi thứ sẽ hoạt động giống Python.
Ví dụ:
if (x ^ y == 0)
Khi viết dòng này, trong đầu tôi đang nghĩ:
if ((x ^ y) == 0)
Nhưng C++ không đọc suy nghĩ của tôi, mà hiểu là:
if (x ^ (y == 0))
Do thứ tự ưu tiên toán tử, biểu thức có thể được hiểu khác hẳn những gì tôi mong muốn.
Hậu quả là logic kiểm tra bit sai hoàn toàn.
Điều làm tôi mất thời gian là nhìn vào code, tôi vẫn thấy nó “có vẻ đúng”.
Sau vài lần như vậy, tôi quyết định đơn giản hóa cuộc sống của mình:
if ((x ^ y) == 0)
Tôi dùng ngoặc nhiều hơn mức cần thiết.
Có thể không đẹp.
Có thể hơi dài.
Nhưng tôi ngủ ngon hơn.
Và thành thật mà nói, AC quan trọng hơn việc code trông thật ngầu.
size() và kiểu unsigned
Một lỗi mà tôi từng mất khá lâu mới hiểu:
for (int i = v.size() - 1; i >= 0; i--)
Thoạt nhìn hoàn toàn hợp lý.
Tôi nghĩ rằng nếu vector rỗng thì:
v.size() - 1
sẽ là -1.
Nhưng thực tế không phải vậy.
size() trả về kiểu unsigned.
Vì thế:
0 - 1
không trở thành -1, mà trở thành một số rất lớn.
Hậu quả là vòng lặp có thể chạy theo cách hoàn toàn khác những gì tôi hình dung.
Sau khi hiểu nguyên nhân, tôi bắt đầu chú ý hơn đến việc trộn kiểu có dấu và không dấu trong C++.
STL & hiệu năng – thủ phạm của TLE “khó hiểu”
Dùng map như dict
Khi mới chuyển từ Python sang, tôi thấy:
map<int, int>
trông rất giống:
dict()
Thế là tôi dùng map ở khắp mọi nơi.
Chương trình vẫn đúng.
Nhưng một số bài lại TLE một cách khó hiểu.
Sau khi đọc editorial, tôi mới phát hiện rằng:
map→ O(log n)unordered_map→ trung bình O(1)
Thuật toán không sai.
Nhưng cấu trúc dữ liệu không phù hợp.
Đó là một trong những lần đầu tiên tôi nhận ra rằng việc chọn data structure đôi khi quan trọng không kém bản thân thuật toán.
Copy dữ liệu không cần thiết
Ví dụ:
void solve(vector<int> a)
Trong thời gian đầu, tôi không hề biết dòng này tạo ra một bản sao của cả vector.
Nếu vector chỉ có vài chục phần tử thì chẳng sao.
Nhưng nếu vector có vài trăm nghìn phần tử và hàm được gọi nhiều lần, chi phí copy sẽ trở nên đáng kể.
Điều khó chịu là thuật toán trên giấy vẫn đúng.
Độ phức tạp lớn vẫn đúng.
Nhưng chương trình chậm hơn rất nhiều so với kỳ vọng.
Tôi dần hình thành thói quen:
void solve(const vector<int>& a)
trừ khi thực sự cần sửa dữ liệu.
priority_queue
Một lỗi rất đơn giản nhưng tôi đã mắc nhiều lần.
Mặc định:
priority_queue<int> pq;
là max-heap.
Trong khi khá nhiều bài lại cần min-heap.
Có lần tôi debug cả thuật toán Dijkstra vì nghĩ phần relax cạnh bị sai.
Cuối cùng phát hiện mình chỉ dùng nhầm loại heap.
Cách khai báo min-heap:
priority_queue<int, vector<int>, greater<int>> pq;
Comparator sai
Khi truyền hàm cho hàm sort tôi đã từng dùng:
return a <= b;
Tôi từng nghĩ rằng điều này hoàn toàn hợp lý.
Nhưng comparator của sort phải tạo ra một thứ tự nghiêm ngặt.
Khi dùng:
return a <= b;
chương trình có thể xuất hiện những hành vi rất khó dự đoán.
Phải viết:
return a < b;
Đây là một trong những lỗi mà nếu chưa từng gặp, rất khó nghĩ đến khi debug.
Nối chuỗi
Đã từng có lúc tôi viết kiểu thế này. Với Python, hiệu suất của phép nối chuỗi này khá tốt, tốt hơn nhiều so với C++, nên thường không gặp vấn đề gì.
s = s + 'a';
Nhưng C++ hoạt động hoàn toàn khác. Phép nối chuỗi này sẽ tạo ra chuỗi mới và gán giá trị lại cho s, với dữ liệu nhỏ thì không sao.
Nhưng nếu đặt trong một vòng lặp lớn, việc tạo chuỗi mới liên tục có thể khiến hiệu năng giảm đáng kể.
Tôi thường dùng:
s += 'a';
vừa tự nhiên hơn vừa hiệu quả hơn.
Lỗi về trạng thái & cấu trúc chương trình
Stack overflow khi đệ quy sâu
Khi gặp các bài toán đề đồ thị, rất hay gặp DFS, tôi rất thích đệ quy.
Code ngắn.
Code đẹp.
Code dễ đọc.
Nhưng có lần chương trình của tôi đột nhiên crash dù thuật toán hoàn toàn đúng.
Sau một lúc tìm hiểu, tôi phát hiện cây trong test có độ sâu quá lớn.
Mỗi lần gọi hàm đệ quy lại sử dụng thêm stack.
Cuối cùng stack bị tràn.
Đây là lỗi mà người quen Python cũng có thể gặp, nhưng tôi thấy nó xuất hiện khá thường xuyên trong C++ khi xử lý cây hoặc đồ thị có dạng dây thẳng dài.
Vì vậy, bây giờ, các thuật toán liên quan đến DFS tôi sẽ dùng stack và không đệ quy.
Debug trong C++ – “khó hơn nhưng không phải không làm được”
So với Python, tôi cảm thấy việc debug C++ khó hơn một chút.
Không phải vì công cụ kém.
Mà vì có nhiều lỗi:
- Không crash
- Không báo exception
- Không compile error
Chương trình chỉ đơn giản cho ra đáp án sai.
Đó cũng là khác biệt lớn nhất mà tôi cảm nhận khi chuyển từ Python sang C++.
Nếu một chương trình Python lỗi, rất nhiều trường hợp nó sẽ ném ra một exception khá rõ ràng.
Còn trong C++, nhiều lỗi vẫn cho phép chương trình tiếp tục chạy như chưa có chuyện gì xảy ra.
Sau rất nhiều lần WA, tôi hình thành một checklist nho nhỏ:
- Overflow chưa?
- Index có vượt biên không?
- Có quên đọc input không?
- Có quên reset dữ liệu không?
- Kiểu dữ liệu đã phù hợp chưa?
- Có UB ở đâu không?
Nhiều lần đáp án nằm ngay trong những câu hỏi đơn giản đó.
Kết luận
Là một người đi từ Python trước khi học C++, tôi thấy hành trình này vừa khó khăn vừa thú vị.
Python giúp tôi yêu thích competitive programming vì tốc độ phát triển nhanh và thư viện phong phú. Nhưng khi muốn đi sâu hơn vào các bài toán thuật toán, tôi nhận ra C++ gần như là một kỹ năng bắt buộc.
Điều bất ngờ là C++ không hề “nghèo nàn” như tôi từng nghĩ khi so sánh với Python. Ngược lại, STL cung cấp rất nhiều công cụ mạnh mẽ, hiệu năng cao và cực kỳ phù hợp cho thi đấu.
Thứ khó nhất không phải là học các tính năng mới, mà là thay đổi tư duy. Tôi phải học cách chú ý đến kiểu dữ liệu, độ phức tạp thực tế, hành vi của toán tử và rất nhiều chi tiết nhỏ khác.
Nhìn lại, gần như mọi lỗi trong bài viết này tôi đều đã từng mắc nhiều hơn một lần. Có lỗi khiến tôi mất vài phút để sửa, có lỗi khiến tôi mất cả buổi tối để debug, nhưng chính những lần vấp ngã đó giúp tôi hiểu C++ hơn từng chút một.
Nếu bạn cũng đang chuyển từ Python hoặc bất kỳ ngôn ngữ script nào khác sang C++, tôi nghĩ việc mắc những lỗi như vậy là hoàn toàn bình thường.
Quan trọng nhất không phải là tránh được mọi sai lầm ngay từ đầu.
Quan trọng là mỗi lần gặp lỗi, chúng ta hiểu được nguyên nhân của nó, hiểu được mình đã nghĩ sai ở đâu, để rồi một ngày nào đó những thứ từng khiến mình đau đầu lại trở thành phản xạ tự nhiên khi viết code.
Và có lẽ đó cũng là lúc chúng ta thực sự bắt đầu hiểu C++, chứ không chỉ đơn thuần là biết viết C++.
Welcome

Đây là thế giới của manhhomienbienthuy (naa). Chào mừng đến với thế giới của tôi!
Bài viết liên quan
Bài viết mới
Chuyên mục
Lưu trữ theo năm
Thông tin liên hệ
Cảm ơn bạn đã quan tâm blog của tôi. Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.