APIO 2025

Bộ đề bài

# Bài tập Điểm Thời gian: Giới hạn bộ nhớ
1 APIO 2025 - Hack! 100 (p) 3.0s 1G
2 APIO 2025 - Permutation Game 100 (p) 2.0s 1G
3 APIO 2025 - Rotating Lines 100 (p) 3.0s 1G

1. APIO 2025 - Hack!

Điểm: 100 (p) Thời gian: 3.0s Bộ nhớ: 1G Input: bàn phím Output: màn hình

Sau một giờ thi Codeforces, bạn nhận thấy một thí sinh khác trong phòng đã giải được một bài bằng unordered_set. Đã đến lúc hack!

Bạn biết rằng unordered_set sử dụng một bảng băm gồm \(n\) bucket, được đánh số từ \(0\) đến \(n-1\). Bạn không biết \(n\) và muốn xác định giá trị này.

Khi chèn số nguyên \(x\) vào bảng băm, nó được đưa vào bucket thứ \((x \bmod n)\). Nếu bucket đó đang có \(b\) phần tử thì phép chèn gây ra \(b\) xung đột băm.

Với một truy vấn gồm \(k\) số nguyên phân biệt \(x[0],x[1],\ldots,x[k-1]\), trình tương tác trả về tổng số xung đột phát sinh khi lần lượt chèn chúng vào một unordered_set rỗng. Chi phí của truy vấn là \(k\).

Ví dụ, nếu \(n=5\)\(x=[2,15,7,27,8,30]\) thì có tổng cộng \(4\) xung đột:

Thao tác Xung đột mới Các bucket
Ban đầu - [], [], [], [], []
Chèn \(x[0]=2\) 0 [], [], [2], [], []
Chèn \(x[1]=15\) 0 [15], [], [2], [], []
Chèn \(x[2]=7\) 1 [15], [], [2, 7], [], []
Chèn \(x[3]=27\) 2 [15], [], [2, 7, 27], [], []
Chèn \(x[4]=8\) 0 [15], [], [2, 7, 27], [8], []
Chèn \(x[5]=30\) 1 [15, 30], [], [2, 7, 27], [8], []

Mỗi truy vấn sử dụng một unordered_set rỗng mới, nên các truy vấn hoàn toàn độc lập.

Hãy tìm \(n\) với tổng chi phí không quá \(1\,000\,000\).

Chi tiết cài đặt

Bạn cần cài đặt hàm:

C++
int hack()

Hàm phải trả về giá trị bí mật \(n\). Trong một test, trình chấm có thể gọi hack() nhiều lần; mỗi lời gọi là một kịch bản độc lập.

Trong hack(), bạn có thể gọi:

C++
long long collisions(std::vector<long long> x)
  • x là một mảng các số nguyên phân biệt, với \(1\le x[i]\le 10^{18}\).
  • Hàm trả về tổng số xung đột khi lần lượt chèn các phần tử của x vào một unordered_set rỗng.
  • Có thể gọi hàm nhiều lần. Trong mỗi lời gọi hack(), tổng độ dài của x qua tất cả các truy vấn không được vượt quá \(1\,000\,000\).

hack() có thể được gọi nhiều lần, hãy lưu ý dữ liệu còn lại từ lời gọi trước, đặc biệt là trạng thái trong các biến toàn cục.

Giới hạn chi phí \(1\,000\,000\) áp dụng riêng cho từng kịch bản. Nếu có \(t\) lời gọi hack(), tổng chi phí có thể đạt \(t\cdot 1\,000\,000\), nhưng mỗi lời gọi vẫn không được vượt quá \(1\,000\,000\).

Trình tương tác không thích ứng: các giá trị \(n\) được cố định trước khi quá trình tương tác bắt đầu.

Ví dụ

Giả sử có hai kịch bản. Trong lời gọi hack() đầu tiên, bạn thực hiện:

Lời gọi Giá trị trả về
collisions([2, 15, 7, 27, 8, 30]) 4
collisions([1, 2, 3]) 0
collisions([10, 20, 30, 40, 50]) 10

Từ đó, nếu xác định được \(n=5\), hack() phải trả về \(5\).

Trong lời gọi hack() tiếp theo, giả sử bạn thực hiện:

Lời gọi Giá trị trả về
collisions([1, 3]) 1
collisions([2, 4]) 1

Giá trị \(n\) duy nhất phù hợp là \(2\), nên hack() phải trả về \(2\).

