ICPC Regional Hanoi 2024 · Part 3 of 4
ICPC Regional Hanoi 2024 Editorial - Z
Lời giải hai bài rất khó I và E trong đề.
I: Inversland
Đề bài
Cho mệnh giá tiền, mệnh giá tiền thứ là với là số nguyên tố. Đếm số lượng giá trị hữu tỉ có thể trả được bằng các mệnh giá tiền này mà người bán bắt buộc phải trả lại tiền thừa, modulo .
Lời giải
Ta phát biểu lại bài toán từ các mệnh giá hữu tỉ về các mệnh giá nguyên mà không mất tính tổng quát: Gọi là tích của tất cả các . Nhân mọi mệnh giá với thì mệnh giá thứ trở thành số nguyên . Trong bối cảnh bài toán này ta nói một số tiền nguyên là biểu diễn được nếu nó có thể biểu diễn bằng tổ hợp tuyến tính hệ số không âm của các giá trị , tức là với . Đáp án là số lượng giá trị không biểu diễn được.
Trước tiên ta phát biểu một số kết quả toán học cần thiết để chứng minh lời giải của bài toán này:
Định lý Bezout cho số
Phát biểu:
Với mọi tập gồm số nguyên (), luôn tồn tại bộ số nguyên sao cho:
Chứng minh:
Ta chứng minh bằng quy nạp. Trường hợp cơ sở với là bổ đề Bezout[2] đã được chứng minh.
Giả sử định lý đúng với . Nghĩa là với mọi bộ số nguyên , đặt , luôn tồn tại bộ số nguyên thỏa mãn: . Ta chứng minh mệnh đề đúng với .
Theo tính chất kết hợp của hàm gcd ta có
Áp dụng Bổ đề Bézout cho 2 số và , tồn tại hai số nguyên sao cho:
Thay từ giả thiết quy nạp vào phương trình trên:
Khai triển và nhóm lại:
Vì và các đều là số nguyên, nên (với ) và cũng là các số nguyên. Do đó, ta đã biểu diễn được dưới dạng tổ hợp tuyến tính nguyên của số . Theo nguyên lý quy nạp ta có điều phải chứng minh.
Số Frobenius
Số Frobenius (ký hiệu ) của một tập các số nguyên dương (với điều kiện ) là số nguyên lớn nhất không thể biểu diễn dưới dạng tổ hợp tuyến tính không âm của các số đó. Mô tả bằng công thức: là số nguyên lớn nhất sao cho phương trình:
không có nghiệm nguyên không âm (). Ngược lại, với mọi số nguyên , phương trình luôn có ít nhất một bộ nghiệm .
Định lý Schur-Frobenius
Phát biểu: Cho số nguyên dương () nguyên tố cùng nhau. Khi đó, tồn tại một ngưỡng số nguyên sao cho với mọi số nguyên , phương trình Diophantine:
luôn có ít nhất một bộ nghiệm nguyên không âm .
Chứng minh:
Vì , theo Định lý Bézout, luôn tồn tại các số nguyên (có thể âm) sao cho .
Xét một số dư bất kỳ trong tập . Nhân cả 2 vế của phương trình trên với :
Lấy modulo hai vế cho , ta thu được:
Các hệ số ở trên có thể bị âm. Nếu hệ số nào bị âm, ta chỉ cần cộng thêm vào hệ số đó một lượng (chọn đủ lớn để hệ số trở thành số dương) mà không làm thay đổi số dư khi chia tổng trên cho . Nhờ đó, với mỗi số dư , ta luôn tìm được một số được tạo thành từ tổ hợp không âm của sao cho . Với số dư khả dĩ , ta thu được giá trị .
Đặt . Lấy một số bất kỳ. Giả sử chia dư :
- Do và có cùng số dư khi chia cho , hiệu chia hết cho .
- Do , ta có . Khi này ta có thể viết (với ).
- Vì và vốn đã là tổ hợp không âm của , ta suy ra có thể biểu diễn được bằng tổ hợp không âm của .
Không chỉ vậy, định lý này còn có một kết quả mạnh hơn. Bạn đọc có thể đọc ở đây: Schur's theorem - Wikipedia
Quay trở lại bài toán. Từ định lý Schur-Frobenius ta suy ra một hệ quả trực tiếp là số các mệnh giá không biểu diễn được là hữu hạn, nên không có trường hợp in ra infinite.
Do các đôi một nguyên tố cùng nhau, theo Định lý Số dư Trung Hoa, tồn tại duy nhất bộ số với sao cho:
Đặt . Khi đó luôn viết được dưới dạng với . Ta có bổ đề sau:
Bổ đề: Số là biểu diễn được (tức ).
Chứng minh
Chiều thuận:
Ta có . Do , tất cả hệ số đều
Chiều nghịch:
Giả sử với . Xét modulo , ta có .
Do và , ta bắt buộc có với .
Suy ra .
Từ bổ đề trên, một số là không biểu diễn được khi và chỉ khi:
Để tìm số không thể biểu diễn lớn nhất (số Frobenius ):
- Chọn và chọn các sao cho đạt giá trị lớn nhất, tức là chọn với mọi . Do đó: .
- Biến đổi đại số:
- Thế vào công thức :
Từ công thức trên ta có thể chứng minh được luôn là một số lẻ (bạn đọc có thể tự chứng minh bằng cách xét trường hợp).
Xét một số nguyên . Ta ghép cặp với . Tập hợp gồm đúng số nguyên, được chia thành cặp . Ta chứng minh rằng trong mỗi cặp luôn có đúng một số biểu diễn được và một số không biểu diễn được.
Chứng minh
Ý 1: Không thể cả 2 số cùng biểu diễn được
Nếu cả và đều biểu diễn được, thì tổng của chúng là cũng phải biểu diễn được (tổng 2 tổ hợp tuyến tính không âm là tổ hợp tuyến tính không âm). Điều này mâu thuẫn với định nghĩa là số không biểu diễn được.
Ý 2: Không thể cả 2 số cùng không biểu diễn được
Không mất tính tổng quát, giả sử không biểu diễn được. Theo bổ đề, với . Ta xét số :
Đặt . Vì , ta có . Do , biểu thức trên chính là dạng với . Theo bổ đề, biểu diễn được.
Tổng kết 2 ý ta suy ra trong hai số và phải có đúng 1 số biểu diễn được và 1 số không biểu diễn được, tức điều phải chứng minh.
Do biểu diễn được (), các số không biểu diễn được đều nằm trong khoảng . Mọi số đều biểu diễn được. Vậy tổng số lượng các số nguyên dương không biểu diễn được là:
Cài đặt. Vì có thể trùng với modulo nên ta không được dùng phép chia bằng nghịch đảo modulo. Thay vào đó, ta có thể tính nhanh được cả và bằng tích tiền tố nhân tích hậu tố của các số . Độ phức tạp cho mỗi test là .
Code
#include <iostream>#include <vector>
using namespace std;
static constexpr int MOD = 998244353;static constexpr long long INV2 = (MOD + 1) / 2; // Modular inverse of 2 mod MOD
void solve() { int n; cin >> n;
vector<long long> p(n); for (int i = 0; i < n; ++i) { cin >> p[i]; p[i] %= MOD; }
// Compute suffix products in O(N): suff[i] = p[i] * p[i+1] * ... * p[n-1] % MOD vector<long long> suff(n + 1, 1); for (int i = n - 1; i >= 0; --i) { suff[i] = (suff[i + 1] * p[i]) % MOD; }
long long P = suff[0]; // Total product P = p[0] * ... * p[n-1] % MOD long long S = 0; long long pref = 1;
// Compute S = sum(P / p_i) = sum(pref[i] * suff[i+1]) in O(N) for (int i = 0; i < n; ++i) { long long term = (pref * suff[i + 1]) % MOD; S = (S + term) % MOD; pref = (pref * p[i]) % MOD; }
// Frobenius number F = (n - 1) * P - S (mod MOD) long long F = ((1LL * (n - 1) % MOD * P) - S) % MOD; if (F < 0) F += MOD;
// Genus g = (F + 1) / 2 (mod MOD) long long g = (F + 1) % MOD * INV2 % MOD;
cout << g << "\n";}
int main() { // Fast I/O ios::sync_with_stdio(false); cin.tie(nullptr);
int T; if (cin >> T) { while (T--) { solve(); } } return 0;}E: Easy Game
Đề bài
Bash và Chikapu chơi một trò chơi đối kháng trên một băng giấy gồm ô trống ban đầu. Hai người luân phiên đi. Tại mỗi lượt, chọn 1 ô trống và điền 1 ký tự thuộc xâu vào ô đó.
Người chơi giành chiến thắng ngay lập tức nếu nước đi của mình tạo ra xâu có độ dài liên tiếp trên bảng. Trò chơi kết thúc Hòa nếu điền hết ô mà xâu vẫn chưa xuất hiện. Biết rằng không chứa 3 chữ cái liên tiếp giống nhau.
Xác định kết quả của trò chơi (Bash thắng, Chikapu thắng, hay Hòa) khi cả hai đều chơi tối ưu.
Giới hạn: testcase; ; ; .
Lời giải
Đây là một bài chia trường hợp rất khó và không có đội nào AC trong kì thi. Lời giải dưới đây bổ sung dựa trên sketch ý tưởng của anh RR.
Ta bắt đầu với các nhận xét:
- Sau một nước đi của người X bất kì, nếu nó tạo ra trạng thái mà đối thủ có thể thắng luôn thì đó được gọi là “trạng thái chết”.
- Nếu một người luôn có nước đi không tạo ra trạng thái chết ("nước an toàn") thì người đó không bao giờ thua. Nên nếu cả hai đều có chiến thuật luôn đi được nước an toàn thì kết quả là hòa. Kết quả chỉ khác hoà khi có người bị ép phải tạo ra trạng thái chết.
Các trường hợp dễ
- Có kí tự của không thuộc : không bao giờ tạo được hoà.
- : không đủ chỗ để tạo ra xâu hòa.
- : Bash điền ngay chữ đó ở nước đầu Bash.
- Có chữ thuộc mà không thuộc : chữ cái đó không khớp ô nào của , nên điền nó sẽ luôn an toàn hòa.
Như vậy, ta chỉ còn các trường hợp mà tập các chữ cái của trùng với tập các chữ cái của và

