Đăng nhập để tham gia lộ trình luyện tập
Queue trong lập trình thi đấu
1. Queue là gì?
Queue là cấu trúc dữ liệu hàng đợi hoạt động theo nguyên tắc:
Nghĩa là:
Phần tử được đưa vào trước sẽ được lấy ra trước.
Ví dụ, lần lượt đưa vào:
thì thứ tự lấy ra là:
Queue thường hỗ trợ các thao tác:
push: thêm phần tử vào cuối hàng đợi.pop: xóa phần tử ở đầu hàng đợi.front: lấy phần tử đầu hàng đợi.back: lấy phần tử cuối hàng đợi.empty: kiểm tra hàng đợi có rỗng hay không.size: số phần tử hiện có.
Với cách cài đặt thích hợp, các thao tác cơ bản có độ phức tạp:
2. Bản chất cần nhớ
Queue phù hợp khi bài toán có tính chất:
Trạng thái nào được sinh ra trước thì cần được xử lý trước.
Có thể hình dung:
$$\text{đưa trạng thái mới vào cuối} \rightarrow \text{xử lý trạng thái cũ ở đầu}$$Đây là lý do Queue xuất hiện tự nhiên trong:
- Mô phỏng.
- BFS.
- Đường đi ngắn trên đồ thị không trọng số.
- Duyệt theo từng lớp.
- Multi-source BFS.
- Topological Sort bằng Kahn.
- Flood Fill.
- State-space BFS.
- Monotonic Queue.
- Sliding Window.
- 0-1 BFS.
- Một số tối ưu Quy hoạch động.
3. Queue cơ bản
Giả sử hàng đợi hiện tại là:
Phần tử đầu:
Phần tử cuối:
Nếu thêm :
Nếu lấy một phần tử ra:
Phần tử bị loại vì nằm ở đầu hàng đợi.
4. Queue trong bài toán mô phỏng
Dạng đơn giản nhất là mô phỏng một hàng người, hàng công việc hoặc danh sách sự kiện.
Dấu hiệu nhận biết:
- Người đến trước được phục vụ trước.
- Công việc được xử lý theo thứ tự xuất hiện.
- Mỗi bước lấy phần tử đầu rồi sinh thêm phần tử mới.
- Một phần tử xử lý xong có thể quay lại cuối hàng.
Mô hình tổng quát:
$$\text{front} \rightarrow \text{xử lý} \rightarrow \text{pop} \rightarrow \text{push trạng thái mới}$$Các dạng thường gặp:
- Hàng người chờ.
- Máy in.
- Bộ lập lịch Round Robin.
- Trò chơi chuyền lượt.
- Hot Potato.
- Josephus biến thể.
- Mô phỏng tiến trình.
5. Circular Queue
Nếu tự cài Queue bằng mảng cố định, không nên liên tục dịch toàn bộ phần tử sau mỗi lần pop.
Ta sử dụng hai chỉ số:
và xem mảng như một vòng tròn.
Chỉ số tiếp theo:
Khi đó:
- Thêm cuối: cập nhật .
- Xóa đầu: cập nhật .
Mỗi thao tác vẫn là:
Circular Queue đặc biệt hữu ích khi:
- Bộ nhớ có kích thước cố định.
- Dữ liệu được xử lý liên tục.
- Cần tự cài Queue không dùng thư viện.
6. BFS và Queue
Ứng dụng quan trọng nhất của Queue là Breadth-First Search.
BFS duyệt đồ thị theo từng lớp khoảng cách.
Nếu bắt đầu tại đỉnh :
Nếu từ đi được sang bằng một cạnh:
với điều kiện chưa được thăm.
Queue đảm bảo các đỉnh có khoảng cách nhỏ hơn được xử lý trước.
Thứ tự tổng quát:
- Đưa đỉnh xuất phát vào Queue.
- Lấy đỉnh đầu Queue.
- Xét tất cả đỉnh kề.
- Đỉnh chưa thăm được đánh dấu ngay.
- Gán khoảng cách.
- Đưa đỉnh mới vào cuối Queue.
7. Vì sao BFS tìm được đường đi ngắn nhất?
Trong đồ thị không trọng số, mỗi cạnh có thể xem như có chi phí:
BFS xử lý các đỉnh theo thứ tự:
tính theo số cạnh từ nguồn.
Do đó lần đầu tiên đến được đỉnh chính là lúc tìm được khoảng cách nhỏ nhất:
BFS giải bài toán đường đi ngắn nhất khi:
cho mọi cạnh .
Độ phức tạp:
với:
- : số đỉnh.
- : số cạnh.
8. Truy vết đường đi bằng BFS
Ngoài khoảng cách, lưu:
khi lần đầu đi từ sang .
Muốn dựng đường từ đến , đi ngược:
$$t \rightarrow parent[t] \rightarrow parent[parent[t]] \rightarrow \cdots \rightarrow s$$sau đó đảo ngược thứ tự.
9. BFS trên lưới
Một ô được xem như một trạng thái.
Với lưới bốn hướng:
Từ:
có thể sinh:
Điều kiện thường gồm:
- Không ra ngoài lưới.
- Không đi vào vật cản.
- Chưa được thăm.
Độ phức tạp với lưới :
10. Flood Fill bằng Queue
Flood Fill dùng BFS để tìm các ô thuộc cùng một thành phần liên thông.
Tư duy:
Chọn một ô chưa thăm, BFS từ ô đó sẽ đánh dấu toàn bộ vùng liên thông chứa nó.
Nếu cần đếm số vùng:
mỗi khi bắt đầu một BFS mới.
Ứng dụng:
- Đếm phòng.
- Đếm đảo.
- Đếm vùng màu.
- Đếm thành phần liên thông trên ma trận.
11. Multi-source BFS
Nếu có nhiều nguồn cùng bắt đầu tại thời điểm , không cần chạy BFS nhiều lần.
Khởi tạo:
với mọi nguồn .
Sau đó đưa tất cả nguồn vào Queue trước khi BFS.
Khi đó:
Nghĩa là khoảng cách từ đến nguồn gần nhất.
Độ phức tạp vẫn là:
không phải:
với là số nguồn.
Ứng dụng:
- Nhiều đám cháy lan đồng thời.
- Nhiều quái vật di chuyển.
- Khoảng cách tới cửa hàng gần nhất.
- Khoảng cách tới số gần nhất.
- Nhiều nguồn phát tín hiệu.
- Voronoi trên đồ thị không trọng số.
12. BFS theo từng lớp
Nếu cần xử lý riêng từng mức khoảng cách, tại mỗi vòng lấy:
Sau đó xử lý đúng phần tử hiện tại.
Toàn bộ phần tử này thuộc cùng một lớp.
Ứng dụng:
- Duyệt cây theo tầng.
- Tính số bước.
- Mô phỏng thời gian.
- Lan truyền theo từng phút.
- Level Order Traversal.
13. BFS trên không gian trạng thái
Đỉnh của BFS không nhất thiết là một số nguyên.
Một trạng thái có thể là:
hoặc:
hoặc:
hoặc:
Điều quan trọng nhất là xác định:
và:
Nếu mỗi phép chuyển có cùng chi phí , có thể dùng BFS.
Ví dụ:
Khoảng cách phải lưu theo toàn bộ trạng thái:
không thể chỉ lưu:
nếu trạng thái khóa ảnh hưởng tới tương lai.
14. Topological Sort bằng Queue
Thuật toán Kahn sử dụng Queue để sắp xếp topo DAG.
Gọi:
là số cạnh đi vào đỉnh .
Ban đầu đưa mọi đỉnh có:
vào Queue.
Khi lấy ra, với mỗi cạnh:
thực hiện:
Nếu:
thì đưa vào Queue.
Nếu số đỉnh lấy được nhỏ hơn , đồ thị có chu trình.
Độ phức tạp:
15. Deque
Deque là hàng đợi hai đầu.
Ta có thể:
- Thêm đầu.
- Thêm cuối.
- Xóa đầu.
- Xóa cuối.
- Xem phần tử đầu.
- Xem phần tử cuối.
Mỗi thao tác:
Deque là nền tảng của:
- 0-1 BFS.
- Monotonic Queue.
- Sliding Window Minimum.
- Sliding Window Maximum.
- Một số tối ưu DP.
16. 0-1 BFS
Nếu trọng số cạnh chỉ thuộc:
thì có thể dùng Deque thay vì Dijkstra.
Relax cạnh:
Nếu cập nhật được:
thì:
- Nếu , đưa vào đầu Deque.
- Nếu , đưa vào cuối Deque.
Tức là:
Bản chất:
Trạng thái không làm tăng khoảng cách phải được xử lý sớm hơn.
Độ phức tạp:
17. Khi nào dùng BFS, 0-1 BFS hay Dijkstra?
Nếu mọi cạnh có:
dùng BFS.
Nếu:
dùng 0-1 BFS.
Nếu:
và trọng số tổng quát, thường dùng Dijkstra.
Phản xạ cần nhớ:
$$\text{trọng số không âm bất kỳ} \rightarrow \text{Dijkstra}$$18. Queue có truy vấn Min hoặc Max
Queue thông thường không thể lấy giá trị nhỏ nhất trong:
nếu không có cấu trúc bổ sung.
Ta có thể xây dựng Min Queue sao cho:
push: trung bình.pop: .minimum: .
Một phương pháp quan trọng là sử dụng Monotonic Queue.
19. Monotonic Queue
Monotonic Queue thường được cài bằng Deque.
Nó duy trì các phần tử theo thứ tự đơn điệu.
Có hai dạng chính:
- Monotonic Increasing Queue.
- Monotonic Decreasing Queue.
Mục tiêu:
Loại bỏ những phần tử chắc chắn không còn khả năng trở thành đáp án trong tương lai.
Đây là tư tưởng quan trọng nhất.
20. Monotonic Increasing Queue
Dùng để duy trì giá trị nhỏ nhất.
Ta giữ:
Khi thêm phần tử mới , trong khi:
thì loại khỏi cuối.
Sau đó thêm .
Khi đó phần tử nhỏ nhất luôn nằm ở đầu:
21. Monotonic Decreasing Queue
Dùng để duy trì giá trị lớn nhất.
Ta giữ:
Khi thêm , trong khi:
thì loại phần tử cuối.
Sau đó thêm .
Giá trị lớn nhất:
22. Vì sao được phép loại phần tử?
Giả sử đang tìm minimum.
Có hai phần tử:
nhưng:
Phần tử :
- Xuất hiện muộn hơn.
- Không lớn hơn .
Do đó trong mọi cửa sổ tương lai chứa cả hai, không thể tốt hơn .
Vì vậy bị dominate bởi và có thể xóa vĩnh viễn.
Đây chính là bản chất của Monotonic Queue.
23. Sliding Window Minimum
Cho mảng:
và cửa sổ kích thước .
Cần tính:
cho mọi .
Dùng Deque lưu chỉ số.
Khi xét :
- Loại đầu nếu chỉ số đã ra khỏi cửa sổ.
- Loại cuối trong khi giá trị cuối không tốt hơn .
- Thêm vào cuối.
- Đầu Deque là vị trí minimum.
Điều kiện phần tử hết hạn:
Độ phức tạp toàn bộ:
24. Sliding Window Maximum
Tương tự Sliding Window Minimum nhưng duy trì Deque giảm dần.
Khi xét , loại cuối khi:
Maximum hiện tại:
Độ phức tạp:
25. Vì sao Monotonic Queue là ?
Một phần tử có thể:
- Được thêm vào Deque đúng một lần.
- Bị xóa khỏi Deque nhiều nhất một lần.
Do đó tổng số thao tác trên toàn bộ mảng là tuyến tính:
Đây là amortized analysis.
Không phải mỗi vòng while chạy độc lập.
26. Hai Monotonic Queue cùng lúc
Một dạng rất quan trọng là duy trì đồng thời:
và:
Ta dùng:
- Một Deque giảm dần cho maximum.
- Một Deque tăng dần cho minimum.
Khi đó có thể kiểm tra nhanh điều kiện:
Ứng dụng:
- Longest Continuous Subarray.
- Đoạn dài nhất có độ dao động không quá .
- Two Pointers kết hợp Monotonic Queue.
Độ phức tạp:
27. Two Pointers kết hợp Monotonic Queue
Giữ cửa sổ:
Mỗi khi tăng , thêm vào hai Monotonic Queue.
Nếu điều kiện bị vi phạm:
thì tăng cho đến khi cửa sổ hợp lệ.
Mỗi chỉ số vào và ra cấu trúc số lần hữu hạn nên tổng độ phức tạp:
28. Prefix Sum kết hợp Monotonic Queue
Một dạng nâng cao rất quan trọng xuất hiện trong bài toán tổng đoạn.
Đặt:
Tổng đoạn:
Nhiều bài toán có thể biến thành tìm hai prefix:
thỏa mãn một điều kiện.
Nếu cần tìm ứng viên tốt nhất trong một miền trượt, Monotonic Queue có thể giảm:
xuống:
29. Shortest Subarray With Sum At Least K
Cần tìm đoạn ngắn nhất sao cho:
tương đương:
Ta duy trì Deque các chỉ số prefix có giá trị tăng dần:
Với mỗi :
Nếu:
thì có một đoạn hợp lệ.
Cập nhật:
và tiếp tục loại đầu để thử tìm đoạn ngắn hơn.
Ở cuối, loại các prefix bị dominate:
Đây là một trong những ứng dụng quan trọng nhất của Monotonic Queue.
30. Monotonic Queue trong Quy hoạch động
Một dạng tổng quát:
Nếu phần phụ thuộc vào có thể tách thành:
thì:
Ta chỉ cần duy trì minimum của trong một cửa sổ trượt.
Monotonic Queue biến mỗi transition từ:
thành:
amortized.
Tổng độ phức tạp giảm từ:
xuống:
31. Mẫu DP cửa sổ trượt
Ví dụ:
Thay vì duyệt:
ta duy trì Deque chứa các chỉ số sao cho:
Khi đó:
Độ phức tạp:
32. DP với giới hạn khoảng cách
Một dạng phổ biến:
Nếu miền:
trượt dần sang phải, ta có thể duy trì các hợp lệ bằng Monotonic Queue.
Đây là dấu hiệu rất mạnh:
DP có transition lấy min/max trên một đoạn chỉ số liên tiếp đang trượt.
33. Bounded Knapsack và Monotonic Queue
Một dạng nâng cao là tối ưu Knapsack có giới hạn số lượng.
Giả sử một loại vật có:
- Trọng lượng .
- Giá trị .
- Số lượng tối đa .
Transition:
Xét các vị trí có cùng phần dư modulo :
Khi đó:
$$newdp[r+tw] = tv+ \max_{t-c\le j\le t} \{dp[r+jw]-jv\}$$Đặt:
Ta cần maximum trên một cửa sổ:
Đây chính là Sliding Window Maximum.
Dùng Monotonic Queue có thể giảm một transition lớn xuống gần:
cho mỗi loại vật, với là sức chứa ba lô.
Đây là một ứng dụng nâng cao rất quan trọng của Monotonic Queue trong DP.
34. Queue bằng hai Stack
Có thể xây dựng Queue bằng hai Stack:
Khi thêm phần tử:
Khi cần lấy đầu mà rỗng, chuyển toàn bộ:
Thứ tự bị đảo nên phần tử cũ nhất nằm trên đỉnh .
Độ phức tạp amortized mỗi thao tác:
35. Min Queue bằng hai Min Stack
Nếu mỗi Stack có thể trả về minimum trong:
thì Queue bằng hai Stack cũng có thể trả về minimum:
Các thao tác vẫn có độ phức tạp amortized:
Đây là một cách khác để xây dựng Queue hỗ trợ truy vấn min.
36. Bidirectional BFS
Nếu cần tìm đường từ đến trong không gian trạng thái rất lớn, có thể BFS từ hai phía.
Một phía bắt đầu tại:
phía kia tại:
Khi hai miền tìm kiếm gặp nhau, ghép kết quả.
Nếu hệ số phân nhánh là và khoảng cách là , BFS thường cần khoảng:
trạng thái.
Bidirectional BFS có thể giảm gần thành:
trên mỗi phía.
Hiệu quả đặc biệt khi:
- Có một nguồn và một đích rõ ràng.
- Phép chuyển có thể đi ngược.
- Không gian trạng thái lớn.
37. Queue và bài toán lan truyền
Nhiều bài mô tả quá trình lan truyền:
- Lửa.
- Nước.
- Virus.
- Sóng.
- Tin nhắn.
- Trạng thái.
- Khoảng cách.
Nếu mỗi bước lan sang các trạng thái kề trong đúng một đơn vị thời gian thì bản chất thường là:
Thời điểm đến:
38. Queue và mô phỏng sự kiện
Không phải bài Queue nào cũng là BFS.
Nếu sự kiện phải xử lý đúng thứ tự xuất hiện, Queue thuần có thể đủ.
Nhưng nếu cần xử lý sự kiện có thời gian nhỏ nhất trước thì Queue thường không còn phù hợp.
Khi đó cần:
Phân biệt:
$$\text{nhỏ nhất/lớn nhất trước} \rightarrow \text{Priority Queue}$$39. Queue và Priority Queue khác nhau
Queue chọn phần tử theo:
Priority Queue chọn phần tử theo:
Ví dụ:
BFS dùng Queue vì khoảng cách tăng theo từng lớp.
Dijkstra dùng Priority Queue vì cần luôn chọn đỉnh có:
nhỏ nhất hiện tại.
Không được nhầm hai cấu trúc này.
40. Queue và Deque khác nhau
Queue chỉ cho phép:
và:
Deque cho phép cả:
và:
Do đó Deque mạnh hơn và là công cụ chính của:
- 0-1 BFS.
- Monotonic Queue.
- Sliding Window Minimum.
- Sliding Window Maximum.
41. Khi nào nghĩ đến Monotonic Queue?
Hãy nghĩ tới Monotonic Queue nếu bài có đồng thời các dấu hiệu:
- Cửa sổ trượt.
- Cần min hoặc max.
- Các ứng viên hết hạn theo thứ tự.
- Chỉ số chỉ di chuyển từ trái sang phải.
- Một ứng viên mới có thể làm ứng viên cũ trở nên vô dụng.
- DP cần min/max trên một đoạn chỉ số liên tiếp.
Mẫu đặc biệt quan trọng:
hoặc:
Nếu cửa sổ trượt theo , cần nghĩ ngay tới Monotonic Queue.
42. Tư duy Dominance
Monotonic Queue hoạt động nhờ loại bỏ trạng thái bị dominate.
Nếu ứng viên :
- Mới hơn .
- Tốt hơn hoặc bằng .
thì thường không còn cần thiết.
Ví dụ tìm minimum:
và:
thì bị dominate bởi .
Phản xạ quan trọng:
Không chỉ hỏi phần tử nào tốt nhất hiện tại, mà phải hỏi phần tử nào còn khả năng trở thành tốt nhất trong tương lai.
43. Hai nguyên nhân xóa phần tử khỏi Monotonic Queue
Một phần tử bị xóa vì một trong hai lý do.
Hết hạn
Chỉ số không còn thuộc cửa sổ hiện tại.
Ví dụ:
Bị dominate
Một phần tử mới tốt hơn và tồn tại lâu hơn.
Ví dụ với minimum:
Phân biệt rõ hai nguyên nhân này là chìa khóa để viết đúng Monotonic Queue.
44. Vì sao nên lưu chỉ số thay vì chỉ lưu giá trị?
Trong Sliding Window, cần biết phần tử có hết hạn hay chưa.
Nếu chỉ lưu giá trị, ta không biết nó thuộc vị trí nào.
Do đó thông thường nên lưu:
Sau đó lấy giá trị qua:
Điều này cho phép đồng thời kiểm tra:
- Giá trị.
- Vị trí.
- Hết hạn.
- Dominance.
45. Lỗi thường gặp với BFS
Các lỗi phổ biến:
- Đánh dấu
visitedquá muộn. - Một đỉnh bị đưa vào Queue nhiều lần không cần thiết.
- Không kiểm tra biên trên lưới.
- Quên vật cản.
- Sai trạng thái.
- Dùng BFS cho cạnh có trọng số khác nhau.
- Không lưu đầy đủ state.
- Sai truy vết parent.
Quy tắc quan trọng:
Với BFS thông thường, nên đánh dấu trạng thái ngay khi đưa vào Queue.
46. Lỗi thường gặp với Monotonic Queue
Các lỗi phổ biến:
- Lưu giá trị thay vì chỉ số.
- Kiểm tra hết hạn sai.
- Nhầm dấu
<,<=,>,>=. - Xóa đầu và xóa cuối sai thứ tự.
- Duy trì tăng nhưng lại lấy maximum.
- Duy trì giảm nhưng lại lấy minimum.
- Không hiểu phần tử bằng nhau nên giữ phần tử nào.
Nếu muốn minimum và ưu tiên phần tử mới hơn, thường loại cuối khi:
Nếu muốn giữ phần tử cũ hơn khi bằng nhau, có thể dùng:
Việc chọn dấu phụ thuộc yêu cầu bài toán.
47. Công thức cốt lõi cần nhớ
BFS:
Multi-source BFS:
0-1 BFS:
với:
Prefix Sum:
Tổng đoạn:
Sliding Window Minimum:
Sliding Window Maximum:
DP tối ưu bằng Monotonic Queue:
hoặc:
48. Bản đồ nhận dạng nhanh
Nếu đề nói:
Đi ít bước nhất trên đồ thị không trọng số
Nghĩ tới:
Nhiều nguồn xuất phát cùng lúc
Nghĩ tới:
Cạnh chỉ có trọng số hoặc
Nghĩ tới:
Xử lý đỉnh indegree bằng
Nghĩ tới:
Min hoặc max trong mọi cửa sổ độ dài
Nghĩ tới:
Đoạn dài nhất với
Nghĩ tới:
DP lấy min/max trên trạng thái gần nhất
Nghĩ tới:
Prefix Sum và cần tìm đoạn ngắn nhất thỏa tổng
Nghĩ tới:
49. Cây tư duy chọn cấu trúc
Nếu cần xử lý theo đúng thứ tự xuất hiện:
Nếu cần thao tác cả hai đầu:
Nếu cần phần tử nhỏ nhất hoặc lớn nhất theo độ ưu tiên:
Nếu cần min/max trên cửa sổ trượt:
Nếu cần đường đi ngắn nhất với cạnh bằng :
Nếu trọng số chỉ là và :
50. Các mức cần thành thạo
Mức nền tảng:
- Queue FIFO.
- Push, pop, front.
- Circular Queue.
- Mô phỏng.
Mức đồ thị:
- BFS.
- Grid BFS.
- Flood Fill.
- Multi-source BFS.
- Truy vết BFS.
- State-space BFS.
- Level BFS.
- Kahn Topological Sort.
Mức Deque:
- Deque hai đầu.
- 0-1 BFS.
- Bidirectional BFS.
Mức Monotonic Queue:
- Sliding Window Minimum.
- Sliding Window Maximum.
- Hai Deque min/max.
- Two Pointers kết hợp Deque.
- Prefix Sum kết hợp Deque.
- Shortest Subarray.
- DP Sliding Window Optimization.
- Bounded Knapsack Optimization.
51. Phản xạ cốt lõi
Khi gặp Queue, không nên chỉ hỏi:
Có thể dùng
queuehay không?
Cần hỏi theo thứ tự:
- Trạng thái là gì?
- Thứ tự xử lý trạng thái là gì?
- Cạnh hoặc phép chuyển có chi phí bằng nhau không?
- Có nhiều nguồn hay không?
- Có cần thao tác cả hai đầu không?
- Có cửa sổ trượt không?
- Có truy vấn min/max không?
- Có ứng viên nào bị dominate không?
- Transition DP có phải min/max trên một đoạn trượt không?
Nếu trả lời đúng các câu hỏi này, phần lớn bài Queue trong lập trình thi đấu sẽ trở thành một trong các mẫu quen thuộc.
52. Kết luận
Queue không chỉ là cấu trúc dữ liệu FIFO cơ bản.
Trong lập trình thi đấu, hệ sinh thái Queue gồm:
$$\boxed{ \text{Queue} \rightarrow \text{BFS} \rightarrow \text{Multi-source BFS} \rightarrow \text{Deque} \rightarrow \text{0-1 BFS} \rightarrow \text{Monotonic Queue} \rightarrow \text{DP Optimization} }$$Ba phản xạ quan trọng nhất cần hình thành là:
- FIFO nghĩ đến Queue.
- Đường đi ngắn nhất với bước có cùng chi phí nghĩ đến BFS.
- Min/Max trên một cửa sổ trượt nghĩ đến Monotonic Queue.
Đặc biệt, Monotonic Queue cần được xem là một kỹ thuật độc lập trong lập trình thi đấu, không chỉ là một biến thể nhỏ của Queue, vì nó là cầu nối quan trọng giữa:
$$\text{Deque} + \text{Sliding Window} + \text{Prefix Sum} + \text{Two Pointers} + \text{Dynamic Programming}$$- Người tham gia
- 2
- Tạo bởi