# THI THỬ HSG TỈNH K9 - 2025 - II >[!Note] Thông tin >Sau đây là lời giải của đề thi thử Kỳ thi Chọn HSG tỉnh Bà Rịa - Vũng Tàu K9 lần II (được chuẩn bị theo **ma trận kiến thức của Sở GD&ĐT tỉnh Bà Rịa - Vũng Tàu**) > >**Thời gian:** 20:00 - 22:30 ngày 02/03/2025 > >**Bạn đọc có thể nộp và chấm bài** [TẠI ĐÂY](https://chuyentinbrvt.online/contest/algi_mockhsg9lan2_2025) > >:::info >Chuẩn bị bởi ==team Logica==, thuộc khuôn khổ dự án [**The Algitect (ALGI Project)**](https://www.facebook.com/algitect.project) >::: :::success **Lưu ý:** Lời giải này chỉ hướng đến việc **AC (tức là đạt trọn điểm)** các bài. ::: [toc] ## Bài 1: Nỗi sợ của loài rồng (5 điểm) ### Tóm tắt đề bài: Có $n$ con rồng lần lượt bay đến tấn công lâu đài của Trâm. - Những con rồng thứ ==$k, k \cdot 2, k \cdot 3, \dots$== sẽ bị kẹp đuôi. - Những con rồng thứ ==$l, l \cdot 2, l \cdot 3, \dots$== sẽ bị cắt vuốt. **Yêu cầu:** Đếm số lượng con rồng đã bị Trâm kẹp đuôi hoặc cắt vuốt. **Dữ liệu bảo đảm:** $1 \le k, l \le k \cdot l, n \le 10^{18}$ **Subtasks:** - $30\%$ số điểm: $1 \le k, l \le n \le 10^6$. - $30\%$ số điểm: $k \cdot l \le 10^6$. - $40\%$ số điểm: Không có ràng buộc gì thêm. ### Mô hình hóa bài toán: Bài toán đơn giản là ==đếm số lượng số từ $1$ đến $n$ chia hết cho $k$ hoặc $l$==. >[!Caution] Lưu ý > Những số chia hết cho ==cả $k$ và $l$== chỉ được xem là $1$ số. ### Lời giải >[!Tip] >Ta có công thức sau: >$$ >cnt_x = \left \lfloor \frac{n}{x} \right \rfloor >$$ > Với $cnt_x$ là số lượng bội của $x$ từ $1$ đến $n$. Như vậy, ta có thể tính được số lượng bội của $k$ và $l$ là: $$ \left \lfloor \frac{n}{k} \right \rfloor + \left \lfloor \frac{n}{l} \right \rfloor $$ Tuy nhiên, với phép tính trên, những số ==vừa chia hết cho $k$ vừa chia hết cho $l$== đang bị đếm lặp lại $2$ lần và cần trừ đi. >[!Tip] Nhận xét >Số nhỏ nhất vừa chia hết cho $k$ và vừa chia hết cho $l$ là ==bội chung nhỏ nhất== của $k$ và $l$ >:::success >Vì vậy, ==bội== của ==bội chung nhỏ nhất== của $k$ và $l$ là những số chúng ta cần trừ đi Ta tính được bội chung nhỏ nhất của $k$ và $l$ bằng: $$ \frac{k \cdot l}{\operatorname{GCD}(k, l)} $$ Kết quả bài toán là: $$ \left \lfloor \frac{n}{k} \right \rfloor + \left \lfloor \frac{n}{l} \right \rfloor - \left \lfloor \frac{n}{\frac{k \cdot l}{\operatorname{GCD}(k, l)}} \right \rfloor $$ **Độ phức tạp thời gian:** $O \left( \log \min(k, l) \right)$. > Độ phức tạp trên là độ phức tạp để tính $\operatorname{GCD}$ bằng [**thuật toán Euclid**](https://wiki.vnoi.info/algo/algebra/euclid). :::success :::spoiler Code ```cpp=1 #include <bits/stdc++.h> using namespace std; long long n, k, l; long long __lcm(long long a, long long b) { return (a / __gcd(a, b)) * b; } int main() { cin >> n >> k >> l; cout << (n / k) + (n / l) - (n / __lcm(k, l)); return 0; } ``` ::: ## Bài 2: Mật mã cổ đại (5 điểm) ### Tóm tắt đề bài: Cho $q$ truy vấn, mỗi truy vấn gồm một số nguyên $n$. **Yêu cầu:** In ra các ước nguyên tố của $n$ theo thứ tự giảm dần. **Dữ liệu bảo đảm:** $1 \le q \le 10^5$ và $1 \le n \le 10^6$. **Subtasks:** - $20\%$ số điểm: $1 \le q, n \le 100$. - $40\%$ số điểm: $1 \le q \le 10^4$. - $40\%$ số điểm: Không có ràng buộc gì thêm. ### Mô hình hóa bài toán: Thực chất, bài toán yêu cầu ==phân tích thừa số nguyên tố== của $n$. ### Lời giải: Định nghĩa $E_i$ là ước nguyên tố ==nhỏ nhất== của $i$. >VD: $E_2 = 2, E_9 = 3$. Giả sử đã tính được $E_i$ với $i$ từ $1$ đến $n$, có thể tìm được tất cả các ước số nguyên tố của $n$ bằng cách không ngừng chia $n$ cho $E_n$ đến khi $n = 1$. >VD: Phân tích thừa số nguyên tố của $n = 40$ > - $E_{40} = 2 \rightarrow n = \frac{40}{2} = 20$ > - $E_{20} = 2 \rightarrow n = \frac{20}{2} = 10$ > - $E_{10} = 2 \rightarrow n = \frac{10}{2} = 5$ > - $E_{5} = 5 \rightarrow n = \frac{5}{5} = 1$ > > $n = 40$ có các ước nguyên tố là $2$ và $5$. >[!Tip] > Tạm gọi việc thực hiện tính trước mảng $E$ là ==sàng ước nguyên tố==. Đúng như tên gọi của nó, phương pháp này áp dụng tư tưởng của thuật toán ==sàng nguyên tố==. > :::success > Cụ thể, ta duyệt $i$ tăng dần từ $2$, nếu $i$ chưa tìm được ước nguyên tố $(E_i = 0)$, ta duyệt qua tất cả các bội của $j$ $\left( i, i \cdot 2, i \cdot 3, \dots \right)$ và đánh dấu $E_j = i$. > ::: > Vì với mỗi số, ta duyệt qua các bội của nó, do đó thuật toán trên có độ phức tạp: > $$ > O \left( \frac n1 + \frac n2 + \frac n2 \dots \right) \approx O \left( n \log n \right) > $$ > Với $n$ là giá trị lớn nhất cần sàng tới. > :::warning > Tuy nhiên, trong thực tế, thuật toán trên chạy nhanh hơn rất nhiều vì **ta chỉ duyệt qua bội của những số nguyên tố**. > ::: **Độ phức tạp thời gian:** $O \left( n \log n \right)$. :::success :::spoiler Code ```cpp=1 #include <bits/stdc++.h> using namespace std; const int N = 1e6; int q, n, E[N+1]; void sieve() { for (int i = 2; i <= N; ++i) { if (!E[i]) { for (int j = i; j <= N; j += i) { E[j] = i; } } } } int main() { sieve(); cin >> q; while (q--) { cin >> n; while (n > 1) { int p = E[n]; cout << p << " "; while (n % p == 0) { n /= curr; } } cout << "\n"; } return 0; } ``` ::: ## Bài 3: Chuyển nhà (5 điểm) ### Tóm tắt đề bài: Khoa có $n$ món đồ, món đồ thứ $i$ có khối lượng là $A_i$. Cậu cần xếp các món đồ vào thùng có sức chứa tối đa là $k$. **Yêu cầu:** Đếm số cách để Khoa chọn hai món đồ bỏ vào thùng. **Dữ liệu bảo đảm:** $1 \le n \le 10^6$ và $1 \le A_i, k \le 10^9$. **Subtasks:** - $40\%$ số điểm: $2 \le n \le 10^3$. - $60\%$ số điểm không có ràng buộc gì thêm. ### Mô hình hóa bài toán Thực chất, bài toán yêu cầu đếm số cặp ==$i \lt j$== thỏa ==$A_i + A_j \le k$==. ### Lời giải: Xét trường hợp ta cố định $A_i$, khi đó cần đếm số lượng $j$ thỏa: - $j \gt i$. - $A_j \le k - A_i$. Điều này có thể thực hiện bằng ==tìm kiếm nhị phân== cơ bản. >[!Caution] > Có thể, khoảng $A_j$ thỏa mãn chứa cả $A_i$ mà ta đang cố định. Khi đó, cần trừ $A_i$ ra. **Độ phức tạp thời gian:** $O \left( n \log n \right)$. :::success :::spoiler Code ```cpp=1 #include <bits/stdc++.h> using namespace std; const int N = 1e6; ll n, k, ans, A[N+1]; int main() { cin >> n >> k; for (int i = 1; i <= n; i++){ cin >> A[i]; } sort(A+1, A+1+n); for(int i = 1; i < n; i++){ if (A[i] >= k) break; int pos = upper_bound(A+1, A+1+n, k - a[i]) - A - 1; ans += max(0, pos - i); } cout << ans; return 0; } ``` ::: ## Bài 4: Hộp ma thuật (5 điểm) ### Tóm tắt đề bài: Tí có một chiếc hộp ma thuật và được đưa cho một hằng số $K$. Có $q$ truy vấn, mỗi truy vấn thuộc một trong hai dạng: - `+ x`: Thêm một viên ngọc có sức mạnh $x$ vào hộp. - `- x`: Lấy ra một viên ngọc có sức mạnh $x$ (đảm bảo có ít nhất một viên trong hộp). **Yêu cầu:** Với mỗi truy vấn, tính số cách lấy một số viên ngọc trong hộp để tổng sức mạnh bằng $K$. In ra số cách chia dư cho $998244353$ **Dữ liệu bảo đảm:** $1 \le q \le 10^5$ và $2 \le n \le 10^6$. **Subtasks:** - $20\%$ số điểm: $1 \le q \le 20$. - $40\%$ số điểm: $1 \le q,k \le 200$. - $40\%$ số điểm: Không có ràng buộc gì thêm. ### Mô hình hóa bài toán: Với mỗi truy vấn, đếm số cách chọn một tập con trong tập hợp các số để tổng bằng $K$. Đây là một bài toán ==quy hoạch động cái túi (balo)== điển hình. ### Lời giải: - **Định nghĩa:** ==$dp_i$== là số cách tạo tổng bằng $i$. - **Khởi gán:** - ==$dp_0 = 1$==. - ==$dp_i = 0$== với mọi $i \not = 0$. - **Công thức:** - Khi thêm một giá trị $x$: ==$dp_i = dp_i + dp_{i-x}$== với $i \ge x$. > Với những cách tạo ra tổng $i - x$ ban đầu, có thể thêm giá trị $x$ để tạo tổng $i$. - Khi xóa đi một giá trị $x$: ==$dp_i = dp_i - dp_{i-x}$== > Với những cách tạo ra tổng $i - x$ ban đầu, nay mất đi một giá trị $x$ để tạo tổng $i$. - **Kết quả:** ==$dp_K$==. >[!Caution] Lưu ý: > - Đối với truy vấn `- x` cần phải duyệt $i$ giảm dần để đảm bảo ==$dp_{i-x}$== đang lưu trạng thái của ==truy vấn trước== chứ không phải ==truy vấn hiện tại==. > - Ngược lại, đối với truy vấn `+ x` cần phải duyệt $i$ tăng dần để ==$dp_{i-x}$== lưu trạng thái khi đã xét thêm giá trị $x$. **Độ phức tạp thời gian:** $O \left( q \cdot K \right)$. :::success :::spoiler Code ```cpp=1 #include <bits/stdc++.h> using namespace std; const int N = 5000; const long long M = 998244353; long long q, k, dp[N+1]; int32_t main() { dp[0] = 1; cin >> q >> k; while (q--) { char chr; int x; cin >> chr >> x; if (chr == '+') for (int i = k; i >= x; i--) (dp[i] += dp[i-x]) %= M; if (chr == '-') for (int i = x; i <= k; i++) (((dp[i] -= dp[i-x]) %= M) += M) %= M; cout << dp[k] << "\n"; } return 0; } ``` :::