| # | Bài tập | Điểm | Thời gian: | Giới hạn bộ nhớ |
|---|---|---|---|---|
| 1 | IOI 2026 Ngày 1 Bài 1 - Ball Machine | 100 (p) | 1.0s | 2G |
| 2 | IOI 2026 Ngày 1 Bài 2 - Monuments | 100 (p) | 1.0s | 2G |
| 3 | IOI 2026 Ngày 1 Bài 3 - Tiling Game | 100 (p) | 1.0s | 2G |
Madina đã phát minh ra một máy bắn bóng để giải trí cho những thí sinh của IOI. Cấu trúc bên trong của máy như sau:
Bạn không biết \(N\) và không biết nút cha \(P[u]\) của bất kỳ nút \(u\) nào. Thay vào đó, Madina cho bạn biết \(M\) là số lượng nút lá, và chỉ số của các nút lá là \(0, 1, \ldots, M - 1\). Nhiệm vụ của bạn là xác định cấu trúc bên trong của máy bằng cách vận hành nó.
Mỗi nút của máy có thể chứa tối đa một quả bóng, và mỗi quả bóng có một giá trị là một số nguyên không âm. Ta nói rằng một nút có giá trị \(x\) nếu nó chứa một quả bóng có giá trị \(x\). Một nút được coi là bị chiếm dụng nếu nó chứa một quả bóng; ngược lại, nó là trống. Ban đầu, mọi nút đều trống.
Bạn có thể thực hiện hai loại thao tác:
insert(U, X): Chèn một quả bóng mới có giá trị là \(X\) vào nút lá \(U\).false và không chèn quả bóng.true.collect(): Nếu nút gốc trống (nghĩa là máy trống), hàm collect trả về một mảng rỗng. Ngược lại, hàm đệ quy traverse được mô tả bên dưới sẽ thu thập giá trị của tất cả các quả bóng hiện có trong máy vào một mảng \(S\). Ban đầu, mảng \(S\) trống và traverse(N - 1) được gọi.traverse(u):
thêm giá trị của quả bóng tại nút u vào cuối S
gọi c[u] là danh sách các nút con bị chiếm dụng của nút u
sắp xếp c[u] theo thứ tự không giảm dựa trên giá trị
của các quả bóng tại các nút đó (nếu có nhiều nút giá trị giống
nhau, chúng có thể xuất hiện theo bất kỳ thứ tự nào)
for mỗi v thuộc c[u]:
traverse(v)
Sau khi hàm này kết thúc, tất cả các quả bóng sẽ được lấy ra khỏi máy và collect sẽ trả về \(S\).
Ví dụ, xét một máy có \(N = 7\) nút và mảng cha \(P = [4, 6, 5, 5, 6, 6]\). Hình bên trái hiển thị máy trống với các chỉ số nút, trong khi hình bên phải cho thấy một cấu hình khả thi của các quả bóng sau một dãy các thao tác insert (dãy này được đưa ra trong phần Ví dụ).
Khi collect() được gọi, nút gốc (nút \(6\)) bị chiếm dụng, do đó \(S\) được khởi tạo là \(S = []\) và traverse(6) được gọi.
traverse(6): Nút \(6\) có giá trị \(0\); thêm nó vào \(S\) sẽ cho \(S = [0]\). Các nút con của nút \(6\) là \(4\), \(1\) và \(5\), tất cả đều đã được chiếm dụng. Giá trị của chúng lần lượt là \(20\), \(10\) và \(20\). Sắp xếp theo thứ tự không giảm sẽ cho \(c[6] = [1, 5, 4]\) (lưu ý rằng \(c[6] = [1, 4, 5]\) cũng hợp lệ, vì các nút \(4\) và \(5\) có cùng giá trị). Sau đó, hàm này gọi traverse trên từng nút trong \(c[6]\) theo thứ tự đó.
traverse(1): Nút \(1\) có giá trị \(10\); thêm nó vào \(S\) sẽ cho \(S = [0, 10]\). Nút \(1\) không có nút con nào bị chiếm dụng, vì vậy lệnh gọi này kết thúc.traverse(5): Nút \(5\) có giá trị \(20\); thêm nó vào \(S\) sẽ cho \(S = [0, 10, 20]\). Nút \(5\) có một nút con bị chiếm dụng là nút \(2\), vì vậy \(c[5] = [2]\) và hàm này gọi traverse(2).traverse(2): Nút \(2\) có giá trị \(30\); thêm nó vào \(S\) sẽ cho \(S = [0, 10, 20, 30]\). Nút \(2\) không có nút con nào bị chiếm dụng, vì vậy lệnh gọi này kết thúc.traverse(5) đã xử lý tất cả các phần tử con trong \(c[5]\), do đó lệnh gọi hàm này kết thúc.traverse(4): Nút \(4\) có giá trị \(20\); thêm nó vào \(S\) sẽ cho \(S = [0, 10, 20, 30, 20]\). Nút \(4\) không có nút con nào bị chiếm dụng, vì vậy lệnh gọi này kết thúc.Tất cả các lệnh gọi đã kết thúc và không còn nút nào cần xử lý nữa. Cuối cùng, tất cả các quả bóng được lấy ra khỏi máy và collect trả về mảng \(S = [0, 10, 20, 30, 20]\).
Nhiệm vụ của bạn là xác định số lượng nút \(N\) và trả về một mảng các nút cha \(R = [R[0], R[1], \ldots, R[N - 2]]\) mô tả cấu trúc của máy, nhưng với cách đánh chỉ số có thể khác cho các nút không phải là nút lá và không phải là nút gốc. Một cách hình thức, một mảng \(R\) được trả về sẽ được coi là đúng nếu có thể đánh các chỉ số đôi một khác nhau \(L[u]\) với \(0 \le L[u] < N\) cho mỗi nút \(u\) của máy sao cho:
Không bắt buộc \(R[u] > u\). Máy phải trống khi bạn trả về lời giải.
Gọi \(K\) là số lần thực hiện thao tác collect, và \(B\) là giá trị lớn nhất của bất kỳ quả bóng nào trong tất cả các lần gọi insert. Gọi \(C = K + B\). Khi đó, \(C\) không được vượt quá \(1000\). Điểm của bạn trong một số subtask phụ thuộc vào giá trị của \(C\).
Bạn cần xây dựng hàm sau.
std::vector<int> find_structure(int M)
Để tương tác với máy, hàm của bạn có thể gọi hai hàm sau.
Hàm thứ nhất là:
bool insert(int U, int X)
true nếu quả bóng được đặt thành công, hoặc false nếu nút lá \(U\) đã bị chiếm dụng.Hàm thứ hai là:
std::vector<int> collect()
Thu thập giá trị của tất cả các quả bóng đã được đưa vào, làm trống máy và trả về mảng \(S\) đã thu thập.
Nếu tại bất kỳ thời điểm nào trong quá trình thực thi chương trình, giá trị của \(C\) vượt quá \(1000\), giải pháp của bạn sẽ nhận được phản hồi Output isn't correct: Too many resources used.
Cấu trúc của máy được cố định trước khi hàm find_structure được gọi. Trình chấm điểm là đơn định theo nghĩa nếu bạn chạy nó hai lần và cả hai lần chạy đều thực hiện cùng một dãy các thao tác, thì các lệnh gọi đến collect sẽ trả về cùng một mảng.
| Subtask | Điểm | Các ràng buộc thêm |
|---|---|---|
| 1 | 5 | Nút gốc có đúng \(M\) nút con. |
| 2 | 10 | \(M \le 3\) |
| 3 | 25 | \(N \le 200\), \(M \le 45\) |
| 4 | 60 | Không có ràng buộc nào thêm. |
Trong các subtask \(3\) và \(4\), điểm số của bạn phụ thuộc vào giá trị của \(C\) như sau.
| Giới hạn | Điểm |
|---|---|
| \(1000 < C\) | \(0\) |
| \(45 < C \le 1000\) | \(13\) |
| \(C \le 45\) | \(25\) |
| Giới hạn | Điểm |
|---|---|
| \(1000 < C\) | \(0\) |
| \(200 < C \le 1000\) | \(7\) |
| \(71 < C \le 200\) | \(47 - \frac{C}{5}\) |
| \(44 < C \le 71\) | \(104 - C\) |
| \(C \le 44\) | \(60\) |
Xét cùng một máy như trong mô tả nhiệm vụ, với \(N = 7\), \(M = 4\) và \(P = [4, 6, 5, 5, 6, 6]\). Máy đó được minh họa lại trong hình sau:
Trình chấm điểm gọi hàm sau:
find_structure(4)
Hàm có thể thực hiện dãy cuộc gọi sau:
insert(0, 0): Nút lá \(0\) trống, nên một quả bóng có giá trị \(0\) được đặt tại lá \(0\). Nút \(P[0] = 4\) trống, nên quả bóng di chuyển đến nút \(4\). Nút \(P[4] = 6\) trống, nên quả bóng di chuyển đến nút \(6\). Vì nút \(6\) là nút gốc, nên quá trình di chuyển dừng lại. Lệnh gọi trả về true.insert(3, 20): Nút lá \(3\) đang trống, nên một quả bóng có giá trị \(20\) được đặt tại lá \(3\). Nút \(P[3] = 5\) đang trống, nên quả bóng di chuyển đến nút \(5\). Vì \(P[5] = 6\) đang bị chiếm dụng, nên quá trình di chuyển dừng lại. Lệnh gọi trả về true.insert(1, 10): Nút lá \(1\) trống, vì vậy một quả bóng có giá trị \(10\) được đặt tại lá \(1\). Vì \(P[1] = 6\) đã bị chiếm dụng, quả bóng vẫn ở nút \(1\). Lệnh gọi trả về true.insert(2, 30): Nút lá \(2\) đang trống, vì vậy một quả bóng có giá trị \(30\) được đặt tại lá \(2\). Vì \(P[2] = 5\) đã bị chiếm dụng, quả bóng vẫn ở nút \(2\). Lệnh gọi trả về true.insert(1, 25): Nút lá \(1\) đã bị chiếm dụng, do đó quả bóng không được đặt tại nút lá \(1\). Lệnh gọi trả về false.insert(0, 20): Nút lá \(0\) đang trống, nên một quả bóng có giá trị \(20\) được đặt tại nút lá \(0\). Nút \(P[0] = 4\) đang trống, nên quả bóng di chuyển đến nút \(4\). Vì \(P[4] = 6\) đã bị chiếm dụng, nên quá trình di chuyển dừng lại. Lệnh gọi trả về true.Cấu hình các quả bóng thu được đã được thể hiện trong phần mô tả nhiệm vụ.
Tiếp theo, hàm có thể gọi:
collect()
Việc thực thi lệnh gọi này đã được giải thích trong phần mô tả nhiệm vụ, vì vậy lệnh gọi này trả về \(S = [0, 10, 20, 30, 20]\). Sau đó, máy lại trở về trạng thái trống.
Tiếp theo, hàm có thể gọi insert(2, 25). Nút lá \(2\) đang trống, vì vậy một quả bóng có giá trị \(25\) được đặt tại nút lá \(2\). Quả bóng này sau đó di chuyển đến nút \(5\), rồi đến nút \(6\), nơi nó dừng lại. Lệnh gọi trả về true. Cấu hình các quả bóng sau khi thực hiện được thể hiện trong hình dưới đây.
Cuối cùng, có thể gọi hàm collect(), hàm này trả về giá trị \(S = [25]\) và làm trống máy.
Có hai mảng cha hợp lệ mà hàm find_structure có thể trả về: \(R = P = [4, 6, 5, 5, 6, 6]\) (tương ứng với cách đánh chỉ số \(L = [0, 1, 2, 3, 4, 5, 6]\)) và \(R = [5, 6, 4, 4, 6, 6]\) (tương ứng với cách đánh chỉ số \(L = [0, 1, 2, 3, 5, 4, 6]\)). Trong ví dụ này, \(K = 2\) và \(B = 30\), do đó \(C = 32\).
Định dạng dữ liệu vào:
N M
P[0] P[1] P[2] ... P[N-2]
Định dạng kết quả ra:
K B C
Q
R[0] R[1] R[2] ... R[Q-1]
Trong đó, \(Q\) là kích thước của mảng \(R\) được trả về bởi find_structure.
Nguồn: IOI 2026, Ngày 1 — Ball Machine (ballmachine), bản đề chính thức tiếng Việt.
Quảng trường Registan nổi tiếng ở Samarkand có \(N\) di tích được xếp thành một hàng chạy ngang qua trung tâm quảng trường. Các di tích được đánh số từ \(0\) đến \(N - 1\). Cột mốc \(i\) (với \(0 \le i < N\)) ban đầu nằm ở tọa độ nguyên \(X[i]\). Tâm của quảng trường nằm ở tọa độ \(0\).
Các kiến trúc sư yêu cầu sự đối xứng hoàn hảo so với tâm. Họ muốn di chuyển một số (có thể là không) di tích sao cho bố cục cuối cùng đối xứng qua tọa độ \(0\). Một cách hình thức:
Tuy nhiên, có \(M\) di tích là những di tích cổ xưa và rất dễ hỏng nên không thể di dời. Chỉ số của chúng là \(P[0], P[1], \ldots, P[M - 1]\). \(M\) di tích này phải được giữ nguyên tại tọa độ ban đầu. \(N - M\) di tích còn lại có thể được di dời đến bất kỳ tọa độ nguyên nào. Các di tích không gây cản trở lẫn nhau trong quá trình di chuyển.
Chi phí để di dời một di tích từ tọa độ \(a\) đến tọa độ \(b\) là độ chênh lệch tuyệt đối \(\lvert a - b \rvert\). Tổng chi phí là tổng của tất cả các chi phí di chuyển các di tích.
Nhiệm vụ của bạn là tìm tổng chi phí di chuyển nhỏ nhất để các vị trí của các di tích đối xứng qua tọa độ \(0\), hoặc xác định rằng không thể đạt được sự đối xứng như vậy với các ràng buộc đã cho.
Bạn cần cài đặt hàm sau:
long long get_cost(std::vector<int> X, std::vector<int> P)
Hàm phải trả về một số nguyên: tổng chi phí ít nhất để làm cho các vị trí trở nên đối xứng, hoặc \(-1\) nếu điều đó là không thể.
| Subtask | Điểm | Các ràng buộc thêm |
|---|---|---|
| 1 | 3 | \(M = N\) |
| 2 | 4 | \(M = 0\) |
| 3 | 5 | \(X[P[j]] < 0\) với mỗi \(j\) sao cho \(0 \le j < M\). |
| 4 | 6 | \(N \le 10\) |
| 5 | 5 | \(N \le 19\) |
| 6 | 5 | \(N \le 32\) |
| 7 | 13 | \(N \le 200\) |
| 8 | 17 | \(N \le 4000\) |
| 9 | 13 | \(M \le 4000\) |
| 10 | 29 | Không có ràng buộc nào thêm. |
Xét lời gọi hàm sau:
get_cost([-3, -2, 1, 3], [1, 2])
Vị trí ban đầu của các di tích được thể hiện trong hình dưới đây.
Có \(N = 4\) di tích, ban đầu nằm tại các tọa độ \([-3, -2, 1, 3]\). Các chỉ số của các di tích cổ là \(P[0] = 1\) và \(P[1] = 2\). Tức là, các di tích tại các tọa độ \(X[P[0]] = -2\) và \(X[P[1]] = 1\) không thể di chuyển. Các di tích còn lại tại các tọa độ \(-3\) và \(3\) có thể được di chuyển.
Để đạt được tính đối xứng với tổng chi phí nhỏ nhất, ta nên di chuyển các di tích như sau:
Sau các thao tác di chuyển này, vị trí các di tích trở nên đối xứng.
Tổng chi phí di chuyển là \(2 + 1 = 3\), do đó hàm cần trả về \(3\).
Xét lời gọi hàm sau:
get_cost([2, 2, 2, 3], [])
Trong đó, \(M = 0\), có nghĩa là không có di tích nào là cổ xưa, và tất cả các di tích đều có thể được di dời. Một giải pháp có tổng chi phí tối thiểu là di dời tất cả các di tích đến tọa độ \(0\). Chi phí cho mỗi di tích là:
Tổng chi phí là \(2 + 2 + 2 + 3 = 9\). Do đó, thủ tục cần trả về \(9\). Lưu ý rằng có các giải pháp khác có cùng tổng chi phí.
Xét lời gọi hàm sau:
get_cost([1, 2, 3, 4], [0, 1, 2, 3])
Tất cả \(4\) di tích đều là di tích cổ và không thể di dời. Vì tọa độ của chúng đều là số dương, nên không thể tạo ra một cấu hình đối xứng qua trục \(0\). Do đó, hàm cần trả về kết quả là \(-1\).
Định dạng dữ liệu vào:
N M
X[0] X[1] ... X[N-1]
P[0] P[1] ... P[M-1]
Lưu ý rằng dòng thứ ba có thể trống nếu \(M\) bằng \(0\).
Định dạng kết quả ra:
C
Trong đó, \(C\) là giá trị trả về bởi hàm get_cost.
Nguồn: IOI 2026, Ngày 1 — Monuments (monuments), bản đề chính thức tiếng Việt.
Barchin và Charos đang chơi một trò chơi trên một lưới gồm \(2N \times 2M\) ô vuông đơn vị. Các hàng được đánh số từ \(0\) đến \(2N - 1\) từ trên xuống dưới, và các cột được đánh số từ \(0\) đến \(2M - 1\) từ trái sang phải. Với \(0 \le i < 2N\) và \(0 \le j < 2M\), ta ký hiệu ô ở hàng \(i\) và cột \(j\) là \((i, j)\).
Barchin đưa cho Charos lần lượt \(N \cdot M\) khối. Mỗi khối là một hình vuông \(2 \times 2\) gồm bốn viên gạch \(1 \times 1\). Barchin đã tô màu đen hoặc trắng cho mọi viên gạch trong khối, đồng thời đảm bảo rằng ít nhất một viên gạch có màu trắng.
Charos phải đặt mỗi khối lên lưới ngay lập tức sau khi nhận được, mà không biết mình sẽ nhận được những khối nào sau đó. Các khối không thể xoay. Mỗi khối phải được đặt hoàn toàn bên trong lưới, che phủ chính xác bốn ô lưới. Hơn nữa, viên gạch trên cùng bên trái của mỗi khối phải che phủ một ô có tọa độ hàng và cột đều là số chẵn. Mỗi ô trong lưới phải được bao phủ bởi tối đa một khối.
Barchin thắng trò chơi nếu, sau khi bất kỳ khối nào được đặt xuống, tồn tại một hình vuông \(2 \times 2\) gồm các ô được phủ bởi bốn viên gạch đen. Một cách hình thức, nếu các ô \((a, b)\), \((a + 1, b)\), \((a, b + 1)\), \((a + 1, b + 1)\) đều được phủ bởi các viên gạch đen với \(0 \le a < 2N - 1\) và \(0 \le b < 2M - 1\), thì Barchin thắng. Các chỉ số \(a\) và \(b\) không cần phải là số chẵn.
Charos thắng nếu cô ấy đặt tất cả \(N \cdot M\) khối mà không để Barchin thắng. Lưu ý rằng việc đặt \(N \cdot M\) khối sẽ phủ kín toàn bộ lưới.
Nhiệm vụ của bạn là cài đặt một chiến lược để Charos giành chiến thắng trong trò chơi. Có thể chứng minh rằng, với các điều kiện ràng buộc đã cho, Charos luôn có thể sắp xếp các khối sao cho đảm bảo chiến thắng, bất kể màu sắc của các khối mà cô ấy nhận được sau này là gì.
Bạn cần cài đặt hai hàm sau:
void init(int N, int M)
N: bằng một nửa số hàng trong lưới.M: bằng một nửa số cột trong lưới.std::pair<int, int> receive_block(int TL, int TR, int BL, int BR)
TL, TR, BL, BR: lần lượt là màu của các viên gạch góc trên bên trái, góc trên bên phải, góc dưới bên trái và góc dưới bên phải của khối hiện tại, như được minh họa trong hình dưới đây. Mỗi giá trị có thể là \(0\) (trắng) hoặc \(1\) (đen).init.Hàm này phải trả về một cặp số nguyên \((i, j)\), trong đó \(i\) là tọa độ hàng và \(j\) là tọa độ cột của ô mà viên gạch góc trên bên trái của khối này sẽ được đặt vào. Cả \(i\) và \(j\) đều phải là số chẵn, và vùng \(2 \times 2\) được khối này che phủ không được chồng chéo lên bất kỳ khối nào đã được đặt trước đó.
Nếu receive_block trả về một cặp số không thỏa mãn các yêu cầu này, hoặc nếu sau khi đặt khối, một ô vuông \(2 \times 2\) các ô bị che phủ hoàn toàn bởi các viên gạch màu đen, trình chấm điểm sẽ ngay lập tức kết thúc chương trình của bạn và kết quả cho trường hợp test sẽ là Output isn't correct.
Hành vi của hệ thống chấm điểm là không thích ứng. Điều này có nghĩa là chuỗi các khối mà Barchin giao cho Charos đã được xác định trước khi hàm init được gọi.
Với mỗi khối, gọi \(S\) là số viên gạch màu đen trong bốn viên của khối đó. Tức là, \(S = TL + TR + BL + BR\).
| Subtask | Điểm | Các ràng buộc thêm |
|---|---|---|
| 1 | 6 | \(S = 1\) cho mỗi khối và \(N = 2\). |
| 2 | 16 | \(S = 3\) cho mỗi khối. \(N = M\), \(N\) là số chẵn, và mỗi cách trong bốn cách có thể tô màu khối xuất hiện chính xác \(\frac{N^2}{4}\) lần. |
| 3 | 10 | \(S = 1\) cho mỗi khối. |
| 4 | 29 | \(S \le 2\) cho mỗi khối. |
| 5 | 39 | Không có ràng buộc nào thêm. |
Xét một trò chơi với \(N = 1\) và \(M = 2\), do đó lưới có \(2\) hàng và \(4\) cột. Trình chấm đầu tiên gọi:
init(1, 2)
Ban đầu, tất cả các ô đều trống. Lưới trông như sau:
Có \(N \cdot M = 2\) khối cần đặt. Giả sử Barchin đưa ra một khối gồm ba viên gạch đen và một viên gạch trắng ở góc trên bên phải. Trình chấm gọi:
receive_block(1, 0, 1, 1)
Charos quyết định đặt khối này ở phía bên trái của lưới bằng cách trả về \((0, 0)\).
Lưới bây giờ trông như sau:
Sau đó, Barchin đưa ra một khối khác gồm ba viên gạch đen và một viên gạch trắng ở góc trên bên trái:
receive_block(0, 1, 1, 1)
Ô duy nhất còn lại có hàng chẵn và cột chẵn có thể đóng vai trò là góc trên bên trái của khối \(2 \times 2\) là \((0, 2)\), vì vậy Charos trả về \((0, 2)\). Lưới cuối cùng sẽ như sau:
Không có ô vuông \(2 \times 2\) được phủ kín hoàn toàn bằng các viên gạch màu đen, vì vậy Charos đã đặt thành công tất cả các khối mà Barchin không bao giờ thắng. Charos thắng trò chơi.
Định dạng dữ liệu vào:
N M
TL[0] TR[0] BL[0] BR[0]
TL[1] TR[1] BL[1] BR[1]
...
TL[NM-1] TR[NM-1] BL[NM-1] BR[NM-1]
Định dạng kết quả ra:
R[0] C[0]
R[1] C[1]
...
R[NM-1] C[NM-1]
Trong đó, \(R[k]\) và \(C[k]\) là cặp số nguyên được trả về bởi lần gọi thứ \(k\) của hàm receive_block.
Nguồn: Đề bài chính thức IOI 2026, bản tiếng Việt (VNM), ngày 1 — Tiling Game (tiling).