Type to search.

ICPC Regional Hanoi 2024 · Part 1 of 4

Length: 1941 wordsReading time: 14 min read

ICPC Regional Hanoi 2024 Editorial - X

Published:


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 aa có nn phần tử. Bỏ đi chính xác một phần tử sao cho max⁡−min⁡\max-\min của dãy là nhỏ nhất.

Lời giải

Sắp xếp aa tăng dần. Nếu n=2n=2 thì đáp án là 00. Nếu không thì ta sẽ bỏ phần tử đầu hoặc cuối đi, đáp án cuối cùng là min⁡(an−1−a1,an−a2)\min(a_{n - 1} - a_1, a_n - a_2).

F: FizzBuzz 

Đề bài

Cho 3 số nguyên dương n,a,bn,a,b, chương trình FizzBuzz sẽ in ra:

  • fizzbuzz nếu nn chia hết cho cả aa và bb.

  • fizz nếu nn chia hết cho aa nhưng không chia hết cho bb.

  • buzz nếu nn chia hết cho bb nhưng không chia hết cho aa.

  • n nếu nn không chia hết cho cả aa và bb.

Cho số tự nhiên n(1≤n≤109)n \left(1\le n\le10^9\right) và một xâu có dạng như trên, tìm một bộ số a,ba,b bất kì sao cho 1<a<b≤1091<a<b\le10^9 và chương trình FizzBuzz với đầu vào nn trả về đúng xâu đó. In ra -1 nếu không tồn tại a,ba,b 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 ss là số

Cần nn không chia hết cho cả aa và bb (1≤a<b≤1091 \le a < b \le 10^9):

  • Trường hợp tổng quát: Chọn a=n+1a = n + 1 và b=n+2b = n + 2. Khi n<n+1<n+2n < n + 1 < n + 2, phép chia lấy dư n mod a=n≠0n \bmod a = n \neq 0 và n mod b=n≠0n \bmod b = n \neq 0.
  • Trường hợp biên:
    • n=1n = 1: Chọn a=2,b=3a = 2, b = 3 (11 không chia hết cho 22 và 33).
    • n=109n = 10^9: Không thể chọn n+1n+1 vì vượt giới hạn. Chọn a=109−2a = 10^9 - 2 và b=109−1b = 10^9 - 1 (số dư lần lượt là 22 và 11).
    • n=109−1n = 10^9 - 1: Chọn a=109−2a = 10^9 - 2 và b=109b = 10^9 (số dư lần lượt là 11 và 109−110^9 - 1).

2. Trường hợp s=s = "fizz"

Cần nn chia hết cho aa nhưng không chia hết cho bb:

  • Trường hợp tổng quát (n<109n < 10^9): Chọn a=na = n và b=n+1b = n + 1. Ta có n mod n=0n \bmod n = 0 và n mod (n+1)=n≠0n \bmod{(n+1)} = n \neq 0.
  • Trường hợp biên (n=109n = 10^9): Chọn a=2,b=3a = 2, b = 3 (vì 10910^9 chia hết cho 22 nhưng không chia hết cho 33).

3. Trường hợp s=s = "buzz"

Cần nn chia hết cho bb nhưng không chia hết cho aa (với a<ba < b):

  • Nếu n≤2n \le 2: Không tồn tại đáp án, in -1 -1.
    • Điều kiện để nn chia hết cho bb với b>a≥1b > a \ge 1 đòi hỏi b≥2b \ge 2.
    • Với n=1n = 1, không có ước nào ≥2\ge 2.
    • Với n=2n = 2, chọn b=2  ⟹  a=1b = 2 \implies a = 1, nhưng 22 lại chia hết cho 11 nên kết quả bị biến thành "fizzbuzz".
  • Nếu n>2n > 2: Chọn b=nb = n và a=n−1a = n - 1. Ta có n mod n=0n \bmod n = 0, còn n mod (n−1)=1≠0n \bmod{(n-1)} = 1 \neq 0 (do n≥3n \ge 3).

4. Trường hợp s=s = "fizzbuzz"

Cần nn chia hết cho cả aa và bb với 1≤a<b1 \le a < b:

  • Nếu n=1n = 1: Không tồn tại cặp (a,b)(a, b) vì 11 chỉ có duy nhất ước là 11, không thỏa mãn a<ba < b. In -1 -1.
  • Nếu n≥2n \ge 2: Chọn hai ước a=1a = 1 và b=nb = n. Cả 11 và nn đều chia hết nn, đồng thời đảm bảo 1<n≤1091 < n \le 10^9.

Code

