ICPC Regional Hanoi 2024 · Part 1 of 4
ICPC Regional Hanoi 2024 Editorial - X
Lời giải cho các bài G, F, C, D, L, 5 bài dễ nhất của kì thi.
G: Gid-osh
Đề bài
Cho dãy có phần tử. Bỏ đi chính xác một phần tử sao cho của dãy là nhỏ nhất.
Lời giải
Sắp xếp tăng dần. Nếu thì đáp án là . Nếu không thì ta sẽ bỏ phần tử đầu hoặc cuối đi, đáp án cuối cùng là .
F: FizzBuzz
Đề bài
Cho 3 số nguyên dương , chương trình FizzBuzz sẽ in ra:
fizzbuzznếu chia hết cho cả và .fizznếu chia hết cho nhưng không chia hết cho .buzznếu chia hết cho nhưng không chia hết cho .nnếu không chia hết cho cả và .
Cho số tự nhiên và một xâu có dạng như trên, tìm một bộ số bất kì sao cho và chương trình FizzBuzz với đầu vào trả về đúng xâu đó. In ra -1 nếu không tồn tại thoả mãn.
Lời giải
Ta chỉ cần xét tất cả trường hợp một cách cần thận.
1. Trường hợp là số
Cần không chia hết cho cả và ():
- Trường hợp tổng quát: Chọn và . Khi , phép chia lấy dư và .
- Trường hợp biên:
- : Chọn ( không chia hết cho và ).
- : Không thể chọn vì vượt giới hạn. Chọn và (số dư lần lượt là và ).
- : Chọn và (số dư lần lượt là và ).
2. Trường hợp "fizz"
Cần chia hết cho nhưng không chia hết cho :
- Trường hợp tổng quát (): Chọn và . Ta có và .
- Trường hợp biên (): Chọn (vì chia hết cho nhưng không chia hết cho ).
3. Trường hợp "buzz"
Cần chia hết cho nhưng không chia hết cho (với ):
- Nếu : Không tồn tại đáp án, in
-1 -1.- Điều kiện để chia hết cho với đòi hỏi .
- Với , không có ước nào .
- Với , chọn , nhưng lại chia hết cho nên kết quả bị biến thành
"fizzbuzz".
- Nếu : Chọn và . Ta có , còn (do ).
4. Trường hợp "fizzbuzz"
Cần chia hết cho cả và với :
- Nếu : Không tồn tại cặp vì chỉ có duy nhất ước là , không thỏa mãn . In
-1 -1. - Nếu : Chọn hai ước và . Cả và đều chia hết , đồng thời đảm bảo .
Code
void solve() { int n; string s; cin >> n >> s; if (s[0] != 'f' && s[0] != 'b') { // s is number if (n == 1) cout << 2 << ' ' << 3 << '\n'; else if (n == int(1e9)) cout << int(1e9) - 2 << ' ' << int(1e9) - 1 << '\n'; else if (n == int(1e9 - 1)) { cout << int(1e9) - 2 << ' ' << int(1e9) << '\n'; } else { cout << n + 1 << ' ' << n + 2 << '\n'; } } else { if (s == "fizz") { if (n == (int)1e9) cout << 2 << ' ' << 3 << '\n'; else cout << n << ' ' << n + 1 << '\n'; } else if (s == "buzz") { if (n <= 2) cout << -1 << ' ' << -1 << '\n'; else cout << n - 1 << ' ' << n << '\n'; } else { if (n == 1) { cout << -1 << ' ' << -1 << '\n'; } else { cout << 1 << ' ' << n << '\n'; } } }}C: Classroom Carnival Chaos
Đề bài
Có lớp học, mỗi lớp có học sinh. Đếm số cách xếp học sinh này vào lưới ô vuông sao cho không có hai học sinh nào cùng lớp đứng cùng cột, modulo Giải test.
Giới hạn: , .
Lời giải
Ta cần đếm số cách xếp mà mỗi cột có đúng một học sinh của mỗi lớp. Ta đếm theo hai bước:
- Chọn cột cho từng học sinh. Mỗi lớp có học sinh và có cột, mỗi cột nhận đúng một học sinh của lớp đó. Như vậy mỗi lớp phân bổ học sinh vào các cột bằng một hoán vị, có cách. Với lớp độc lập, ta có cách.
- Xếp học sinh trong cột. Sau bước 1, mỗi cột có đúng học sinh, và họ được xếp vào ô của cột đó theo cách. Với cột độc lập, ta có cách.
Hai bước trên độc lập nên theo quy tắc nhân, đáp án là
Độ phức tạp. Tính và bằng một vòng lặp trong , sau đó với mỗi test dùng lũy thừa nhanh trong .
D: Distinguished Permutation
Đề bài
Cho hoán vị độ dài . Gọi là số đoạn con của hoán vị mà cũng là một hoán vị. Một hoán vị là đặc biệt nếu của nó là lớn nhất trong toàn bộ các hoán vị cùng độ dài với nó.
Cho hai số tự nhiên và . Tìm hoán vị đặc biệt có thứ tự thứ trong tập các hoán vị đặc biệt độ dài sắp xếp theo thứ tự từ điển.
Giới hạn: . không vượt quá số hoán vị đặc biệt có độ dài .
Lời giải
Để đơn giản ta có thể coi thứ tự từ điển tính từ , khi này ta cần tìm hoán vị đặc biệt có thứ tự từ điển .
Ta có nhận xét khá hiển nhiên sau: Một hoán vị có đạt giá trị cực đại là khi và chỉ khi với mọi , tập hợp tạo thành một đoạn con liên tiếp.
Từ đó ta có cách để xây dựng tất cả các hoán vị đặc biệt như sau: bắt đầu từ phần tử , sau đó lần lượt thêm các số từ đến vào đầu hoặc cuối của hoán vị hiện tại. Từ đó ta suy ra được số hoán vị đặc biệt độ dài là . Hơn nữa, việc thêm số vào đầu hoặc cuối phụ thuộc vào bit thứ trong biểu diễn nhị phân của . Nếu bit này bằng 0 thì ta sẽ thêm vào cuối, nếu là 1 thì sẽ thêm vào đầu.
Chứng minh
Gọi là tập các hoán vị đặc biệt độ dài , đã sắp xếp theo thứ tự từ điển. Ta chứng minh mệnh đề sau:
Với mọi và , hoán vị đặc biệt có thứ tự (tính từ ) là hoán vị nhận được bằng cách bắt đầu từ rồi thêm lần lượt , trong đó được thêm vào đầu nếu bit thứ của bằng , và vào cuối nếu bằng .
Với ta có điều phải chứng minh.
Bước cơ sở. Với , và , mệnh đề đúng.
Bước quy nạp. Giả sử mệnh đề đúng với . Gọi là các hoán vị của , với . Theo giả thiết quy nạp, được tạo từ các bit của .
Xét . Bước hiện tại thêm vào đầu hoặc vào cuối, và trước đó ta có một hoán vị . Vậy có dạng hoặc . Hai dạng này cho các hoán vị khác nhau, và được xác định bằng cách xóa . Do đó gồm đúng hoán vị: mỗi cho một hoán vị và một hoán vị .
Ta sắp xếp các hoán vị này:
- Mọi hoán vị có dạng lớn hơn mọi hoán vị có dạng . Hoán vị đầu bắt đầu bằng , còn hoán vị sau bắt đầu bằng một số nhỏ hơn .
- Trong cùng một nhóm, thứ tự giống thứ tự của .
Vậy thứ tự trong là:
Xét hoán vị có thứ tự , với :
- Nếu , đó là . Khi này bit thứ của (có trọng số ) bằng , và được thêm vào cuối.
- Nếu , đó là . Khi này bit thứ của bằng , và được thêm vào đầu. Phần cũng chính là phần tạo từ các bit thấp của .
Trong cả hai trường hợp, với (các bit của ) được tạo theo giả thiết quy nạp, nên các số từ được thêm đúng theo bit thứ của và số được thêm theo bit thứ của . Ta kết luận mệnh đề đúng với .
Độ phức tạp của thuật toán là .
Code
#include <bits/extc++.h>
using namespace std;
typedef long long ll;
void solve() { int n; ll k; cin >> n >> k; --k; deque<int> p; p.push_back(1);
auto get_bit = [&](ll x, int pos) { return (x >> pos) & 1; };
int pivot = min(n, 61);
for (int i = 2; i <= pivot; ++i) { if (get_bit(k, i - 2) == 1) p.push_front(i); else p.push_back(i); } for (int i = pivot + 1; i <= n; ++i) p.push_back(i); for (auto i : p) cout << i << ' '; cout << '\n';}
int main() { cin.tie(0)->sync_with_stdio(0); cin.exceptions(cin.failbit); int tc = 1; cin >> tc; for (int i = 1; i <= tc; ++i) { solve(); }}L: Locomotive Lane Logistics
Đề bài
Cho một đường ray có độ dài và truy vấn. Mỗi truy vấn có 1 trong 2 dạng:
+ l v: thêm 1 tàu có độ dài và vận tốc vào lịch trình.- l v: xoá 1 tàu có độ dài và vận tốc khỏi lịch trình, đảm bảo trong lịch trình hiện tại có ít nhất 1 tàu như vậy.
Mỗi con tàu có phần mui và phần đuôi, khoảng cách giữa mui và đuôi của tàu chính là độ dài của nó. Con tàu sẽ xuất phát với mui chạm vạch xuất phát, đi đều liên tục với vận tốc về phía vạch kết thúc và chỉ ra khỏi đường ray khi đuôi rời khỏi vạch kết thúc. Mỗi con tàu ta có thể chọn cho nó thời điểm xuất phát là số thực bất kì.
Sau mỗi truy vấn, tính tổng thời gian ít nhất để đưa toàn bộ tàu trong danh sách ra khỏi đường ray mà không xảy ra tai nạn. Tai nạn có thể xảy ra giữa 2 tàu ở thời điểm mui của tàu đi sau đâm vào đuôi của tàu đi trước, vận tốc của tàu đi sau lớn hơn tàu đi trước và tàu đi trước chưa ra khỏi đường ray. In ra đáp án tối ưu (luôn có dạng là số hữu tỉ) theo modulo .
Lời giải
Trước tiên ta có thể rút ra 2 nhận xét tham lam đầu tiên về đáp án tối ưu:
- Nếu 2 tàu có cùng vận tốc, thì chúng có thể đi nối đuôi nhau như là 1 tàu (vẫn thoả mãn điều kiện không xảy ra tai nạn), nên ta có thể giả sử vận tốc của tất cả các tàu đều khác nhau.
- Để tối thiểu hoá tổng thời gian, các tàu cần di chuyển nối tiếp nhau khít nhất có thể, tàu chậm sẽ đi trước, rồi sau một khoảng thời gian tối ưu ta sẽ cho tàu nhanh hơn xuất phát và đuổi theo sau. Một cách để chứng minh nhận xét này, là tàu đi sau đã tận dụng triệt để khoảng thời gian tàu đi trước chiếm dụng đường ray để tranh thủ di chuyển được một khoảng cách nhất định.
Tiếp theo đó, bằng việc ngồi nháp toán và chứng minh công thức ta rút ra nhận xét quan trọng nhất của bài toán:
- Giả sử có 2 tàu và với . Ta cho đi trước, đi sau và sẽ có xu hướng đuổi kịp. Để tối ưu thời gian, ta căn chỉnh sao cho ngay thời điểm đuôi rời khỏi đường ray thì mui của cũng vừa chạm vạch kết thúc. Theo đó ta có giãn cách thời gian tối ưu giữa 2 tàu:
- Áp dụng nhận xét trên cho tàu, thì tổng thời gian tối ưu là:
Chứng minh
Gọi là tàu chậm nhất. Với các tàu xuất phát trước , ta có đuôi của mỗi tàu phải rời vạch xuất phát trước khi mui tàu kế tiếp chạm vạch (nếu không hai tàu chồng lên nhau ngay tại vạch), nên tàu chiếm vạch xuất phát ít nhất , và xuất phát không sớm hơn tổng các đó. Bản thân cần thêm để ra khỏi đường ray. Cộng lại, tổng thời gian với mọi cách sắp xếp.
Các tàu xuất phát sau đều nhanh hơn hoặc bằng nó, mui của chúng luôn nằm sau đuôi , nên chúng chỉ băng qua vạch kết thúc sau khi đã ra hẳn; tại vạch kết thúc mỗi lúc chỉ có một tàu băng qua và tàu chiếm vạch đúng . Vậy nên ta thu được một cách dựng đạt được cận dưới của đáp án, nên công thức của nhận xét là tối ưu.
Để ý rằng chặn dưới không phụ thuộc thứ tự của các tàu khác ngoài , nên qua chứng minh ở trên ta thấy rằng thật ra cho tàu nhanh đi trước cũng có thể đạt được đúng giá trị này. Nhận xét "tàu chậm đi trước" chỉ là một cách dựng, không phải điều bắt buộc.
Tổng kết lại, thuật toán cuối cùng của bài chỉ cần duy trì 2 giá trị sau qua các thao tác thêm và xoá tàu khỏi danh sách:
- Vận tốc của tàu chậm nhất trong danh sách hiện tại, có thể dùng
multisethoặcpriority_queue. - Tổng của các tàu đang có trong danh sách hiện tại.
Độ phức tạp cho mỗi truy vấn là .
Code
#include <bits/extc++.h>
using namespace std;
typedef long long ll;
const int mod = 998244353;
int binpow(int a, int b) { int res = 1; while (b) { if (b & 1) res = (ll)res * a % mod; a = (ll)a * a % mod; b >>= 1; } return res;}
int inv(int a) { return binpow(a, mod - 2); }
void solve() { int s, q; cin >> s >> q; multiset<pair<int, int>> ms; int lv_sum = 0; while (q--) { char ch; int l, v; cin >> ch >> l >> v; if (ch == '+') { ms.insert({v, l}); lv_sum += ll(l) * inv(v) % mod; lv_sum %= mod; } else { auto it = ms.find({v, l}); ms.erase(it); lv_sum -= ll(l) * inv(v) % mod; lv_sum = (lv_sum + mod) % mod; } if (sz(ms) == 0) { cout << "0\n"; } else { auto [v1, l1] = *ms.begin(); int ans = (ll(s) * inv(v1) + lv_sum) % mod; ans = (ans + mod) % mod; cout << ans << '\n'; } }}
int main() { cin.tie(0)->sync_with_stdio(0); cin.exceptions(cin.failbit); int tc = 1; // cin >> tc; for (int i = 1; i <= tc; ++i) { solve(); }}