Đây vẫn còn là trường hợp dễ khi ta chỉ cần xét:
- (khi đó ): Bash điền vào ô bất kỳ, vì nên còn ô trống kề bên, Chikapu điền a vào đó và thắngChikapu.
- : nếu một người chơi đi vào bên phải của 1 ô trống hoặc vào bên trái của 1 ô trống sẽ thua luôn. Do đó cả hai sẽ chơi an toàn bằng việc điền vào bên trái ngoài cùng hoặc vào bên phải ngoài cùng hòa.
Cả 3 trường hợp này đều ra kết quả hoà.
- : ta có thể điền ở vị trí đầu tiên để trở thành người đi sau điền trước hoặc sau , điền sau hoặc trước , điền trước hoặc sau . Kết quả hoà.
- hoặc coi như là một trường hợp . Có một chiến thuật để chắc chắn hòa trở lên không phụ thuộc lượt đi là điền vào vị trí trống đầu tiên. Thật vậy vì 3 trạng thái chết có thể có là nên từ một trạng thái chưa chết ta không thể tạo ra trạng thái chết nếu chơi với chiến thuật trên. Như vậy kết quả là hoà.
,

Đây là lớp trường hợp then chốt và khó nhất của bài toán. Khi này ta định nghĩa “bẫy” là chuỗi có dạng trong đó thể hiện 1 ô trống. Ta thấy rằng nếu trên bảng có bẫy, thì người chơi nào điền bất kỳ hoặc vào vị trí của bẫy sẽ thua vì tạo ra trạng thái chết. Do đó khi tạo được bẫy thì người chơi tiếp theo sẽ chỉ có thể điền bên ngoài bẫy này. 2 người chơi sẽ cố gắng tránh bẫy này cho đến khi hết vị trí ở bên ngoài.
Gọi là số ô trống nằm ngoài mọi bẫy ("ô tự do"). Ô tự do luôn có nước an toàn, còn ô trong bẫy thì không. Mỗi nước an toàn vào ô tự do làm giảm 1, và nếu nó tạo thêm bẫy mới (điền cách một chữ có sẵn đúng 3 ô, ở giữa là hai ô ) thì sẽ giảm thêm 2 cho mỗi bẫy mới. Theo đó mỗi nước đi luôn làm giảm một lượng lẻ, nên tính chẵn lẻ của thay đổi sau mỗi nước đi. Không có cách nào giảm số bẫy đi, nên khi mà có bẫy thì người đến lượt sẽ luôn tạo ra trạng thái chết và thua ở lượt sau.
Giả sử người vừa tạo bẫy lúc có ô trống (trước nước đi đó). Sau nước đó có cùng tính chẵn lẻ với , đối thủ đến lượt, và cứ đến lượt thì lại có cùng tính chẵn lẻ ấy. gặp khi và chỉ khi chẵn, tức lẻ. Từ đó ta có kết luận là thắng nếu lẻ, thua nếu chẵn. Áp dụng vào đầu trò chơi: Bash đi mỗi lượt khi cùng tính chẵn lẻ với ; Chikapu đi khi cùng tính chẵn lẻ với . Do đó:
- lẻ: chỉ Bash được lợi khi tạo bẫy. Nếu bẫy xuất hiện thì Bash thắng; Chikapu sẽ không tự tạo bẫy. Nếu không có bẫy nào thì hòa.
- chẵn: ngược lại, chỉ Chikapu được lợi khi tạo bẫy. Nếu bẫy xuất hiện thì Chikapu thắng, nếu không thì hòa.
Từ nhận xét trên, ta có thể chứng minh được thêm khá nhiều trường hợp:
- Với ta có thể duyệt để chứng minh là kết quả luôn hoà.
- Với , Bash điền vào ô số 4 (chính giữa). Bất kể người chơi sau đi thế nào, Bash có thể tạo được bẫy bằng việc điền vào ô 1 hoặc 7. Với mọi lẻ thì có thể chứng minh Bash thắng tương tự vậy.
- Với chẵn Bash không thể thắng bằng chiến thuật này do khi tạo được bẫy thì Bash luôn thua do tính chẵn lẻ của . Ngược lại Bash chỉ có thể cố gắng cầm hoà. Với có thể duyệt để tìm ra chiến thuật hoà là ở nước chơi đầu tiên, Bash phải đặt vào ô chính giữa hoà.
- Với và chẵn, chiến thuật này không còn hiệu quả nữa, do khi Bash đặt ở nước đầu thì bảng chia ra thành 2 phần, 1 phần chẵn và 1 phần lẻ độ dài . Do đó Chikapu sẽ luôn đưa được game về lẻ đi trước và thắng. (Chú ý rằng Bash không thể đi ở nước đầu tiên vì Chikapu sẽ tạo được bẫy và thắng). Đây là trường hợp khó nhất của bài toán và người bình thường không thể tự nghĩ ra được trừ khi ngồi chứng minh chặt chẽ các chiến thuật chơi cho trường hợp này.
Với thì kết quả luôn là hoà.
Pseudocode
def solve(n, s, a): m = len(s) if m > n: return "Oh no!" # target string exceeds board size
set_A, set_S = set(a), set(s)
if m == 1: return "Bash" if s[0] in set_A else "Oh no!" # single character check
if set_A != set_S: return "Oh no!" # character sets must match
if m == 2 and s[0] == s[1]: return "Chikapu" # pattern "aa"
if m == 3 and s[0] == s[2] != s[1]: # pattern "aba" if n % 2 == 1 and n >= 7: return "Bash" if n % 2 == 0 and n >= 16: return "Chikapu"
return "Oh no!"