C++
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ó nn lớp học, mỗi lớp có mm học sinh. Đếm số cách xếp n×mn \times m học sinh này vào lưới ô vuông n×mn \times m sao cho không có hai học sinh nào cùng lớp đứng cùng cột, modulo 109+7.10^9+7. Giải TT test.

Giới hạn: 1≤T≤1051\le T \le10^5, 1≤n,m≤1071 \le n, m \le 10^7.

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:

  1. Chọn cột cho từng học sinh. Mỗi lớp có mm học sinh và có mm 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ó m!m! cách. Với nn lớp độc lập, ta có (m!)n(m!)^n cách.
  2. Xếp học sinh trong cột. Sau bước 1, mỗi cột có đúng nn học sinh, và họ được xếp vào nn ô của cột đó theo n!n! cách. Với mm cột độc lập, ta có (n!)m(n!)^m cách.

Hai bước trên độc lập nên theo quy tắc nhân, đáp án là (m!)n⋅(n!)m(mod109+7).(m!)^{n}\cdot(n!)^{m}\pmod{10^9+7}.

Độ phức tạp. Tính n!n! và m!m! bằng một vòng lặp trong O(max⁡(n,m))O(\max(n,m)), sau đó với mỗi test dùng lũy thừa nhanh trong O(log⁡n+log⁡m)O(\log n+\log m).

D: Distinguished Permutation 

Đề bài

Cho hoán vị độ dài nn. Gọi F(P)F(P) là số đoạn con của hoán vị PP mà cũng là một hoán vị. Một hoán vị PP là đặc biệt nếu F(P)F(P) 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 nn và kk. Tìm hoán vị đặc biệt có thứ tự thứ kk trong tập các hoán vị đặc biệt độ dài nn sắp xếp theo thứ tự từ điển.

Giới hạn: 1≤n≤105,1≤k≤10181 \le n \le 10^5,1 \le k \le 10^{18}. kk không vượt quá số hoán vị đặc biệt có độ dài nn.

Lời giải

Để đơn giản ta có thể coi thứ tự từ điển tính từ 00, khi này ta cần tìm hoán vị đặc biệt có thứ tự từ điển k−1k-1.

Ta có nhận xét khá hiển nhiên sau: Một hoán vị PP có F(P)F(P) đạt giá trị cực đại là nn khi và chỉ khi với mọi L∈[1,n]L \in [1, n], tập hợp {1,2,…,L}\{1, 2, \dots, L\} 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ử 11, sau đó lần lượt thêm các số ii từ 22 đến nn 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 nn là 2n−12^{n-1}. Hơn nữa, việc thêm số ii vào đầu hoặc cuối phụ thuộc vào bit thứ i−2i-2 trong biểu diễn nhị phân của k−1k-1. 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 SnS_n là tập các hoán vị đặc biệt độ dài nn, đã sắp xếp theo thứ tự từ điển. Ta chứng minh mệnh đề sau:

Với mọi n≥1n\ge1 và 0≤r<2n−10\le r<2^{n-1}, hoán vị đặc biệt có thứ tự rr (tính từ 00) là hoán vị nhận được bằng cách bắt đầu từ [1][1] rồi thêm lần lượt i=2,…,ni=2,\dots,n, trong đó ii được thêm vào đầu nếu bit thứ i−2i-2 của rr bằng 11, và vào cuối nếu bằng 00. 

Với r=k−1r=k-1 ta có điều phải chứng minh.

Bước cơ sở. Với n=1n=1, S1=[1]S_1={[1]} và r=0r=0, mệnh đề đúng.

Bước quy nạp. Giả sử mệnh đề đúng với n−1n-1. Gọi T0<T1<⋯<TM−1T_0<T_1<\dots<T_{M-1} là các hoán vị của Sn−1S_{n-1}, với M=2n−2M=2^{n-2}. Theo giả thiết quy nạp, TjT_j được tạo từ các bit của jj.

Xét P∈SnP\in S_n. Bước hiện tại thêm nn vào đầu hoặc vào cuối, và trước đó ta có một hoán vị T∈Sn−1T\in S_{n-1}. Vậy PP có dạng [T,n][T,n] hoặc [n,T][n,T]. Hai dạng này cho các hoán vị khác nhau, và TT được xác định bằng cách xóa nn. Do đó SnS_n gồm đúng 2M2M hoán vị: mỗi TjT_j cho một hoán vị [Tj,n][T_j,n] và một hoán vị [n,Tj][n,T_j].