Ràng buộc

  • \(1\le t\le 10\), trong đó \(t\) là số kịch bản.
  • \(2\le n\le 10^9\).
  • \(1\le x[i]\le 10^{18}\) trong mọi lời gọi collisions().

Phân nhóm

  1. 8 điểm: \(n\le 500\,000\).
  2. 17 điểm: \(n\le 1\,000\,000\).
  3. 75 điểm: Không có ràng buộc bổ sung.

Trong subtask 3, bạn có thể nhận điểm thành phần. Gọi \(q\) là tổng chi phí lớn nhất của một lời gọi hack() trong tất cả các test thuộc subtask. Điểm của subtask được tính như sau:

Điều kiện Điểm
\(q>1\,000\,000\) \(0\)
\(110\,000<q\le 1\,000\,000\) \(75\cdot\log_{50}\left(\dfrac{10^6}{q-90\,000}\right)\)
\(q\le 110\,000\) \(75\)

Nếu trong bất kỳ test nào, lời gọi collisions() vi phạm các ràng buộc ở trên hoặc hack() trả về sai, lời giải nhận \(0\) điểm cho subtask đó.

Trình chấm mẫu

Trình chấm mẫu đọc:

t
n_1
n_2
...
n_t

Với mỗi kịch bản, gọi \(m\) là giá trị hack() trả về và \(c\) là tổng chi phí truy vấn. Trình chấm mẫu in:

m c

Bạn có thể tải trình chấm mẫu và tệp tiêu đề ở phần đính kèm.


Nguồn: APIO 2025, bài 1 - Hack!.

2. APIO 2025 - Permutation Game

Điểm: 100 (p) Thời gian: 2.0s Bộ nhớ: 1G Input: bàn phím Output: màn hình

Alice và Bob là bạn thời thơ ấu và rất thích các trò chơi trí tuệ. Hôm nay, họ chơi một trò chơi mới trên đồ thị.

Bộ trò chơi gồm một đồ thị liên thông có \(m\) đỉnh, đánh số từ \(0\) đến \(m-1\), và \(e\) cạnh, đánh số từ \(0\) đến \(e-1\). Cạnh thứ \(i\) nối hai đỉnh \(u[i]\)\(v[i]\).

Bộ trò chơi còn có một hoán vị \(p[0],p[1],\ldots,p[n-1]\) độ dài \(n\), trong đó \(m\le n\). Điểm của \(p\) là số chỉ số \(i\) thỏa mãn \(p[i]=i\).

Trò chơi kéo dài tối đa \(10^{100}\) lượt. Trong mỗi lượt:

  1. Nếu Alice quyết định kết thúc, trò chơi dừng lại.
  2. Nếu không, Alice chọn \(m\) chỉ số phân biệt \(t[0],t[1],\ldots,t[m-1]\), với \(0\le t[i]<n\). Không yêu cầu \(t[0]<t[1]<\cdots<t[m-1]\).
  3. Bob chọn một cạnh có chỉ số \(j\), \(0\le j<e\), rồi hoán đổi \(p[t[u[j]]]\)\(p[t[v[j]]]\).

Alice muốn tối đa hóa điểm cuối cùng của hoán vị, còn Bob muốn tối thiểu hóa nó. Điểm tối ưu là điểm cuối cùng nếu cả hai cùng chơi tối ưu.

Bạn cần xác định chính xác điểm tối ưu, sau đó chơi với Bob đủ số lượt để đạt điểm ít nhất bằng điểm tối ưu. Chiến lược của Alice phải đúng với mọi cách đi của Bob, kể cả khi Bob không đi tối ưu.

Chi tiết cài đặt

Bạn cần cài đặt hàm:

C++
int Alice(int m, int e, std::vector<int> u, std::vector<int> v,
          int n, std::vector<int> p)
  • m: số đỉnh của đồ thị.
  • e: số cạnh của đồ thị.
  • u, v: hai mảng độ dài \(e\) mô tả các cạnh.
  • n: độ dài hoán vị.
  • p: hoán vị ban đầu, có độ dài \(n\).
  • Hàm được gọi đúng một lần và phải trả về điểm tối ưu của trò chơi.

Trong hàm này, bạn có thể gọi:

C++
int Bob(std::vector<int> t)
  • t phải có đúng \(m\) phần tử phân biệt, mỗi phần tử thuộc \([0,n-1]\).
  • Hàm trả về chỉ số cạnh \(j\) duy nhất thỏa mãn \(0\le j<e\); trình chấm đồng thời thực hiện phép hoán đổi tương ứng.
  • Có thể gọi hàm nhiều lần.

Ví dụ

Xét lời gọi:

C++
Alice(5, 6, [4, 0, 3, 1, 4, 2], [2, 2, 0, 2, 0, 3],
      10, [8, 2, 7, 6, 1, 5, 0, 9, 3, 4])

Đồ thị tương ứng:

Ban đầu, \(p=[8,2,7,6,1,5,0,9,3,4]\). Có thể chứng minh điểm tối ưu là \(1\).

Giả sử Alice thực hiện bốn lượt minh họa sau:

t truyền cho Bob Giá trị Bob trả về Hai chỉ số của p p sau phép đổi
[3, 1, 5, 2, 0] 5 5, 2 [8, 2, 5, 6, 1, 7, 0, 9, 3, 4]
[9, 3, 7, 2, 1] 0 1, 7 [8, 9, 5, 6, 1, 7, 0, 2, 3, 4]
[5, 6, 7, 8, 9] 1 5, 7 [8, 9, 5, 6, 1, 2, 0, 7, 3, 4]
[7, 5, 2, 3, 6] 3 5, 2 [8, 9, 2, 6, 1, 5, 0, 7, 3, 4]

Các lượt trên chỉ để minh họa, Alice và Bob không nhất thiết đi tối ưu. Alice cũng có thể dừng ngay từ đầu vì điểm ban đầu đã bằng \(1\).

Sau bốn lượt, điểm thực tế là \(3\)\(p[2]=2\), \(p[5]=5\), \(p[7]=7\). Tuy vậy, Alice() vẫn phải trả về điểm tối ưu \(1\). Trả về \(3\) sẽ bị chấm sai.

Ràng buộc

  • \(2\le m\le 400\).
  • \(m-1\le e\le 400\).
  • \(0\le u[i],v[i]<m\).
  • \(m\le n\le 400\).
  • \(0\le p[i]<n\).
  • Đồ thị liên thông, không có khuyên và không có cạnh trùng.
  • \(p\) là một hoán vị: \(p[i]\ne p[j]\) với mọi \(i\ne j\).

Phân nhóm

  1. 6 điểm: \(m=2\).
  2. 6 điểm: \(e>m\).
  3. 10 điểm: \(e=m-1\).
  4. 24 điểm: \(e=m=3\).
  5. 24 điểm: \(e=m=4\).
  6. 30 điểm: \(e=m\).

Mỗi subtask có thể cho điểm thành phần. Gọi \(k\) là số lượt, tức số lần gọi Bob(), và gọi \(r\) là giá trị lớn nhất của \(k/n\) trên tất cả các test trong subtask. Điểm subtask được nhân với hệ số:

Điều kiện Hệ số
\(r\ge 12\) \(0\)
\(3<r<12\) \(1-\log_{10}(r-2)\)
\(r\le 3\) \(1\)

Do đó, dùng không quá \(3n\) lượt sẽ nhận trọn điểm subtask. Dùng từ \(12n\) lượt trở lên sẽ nhận \(0\) điểm và được hiển thị là kết quả sai.

Trình chấm mẫu

Trình chấm mẫu đọc:

m e
u[0] v[0]
u[1] v[1]
...
u[e-1] v[e-1]
n
p[0] p[1] ... p[n-1]

Trình chấm mẫu in lần lượt hoán vị cuối, giá trị Alice() trả về, điểm thực tế của hoán vị cuối và số lượt đã dùng.

Bạn có thể tải trình chấm mẫu và tệp tiêu đề ở phần đính kèm.


Nguồn: APIO 2025, bài 2 - Permutation Game.

3. APIO 2025 - Rotating Lines

Điểm: 100 (p) Thời gian: 3.0s Bộ nhớ: 1G Input: bàn phím Output: màn hình

Asadullo là một nhà nghiên cứu xuất sắc tại APIO (Alliance for Power and Industrial Optimization). Ông đang nghiên cứu cách tạo năng lượng bằng một loại vật liệu chưa từng được biết đến.

\(n\) thanh rất dài làm từ vật liệu này, được mô tả bởi mảng \(v[0],v[1],\ldots,v[n-1]\). Thanh thứ \(i\) nằm ở góc

