Hướng dẫn cho TLEoj Contest #04 - A sussy problem


Chỉ sử dụng khi thực sự cần thiết như một cách tôn trọng tác giả và người viết hướng dẫn này.

Chép code từ bài hướng dẫn để nộp bài là hành vi có thể dẫn đến khóa tài khoản.

Authors: huyhau6a2

Subtask 1: n\le 12

Tutorial

Vì giới hạn n nhỏ, ta có thể backtrack tất cả trò chơi khác nhau, tổng cộng 2^n trò chơi. Với mỗi trò chơi, ta kiểm tra bằng cách xét từng cách imposustor giết một, nếu có ít nhất một cách thỏa mãn thì đó là trò chơi gay cấn, trong mỗi trò chơi có 2^i cách imposustor chọn người (với i là số imposustor). Độ phức tạp để duyệt mỗi trò chơi là O(2^i\times n)

Vì số testcase là quá lớn, thay vì ta backtrack từng test thì ta lưu trước kết quả của từng giá trị n từ 1 đến 12 rồi xuất ra kết quả trong O(1)

Subtask 2: n\le 20

Tutorial

Giới hạn n vẫn nhỏ, ta thử suy nghĩ đến cách kiểm tra trò chơi nhanh hơn:

Ta có thể tham lam để kiểm tra như sau: Ta sẽ ưu tiên về việc imposustor giết người ở bên trái, trong trường hợp không thể giết người ở bên trái ta mới duyệt bên phải, như vậy độ phức tạp chỉ còn O(n)

Ta cũng làm tương tự như subtask 1 là lưu trước kết quả của từng giá trị n từ 1 đến 20 rồi xuất ra kết quả trong O(1)

Subtask 3: n\le 1000

Subtask 4: n\le 10^6

Tutorial

Theo ý tưởng duyệt tham lam mà ta đã nói ở subtask 2, ta có thể coi như 1 imposustor là 1 nhóm gồm imposustor vị trí i và 1 crewmate vị trí i-1 hoặc i+1. Ví dụ: Trong trò chơi 0110, ta có thể chia 1 và 2 vào 1 nhóm; 3 và 4 vào 1 nhóm.

Bài toán từ đây đã được đơn giản hóa, chuyển sang quy hoạch động.

Ta gọi dp[i][j]\ (1\le i\le n, 0\le j<3) là số cách chia nhóm tới vị trí thứ i, và trạng thái ở ô cuối cùng là j. Trong đó:

  • j=0: Ô cuối cùng là crewmate an toàn
  • j=1: 2 ô cuối cùng tương ứng với imposustor ở vị trí i, còn crewmate ở vị trí i-1, imposustor vị trí i sẽ giết crewmate vị trí i-1
  • j=2: 2 ô cuối cùng tương ứng với imposustor ở vị trí i-1, còn crewmate ở vị trí i, imposustor vị trí i-1 sẽ giết crewmate vị trí i
Công thức
  • dp[i][0]=dp[i-1][0]+dp[i-1][1]+dp[i-1][2]
  • dp[i][1]=dp[i-2][0]+dp[i-2][1]+dp[i-2][2]
  • dp[i][2]=dp[i-2][1]+dp[i-2][2]

?: Tại sao công thức dp[i][2] lại không tính giá trị dp[i-2][0]

Giải thích

Theo cách tham lam ở trước, ta buộc phải ưu tiên về việc imposustor tấn công từ bên trái, trong khi công thức dp[i][2] sẽ ưu tiên tấn công từ bên phải. Về cơ bản, ta biết ô i-2 là ô crewmate an toàn, ô i-1 là imposustor và ô i là crewmate, theo tham lam imposustor phải tấn công vị trí i-2

Sau khi tiền xử lý trước mảng dp, ta có thể xuất kết quả ra trong O(1)

Độ phức tạp: O(n)

Subtask 5: n\le 5\times 10^7

Tutorial

Giả sử ta lưu tất cả giá trị dp thì chắc chắn sẽ MLE trong subtask này.

Giải thích

Ta biết một kiểu dữ liệu int là 4 byte hay long long là 8 byte trong C++. Giả sử ta lưu tất cả kết quả từ 1 đến 5\times 10^7 thì sẽ cần lưu 15\times 10^7 giá trị. Nếu chúng ta sử dụng kiểu dữ liệu int để lưu từng số thì bộ nhớ tổng cộng là 600MB, khác biệt rất lớn so với memory limit là 256MB

Ý tưởng chính trong subtask này là ta sắp xếp lại truy vấn, ưu tiên xuất ra kết quả với n nhỏ, xong mới tới n lớn. Ở đây ta không cần lưu hết các kết quả từ 1 đến 5\times 10^7 nên ta có thể tối ưu để giảm bộ nhớ

Độ phức tạp: O(n)

Subtask 6: n\le 10^{18}

Tutorial

Dựa vào công thức dp, ta có thể suy ra được công thức truy hồi theo ma trận như sau:

\begin{pmatrix} dp_{i-2,0} & dp_{i-2,1} & dp_{i-2,2} & dp_{i-1,0} & dp_{i-1,1} & dp_{i-1,2} \end{pmatrix} \times \begin{pmatrix} 0 & 0 & 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 0 & 1 & 1\\ 0 & 0 & 0 & 0 & 1 & 1\\ 1 & 0 & 0 & 1 & 0 & 0\\ 0 & 1 & 0 & 1 & 0 & 0\\ 0 & 0 & 1 & 1 & 0 & 0 \end{pmatrix} = \begin{pmatrix} dp_{i-1,0} & dp_{i-1,1} & dp_{i-1,2} & dp_{i,0} & dp_{i,1} & dp_{i,2} \end{pmatrix}

Ta đặt p_k=\begin{pmatrix} dp_{k-1,0} & dp_{k-1,1} & dp_{k-1,2} & dp_{k,0} & dp_{k,1} & dp_{k,2} \end{pmatrix} và M=\begin{pmatrix} 0 & 0 & 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 0 & 1 & 1\\ 0 & 0 & 0 & 0 & 1 & 1\\ 1 & 0 & 0 & 1 & 0 & 0\\ 0 & 1 & 0 & 1 & 0 & 0\\ 0 & 0 & 1 & 1 & 0 & 0 \end{pmatrix}

Tới đây ta suy ra được p_k=p_{k-1}\times M=p_2\times M^{k-2}

Trong đó p_2=\begin{pmatrix} 1 & 0 & 0 & 1 & 1 & 1 \end{pmatrix}

Tới đây, ta có thể sử dụng nhân ma trận để tính M^{k-2} xong nhân với p_2 và giải quyết trong O(6^3\times log(n)) mỗi testcase, nhưng điều này là không đủ để giúp bạn AC bài này. Ta cần một cách tối ưu hơn

Ý tưởng: Ta sẽ tính trước M^1,M^2,M^4,\dots,M^{2^{59}}. Ta không cần phải tính M^{k-2} mà có thể chia k-2=2^{a_1}+2^{a_2}+2^{a_3}+\dots+2^{a_x}\ (a_i>a_{i+1}) sau đó sẽ nhân p_2 với lần lượt từng M^{2^{a_1}},M^{2^{a_2}},M^{2^{a_3}},\dots,M^{2^{a_x}}. Ta có thể tách k-2 thành dãy a bằng biểu diễn nhị phân. Độ phức tạp cho cách trên sẽ là O(6^2\times log(n)), đủ để AC bài này



Bình luận

Không có bình luận nào.