Ta sắp xếp các hoán vị này:

  • Mọi hoán vị có dạng [n,T][n,T] lớn hơn mọi hoán vị có dạng [T,n][T,n]. Hoán vị đầu bắt đầu bằng nn, còn hoán vị sau bắt đầu bằng một số nhỏ hơn nn.
  • Trong cùng một nhóm, thứ tự giống thứ tự của TjT_j.

Vậy thứ tự trong SnS_n là: 

[T0,n], [T1,n], …, [TM−1,n], [n,T0], [n,T1], …, [n,TM−1].[T_0,n],\ [T_1,n],\ \dots,\ [T_{M-1},n],\ [n,T_0],\ [n,T_1],\ \dots,\ [n,T_{M-1}].

Xét hoán vị có thứ tự rr, với 0≤r<2M=2n−10\le r<2M=2^{n-1}:

  • Nếu r<Mr<M, đó là [Tr,n][T_{r},n]. Khi này bit thứ n−2n-2 của rr (có trọng số 2n−2=M2^{n-2}=M) bằng 00, và nn được thêm vào cuối.
  • Nếu r≥Mr\ge M, đó là [n,Tr−M][n,T_{r-M}]. Khi này bit thứ n−2n-2 của rr bằng 11, và nn được thêm vào đầu. Phần Tr−MT_{r-M} cũng chính là phần tạo từ các bit thấp của rr.

Trong cả hai trường hợp, TjT_j với j=r mod Mj=r\bmod M (các bit 0,…,n−30,\dots,n-3 của rr) được tạo theo giả thiết quy nạp, nên các số ii từ 2,…,n−12,\dots,n-1 được thêm đúng theo bit thứ i−2i-2 của rr và số nn được thêm theo bit thứ n−2n-2 của rr. Ta kết luận mệnh đề đúng với nn. ■\blacksquare

Độ phức tạp của thuật toán là O(n)\mathcal{O}(n).

Code

C++
#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 ss và qq truy vấn. Mỗi truy vấn có 1 trong 2 dạng:

  1. + l v: thêm 1 tàu có độ dài ll và vận tốc vv vào lịch trình.

  2. - l v: xoá 1 tàu có độ dài ll và vận tốc vv 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 ll 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 vv 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 998244353998244353.

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:

  1. 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.
  2. Để 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:

  1. Giả sử có 2 tàu T1=(l1,v1)T_1 = (l_1, v_1) và T2=(l2,v2)T_2 = (l_2, v_2) với v1<v2v_1 < v_2. Ta cho T1T_1 đi trước, T2T_2 đi sau và T2T_2 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 T1T_1 rời khỏi đường ray thì mui của T2T_2 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:
s+l1v1−sv2=sv1−sv2+l1v1\frac{s + l_1}{v_1} - \frac{s}{v_2} = \frac{s}{v_1} - \frac{s}{v_2} + \frac{l_1}{v_1}
  1. Áp dụng nhận xét trên cho nn tàu, thì tổng thời gian tối ưu là:
sv1−svn+∑i=1n−1livi+s+lnvn=sv1+∑i=1nlivi\frac{s}{v_1} - \frac{s}{v_n} + \sum_{i=1}^{n-1} \frac{l_i}{v_i} + \frac{s + l_n}{v_n} = \frac{s}{v_1} + \sum_{i=1}^n \frac{l_i}{v_i}
Chứng minh

Gọi T1T_1 là tàu chậm nhất. Với các tàu xuất phát trước T1T_1, 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 i≠1i \neq 1 chiếm vạch xuất phát ít nhất livi\frac{l_i}{v_i}, và T1T_1 xuất phát không sớm hơn tổng các livi\frac{l_i}{v_i} đó. Bản thân T1T_1 cần thêm s+l1v1\frac{s + l_1}{v_1} để ra khỏi đường ray. Cộng lại, tổng thời gian ≥sv1+∑i=1nlivi\ge \frac{s}{v_1} + \sum_{i=1}^n \frac{l_i}{v_i} với mọi cách sắp xếp. 

Các tàu xuất phát sau T1T_1 đều nhanh hơn hoặc bằng nó, mui của chúng luôn nằm sau đuôi T1T_1, nên chúng chỉ băng qua vạch kết thúc sau khi T1T_1 đã 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 ii chiếm vạch đúng livi\frac{l_i}{v_i}. 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 T1T_1, 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:

  1. Vận tốc v1v_1 của tàu chậm nhất trong danh sách hiện tại, có thể dùng multiset hoặc priority_queue.
  2. Tổng livi\frac{l_i}{v_i} 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à O(log⁡q)\mathcal{O}(\log q).

Code

C++
#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();
}
}