\[ a[i]=360\cdot\frac{v[i]}{100000}^{\circ} \]

so với chiều dương của trục \(x\), theo chiều ngược kim đồng hồ.

Hiệu suất năng lượng của các thanh là

\[ \sum_{i<j}\operatorname{acute}(i,j), \]

trong đó \(\operatorname{acute}(i,j)\) là góc nhọn giữa thanh \(i\) và thanh \(j\); trong bài này, góc \(90^{\circ}\) cũng được coi là góc nhọn. Cụ thể,

\[ \operatorname{acute}(i,j)=\min\bigl(|v[i]-v[j]|,\ 50000-|v[i]-v[j]|\bigr). \]

Ví dụ, với \(v=[5000,12500,37500]\) thì \(a=[18,45,135]\) độ:

Ta có \(\operatorname{acute}(0,1)=7500\) (tức \(27^{\circ}\)), \(\operatorname{acute}(0,2)=17500\) (tức \(63^{\circ}\)), và \(\operatorname{acute}(1,2)=25000\) (tức \(90^{\circ}\)). Hiệu suất bằng \(50000\).

Asadullo muốn xoay các thanh để tối đa hóa hiệu suất, với các hạn chế:

  • Thiết bị cho phép chọn nhiều thanh và xoay đồng thời tất cả chúng cùng một góc.
  • Sau mỗi thao tác, hiệu suất năng lượng không được giảm.
  • Tổng số thanh được chọn qua tất cả các thao tác không được vượt quá \(2\,000\,000\).

Hãy giúp Asadullo đạt hiệu suất năng lượng lớn nhất có thể.

Chi tiết cài đặt

Bạn cần cài đặt hàm:

C++
void energy(int n, std::vector<int> v)
  • n: số thanh.
  • v: mảng độ dài \(n\) mô tả vị trí ban đầu của các thanh.
  • Hàm được gọi đúng một lần.

Trong hàm này, bạn có thể gọi:

C++
void rotate(std::vector<int> t, int x)
  • t là mảng các chỉ số phân biệt: \(0\le t[i]<n\)\(t[i]\ne t[j]\) với mọi \(i<j\). Không yêu cầu t được sắp xếp.
  • Thao tác xoay đồng thời mọi thanh có chỉ số trong t theo tham số x: với mỗi \(i\) thuộc t, \(v[i]\) trở thành \((v[i]+x)\bmod 50000\).
  • Có thể gọi hàm nhiều lần. Tổng độ dài của t qua tất cả các lời gọi không được vượt quá \(2\,000\,000\).

Ví dụ 1

Xét lời gọi:

C++
energy(2, [20000, 10000])

Ban đầu \(v=[20000,10000]\) và hiệu suất bằng \(10000\). Một kịch bản hợp lệ:

  1. Gọi rotate([0, 1], 8000). Khi đó \(v=[28000,18000]\) và hiệu suất không đổi.
  2. Gọi rotate([0], 15000). Khi đó \(v=[43000,18000]\) và hiệu suất trở thành \(25000\).

\(25000\) là hiệu suất lớn nhất có thể cho đầu vào này.

Ví dụ 2

Xét lời gọi:

C++
energy(3, [5000, 12500, 37500])

Đây là cấu hình trong hình phía trên. Hiệu suất ban đầu đã lớn nhất, nên không cần thao tác nào.

Ràng buộc

  • \(2\le n\le 100\,000\).
  • \(0\le v[i]\le 49\,999\) với mọi \(0\le i<n\).
  • Các phần tử của \(v\) không nhất thiết phân biệt.

Phân nhóm

  1. 5 điểm: \(n=2\).
  2. 11 điểm: \(v[i]<25\,000\) với mọi \(0\le i<n\).
  3. 8 điểm: \(n\le 10\).
  4. 15 điểm: \(n\le 100\).
  5. 15 điểm: \(n\le 300\).
  6. 20 điểm: \(n\le 2000\).
  7. 26 điểm: Không có ràng buộc bổ sung.

Trình chấm mẫu

Trình chấm mẫu đọc:

n
v[0] v[1] ... v[n-1]

Trình chấm mẫu in hiệu suất năng lượng cuối cùng. Ngoài ra, thông tin chi tiết về các thao tác xoay được ghi vào log.txt.

Bạn có thể tải trình chấm mẫu và tệp tiêu đề ở phần đính kèm.


Nguồn: APIO 2025, bài 3 - Rotating Lines.